【问题标题】:How to initialize a NamedTuple child class different ways based on input arguments?如何根据输入参数以不同方式初始化 NamedTuple 子类?
【发布时间】:2020-05-16 22:21:31
【问题描述】:

我正在构建一个可以接受不同初始化方式的 typing.NamedTuple 类(see typing.NamedTuple docs here,或者它继承自旧的 collections.namedtuples docs)。

为什么在这种情况下使用 NamedTuple?我希望它是不可变的和自动哈希的,所以它可以是一个字典键,我不必编写哈希函数。

我知道我需要使用 __new__ 而不是 __init__,因为 NamedTuples 是不可变的(例如,see this Q&A。我已经搜索过并且有一些花絮(例如answers to this question on setting up a custom hash for a namedtuple),但是我无法让一切正常工作,我收到一个关于无法覆盖 __new__ 的错误。

这是我当前的代码:

from typing import NamedTuple

class TicTacToe(NamedTuple):
    """A tic-tac-toe board, each character is ' ', 'x', 'o'"""
    row1: str = '   '
    row2: str = '   '
    row3: str = '   '

    def __new__(cls, *args, **kwargs):
        print(f'Enter __new__ with {cls}, {args}, {kwargs}')
        if len(args) == 1 and args[0] == 0:
            new_args = ('   ', '   ', '   ')
        else:
            new_args = args
        self = super().__new__(cls, *new_args, *kwargs)
        return self

if __name__ == '__main__':
    a = TicTacToe(('xo ', 'x x', 'o o'))
    print(a)
    b = TicTacToe(0)
    print(b)

但我收到以下错误:

Traceback (most recent call last):
  File "c:/Code/lightcc/OpenPegs/test_namedtuple.py", line 4, in <module>
    class TicTacToe(NamedTuple):
  File "C:\Dev\Python37\lib\typing.py", line 1384, 
in __new__
    raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
AttributeError: Cannot overwrite NamedTuple attribute __new__

我不能为继承自 NamedTuple 的子类创建单独的 __new__ 函数吗?从消息中可以看出,它正在尝试直接覆盖 NamedTuple 的 __new__,而不是 TicTacToe 类。

这是怎么回事?

【问题讨论】:

    标签: python inheritance constructor immutability namedtuple


    【解决方案1】:

    您可以通过定义classmethod 来避免定义__new__()。在下面的示例代码中,我简单地将其命名为make()。它类似于 collections.namedtype 子类具有的名为 _make() 的类方法。

    这是向任何类提供“替代构造函数”的常用方法。

    请注意,我还更改了对函数的第一次调用,以便将参数正确地传递给 make() 方法。

    from typing import NamedTuple
    
    class TicTacToe(NamedTuple):
        """A tic-tac-toe board, each character is ' ', 'x', 'o'."""
        row1: str = '   '
        row2: str = '   '
        row3: str = '   '
    
        @classmethod
        def make(cls, *args, **kwargs):
            print(f'Enter make() with {cls}, {args}, {kwargs}')
            if len(args) == 1 and args[0] == 0:
                new_args = ('   ', '   ', '   ')
            else:
                new_args = args
            self = cls(*new_args, *kwargs)
            return self
    
    if __name__ == '__main__':
    #    a = TicTacToe.make(('xo ', 'x x', 'o o'))
        a = TicTacToe.make('xo ', 'x x', 'o o')
        print(a)
        b = TicTacToe.make(0)
        print(b)
    

    输出:

    Enter make() with <class '__main__.TicTacToe'>, ('xo ', 'x x', 'o o'), {}
    TicTacToe(row1='xo ', row2='x x', row3='o o')
    Enter make() with <class '__main__.TicTacToe'>, (0,), {}
    TicTacToe(row1='   ', row2='   ', row3='   ')
    

    更新

    无法重载NamedTuple 子类'__new__() 方法的另一种解决方法是将派生类拆分为两个类,一个是公共的,一个是私有的,这样前者就不再是NamedTuple.

    这样做的一个好处是,不再需要使用像上面的make() 这样的专用类方法来创建实例。

    这就是我的意思:

    from typing import NamedTuple
    
    class _BaseBoard(NamedTuple):
        """Private base class for tic-tac-toe board."""
        row1: str = '   '
        row2: str = '   '
        row3: str = '   '
    
    
    class TicTacToe(_BaseBoard):
        """A tic-tac-toe board, each character is ' ', 'x', 'o'."""
        __slots__ = ()  # Prevent creation of a __dict__.
    
        @classmethod
        def __new__(cls, *args, **kwargs):
            print(f'Enter __new__() with {cls}, {args}, {kwargs}')
            if len(args) == 1 and args[0] == 0:
                new_args = ('   ', '   ', '   ')
            else:
                new_args = args
            self = super().__new__(*new_args, *kwargs)
            return self
    
    
    if __name__ == '__main__':
    
        a = TicTacToe('xo ', 'x x', 'o o')
        print(a)
        assert getattr(a, '__dict__', None) is None  # Verify not being created.
        b = TicTacToe(0)
        print(b)
    

    请注意,此方法是应用 Andrew Koenig 的 fundamental theorem of software engineering 的示例,即:“我们可以通过引入额外的间接级别来解决任何问题。”

    【讨论】:

    • 一个不错的选择——基本上是通过类方法创建一个工厂(__new__ 基本上只是类的默认工厂..)。不过,这并不能直接回答我原来的问题 - __new__ 对 NamedTuple 是只读的(我认为它可能不是 namedtuple)是否正确。
    • 确实回答了名义问题“如何根据输入参数以不同的方式初始化 NamedTuple 子类?” ——sao,我觉得你应该接受。
    • 您不能覆盖__new__() 方法的原因是typing.py 模块中的代码明确检查并禁止它——参见source code
    • 我接受了,因为这很有效,而且很有意义。我遇到的问题是我经常忘记使用Class.make(x) 方法,而不仅仅是Class(x)(导致不立即明显的缺陷)。问题是不允许为 NamedTuples 覆盖 __init____new__。这似乎是不合情理的,因为这些是处理这种情况的标准方法。过多地试图保护人们免受自己的伤害。
    • LightCC:您假设您知道为什么typing 模块的作者禁止覆盖__new__()。这可能是出于保护人们免受自身伤害以外的原因——例如它与NamedTuples 提供的一项或多项功能的实现不兼容。与往常一样,设计决策涉及权衡……
    猜你喜欢
    • 1970-01-01
    • 2015-02-23
    • 2022-09-27
    • 2017-06-07
    • 2021-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多