Dado que quiero usar correctamente las anotaciones de tipo para las tuplas con nombre del módulo de escritura:
from typing import NamedTuple, List class Foo(NamedTuple): my_list: List[int] = [] foo1 = Foo() foo1.my_list.append(42) foo2 = Foo() print(foo2.my_list) # prints [42]¿Cuál es la mejor o la forma más limpia de evitar la miseria del valor predeterminado mutable en Python? Tengo algunas ideas, pero nada parece realmente bueno.
Uso de None como predeterminado
class Foo(NamedTuple): my_list: Optional[List[int]] = None foo1 = Foo() if foo1.my_list is None foo1 = foo1._replace(my_list=[]) # super ugly foo1.my_list.append(42) Sobrescribir __new__ o __init__ no funcionará:
AttributeError: Cannot overwrite NamedTuple attribute __init__ AttributeError: Cannot overwrite NamedTuple attribute __new__ Especial @classmethod
class Foo(NamedTuple): my_list: List[int] = [] @classmethod def use_me_instead(cls, my_list=None): if not my_list: my_list = [] return cls(my_list) foo1 = Foo.use_me_instead() foo1.my_list.append(42) # works! ¿Quizás usar frozenset y evitar los atributos mutables por completo? Pero eso no funcionará con Dict s ya que no hay frozendict s.
¿Alguien tiene una buena respuesta?
Utilice una clase de datos en lugar de una tupla con nombre. Una clase de datos permite que un campo especifique una fábrica predeterminada en lugar de un único valor predeterminado.
from dataclasses import dataclass, field @dataclass(frozen=True) class Foo: my_list: List[int] = field(default_factory=list)EDITAR : Actualizado para usar el enfoque de Alex, porque esto funciona mucho mejor que mi idea anterior.
Aquí hay una clase de Foo de Alex puesta en un decorador:
from typing import NamedTuple, List, Callable, TypeVar, cast, Type T = TypeVar('T') def default_factory(**factory_kw: Callable) -> Callable[[Type[T]], Type[T]]: def wrapper(wcls: Type[T]) -> Type[T]: def du_new(cls: Type[T], **kwargs) -> T: for key, factory in factory_kw.items(): if key not in kwargs: kwargs[key] = factory() return super(cls, cls).__new__(cls, **kwargs) # type: ignore[misc] return type(f'{wcls.__name__}_', (wcls, ), {'__new__': du_new}) return wrapper @default_factory(my_list=list) class Foo(NamedTuple): my_list: List[int] = [] # you still need to define the default argument foo1 = Foo() foo1.my_list.append(42) foo2 = Foo() print(foo2.my_list) # prints [] #reveal_type(foo2) # prints Tuple[builtins.list[builtins.int], fallback=foo.Foo]EDITAR :
Combinando mi enfoque con la idea de Sebastian Wagner de usar un decorador , podemos lograr algo como esto:
from typing import NamedTuple, List, Callable, TypeVar, Type, Any, cast from functools import wraps T = TypeVar('T') def default_factory(**factory_kw: Callable[[], Any]) -> Callable[[Type[T]], Type[T]]: def wrapper(wcls: Type[T], /) -> Type[T]: @wraps(wcls.__new__) def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: for key, factory in factory_kw.items(): kwargs.setdefault(key, factory()) new = super(cls, cls).__new__(cls, *args, **kwargs) # type: ignore[misc] # This call to cast() is necessary if you run MyPy with the --strict argument return cast(T, new) cls_name = wcls.__name__ wcls.__name__ = wcls.__qualname__ = f'_{cls_name}' return type(cls_name, (wcls, ), {'__new__': __new__, '__slots__': ()}) return wrapper @default_factory(my_list=list) class Foo(NamedTuple): # You do not *need* to have the default value in the class body, # but it makes MyPy a lot happier my_list: List[int] = [] foo1 = Foo() foo1.my_list.append(42) foo2 = Foo() print(f'foo1 list: {foo1.my_list}') # prints [42] print(f'foo2 list: {foo2.my_list}') # prints [] print(Foo) # prints <class '__main__.Foo'> print(Foo.__mro__) # prints (<class '__main__.Foo'>, <class '__main__._Foo'>, <class 'tuple'>, <class 'object'>) from inspect import signature print(signature(Foo.__new__)) # prints (_cls, my_list: List[int] = []) Ejecútelo a través de MyPy, y MyPy nos informa que el tipo revelado de foo1 y foo2 sigue siendo "Tuple[builtins.list[builtins.int], fallback=__main__.Foo]"
Respuesta original a continuación.
¿Qué tal esto? (Inspirado por esta respuesta aquí ):
from typing import NamedTuple, List, Optional, TypeVar, Type class _Foo(NamedTuple): my_list: List[int] T = TypeVar('T', bound="Foo") class Foo(_Foo): "A namedtuple defined as `_Foo(mylist)`, with a default value of `[]`" __slots__ = () def __new__(cls: Type[T], mylist: Optional[List[int]] = None) -> T: mylist = [] if mylist is None else mylist return super().__new__(cls, mylist) # type: ignore f, g = Foo(), Foo() print(isinstance(f, Foo)) # prints "True" print(isinstance(f, _Foo)) # prints "True" print(f.mylist is g.mylist) # prints "False" Ejecútelo a través de MyPy y el tipo revelado de f y g será: "Tuple[builtins.list[builtins.int], fallback=__main__.Foo]" .
No estoy seguro de por qué tuve que agregar el # type: ignore para que MyPy dejara de quejarse; si alguien me puede aclarar eso, estaría interesado. Parece funcionar bien en tiempo de ejecución.