typing.NamedTuple 和可变默认参数

问题描述 投票:0回答:3

鉴于我想正确使用打字模块中命名元组的类型注释:

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]

避免 Python 中可变默认值痛苦的最佳或最干净的方法是什么?我有一些想法,但似乎没有什么是好的

  1. 使用

    None
    作为默认值

    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)
    
  2. 覆盖

    __new__
    __init__
    不起作用:

    AttributeError: Cannot overwrite NamedTuple attribute __init__
    AttributeError: Cannot overwrite NamedTuple attribute __new__
    
  3. 特别

    @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!
    
  4. 也许使用

    frozenset
    并完全避免可变属性?但这不适用于
    Dict
    ,因为没有
    frozendict

有人有好的答案吗?

python python-typing namedtuple
3个回答
7
投票

使用数据类而不是命名元组。数据类允许字段指定默认的factory,而不是单个默认值。

from dataclasses import dataclass, field


@dataclass(frozen=True)
class Foo:
    my_list: List[int] = field(default_factory=list)

3
投票

编辑:更新为使用Alex的方法,因为这比我之前的想法要好得多。

这是放入装饰器中的 Alex 的

Foo
类:

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]

2
投票

编辑

将我的方法与 Sebastian Wagner 的使用装饰器的想法相结合,我们可以实现这样的目标:

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] = [])

通过MyPy运行它,MyPy告诉我们

foo1
foo2
的显示类型仍然是
"Tuple[builtins.list[builtins.int], fallback=__main__.Foo]"

原答案如下。


这个怎么样? (受到这里的答案的启发):

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"

通过 MyPy 运行它,

f
g
的显示类型将是:
"Tuple[builtins.list[builtins.int], fallback=__main__.Foo]"

我不知道为什么我必须添加

# type: ignore
才能让 MyPy 停止抱怨——如果有人能启发我,我会很感兴趣。似乎在运行时工作正常。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.