如何将特殊方法'Mixin'应用于typing.NamedTuple

11

我喜欢 Python 3.6 中的 typing.NamedTuple。但通常情况下,namedtuple 包含一个不可哈希的属性,并且我想将其用作 dict 键或 set 成员。如果 namedtuple 类使用对象标识(id() 作为 __eq____hash__) 是有意义的,那么将这些方法添加到类中即可。

然而,现在我的代码中出现了这种模式,我想摆脱繁琐的 __eq____hash__ 方法定义。我知道 namedtuple 并不是常规类,我一直无法搞清楚如何解决这个问题。

以下是我尝试过的方法:

from typing import NamedTuple

class ObjectIdentityMixin:
    def __eq__(self, other):
        return self is other

    def __hash__(self):
        return id(self)

class TestMixinFirst(ObjectIdentityMixin, NamedTuple):
    a: int

print(TestMixinFirst(1) == TestMixinFirst(1))  # Prints True, so not using my __eq__

class TestMixinSecond(NamedTuple, ObjectIdentityMixin):
    b: int

print(TestMixinSecond(2) == TestMixinSecond(2))  # Prints True as well

class ObjectIdentityNamedTuple(NamedTuple):
    def __eq__(self, other):
        return self is other

    def __hash__(self):
        return id(self)

class TestSuperclass(ObjectIdentityNamedTuple):
    c: int

TestSuperclass(3)    
"""
Traceback (most recent call last):
  File "test.py", line 30, in <module>
    TestSuperclass(3)
TypeError: __new__() takes 1 positional argument but 2 were given
"""

有没有一种方法,可以让我不必在每个需要“对象标识”(object identity)的NamedTuple中重复这些方法?


你的类ObjectIdentityNamedTupl中的__init__()方法是如何实现的? - developer_hatch
@DamianLattenero 我已经编辑了我的问题,展示了一个完整的代码示例。没有__init__。我尝试添加__new__,但是NamedTuple不允许这样做。 - Damon Maria
1
看起来 Python 3.7 中的 PEP 557 可能会解决所有这些问题。 - Damon Maria
2
你可以在Python 3.7中使用Dataclasses。它的行为类似于NamedTuple,并允许你重写__eq__。只需用dataclasses.dataclass装饰类即可。 - pylang
Dataclasses的文档参考链接:https://docs.python.org/3/library/dataclasses.html https://www.python.org/dev/peps/pep-0557/ - Lekensteyn
1个回答

10
NamedTuple类语法的魔法源泉是其元类metaclassNamedTupleMetabehind the sceneNamedTupleMeta.__new__为您创建一个新类,而不是典型的类,但是是由collections.namedtuple()创建的类。
问题在于,当NamedTupleMeta创建新类对象时,它会忽略基类,您可以检查TestMixinFirst的MRO,其中没有ObjectIdentityMixin
>>> print(TestMixinFirst.mro())
[<class '__main__.TestMixinFirst'>, <class 'tuple'>, <class 'object'>]

您需要扩展NamedTupleMeta以处理基类:

import typing


class NamedTupleMetaEx(typing.NamedTupleMeta):

    def __new__(cls, typename, bases, ns):
        cls_obj = super().__new__(cls, typename+'_nm_base', bases, ns)
        bases = bases + (cls_obj,)
        return type(typename, bases, {})


class TestMixin(ObjectIdentityMixin, metaclass=NamedTupleMetaEx):
    a: int
    b: int = 10


t1 = TestMixin(1, 2)
t2 = TestMixin(1, 2)
t3 = TestMixin(1)

assert hash(t1) != hash(t2)
assert not (t1 == t2)
assert t3.b == 10

做得好。我本来要把这个答案标为正确的,但后来发现了一个非常微妙和令人困惑的错误。如果其中一个字段有默认值,则除了通过名称访问字段时使用的初始化值TestMixin之外,在所有地方都会使用该值。在您的示例中,如果您将b的声明更改为:b: int = 0,那么: str(t1)返回TestMixin(a=1, b=2): 正确; t1[1]返回2: 正确; t.b返回0: 失败。 - Damon Maria
@DamonMaria 很有趣,稍后会研究一下。 - georgexsh
@DamonMaria,请检查更新后的代码,ns已经附加到由namedtuple()创建的基类上,不应该被覆盖。 - georgexsh
谢谢@georgexsh,我刚试了一下,现在所有的测试都通过了。谢谢你。 - Damon Maria
@DamonMaria 很高兴我能帮到你,事实上,我在这上面花费了很多时间。 - georgexsh
1
FYI:这在Python 3.9中不起作用:NamedTupleMeta.__new__的第一行现在有一个断言语句,要求第一个基类是_NamedTuple - alkasm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接