10

I love the typing.NamedTuple in Python 3.6. But there's often the case where the namedtuple contains a non-hashable attribute and I want to use it as a dict key or set member. If it makes sense that a namedtuple class uses object identity (id() for __eq__ and __hash__) then adding those methods to the class works fine.

However, I now have this pattern in my code in several places and I want to get rid of the boilerplate __eq__ and __hash__ method definitions. I know namedtuple's are not regular classes and I haven't been able to figure out how to get this working.

Here's what I've tried:

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
"""

Is there a way I don't have to repeat these methods in each NamedTuple that I need 'object identity' in?

georgexsh
  • 15,984
  • 2
  • 37
  • 62
Damon Maria
  • 1,001
  • 1
  • 8
  • 21
  • How is the__init__() method of your class ObjectIdentityNamedTupl? – developer_hatch Nov 16 '17 at 21:16
  • @DamianLattenero I've edited my question to show a full code example. There is no `__init__`. I've tried adding `__new__` but `NamedTuple`s don't allow that. – Damon Maria Nov 19 '17 at 17:07
  • 1
    It looks like PEP 557 coming in Python 3.7 might solve all of this. – Damon Maria Mar 27 '18 at 19:49
  • 2
    You can used Dataclasses here in Python 3.7_. It behaves like `NamedTuple`, and it allows you to override `__eq__`. Just decorate the class with `dataclasses.dataclass`. – pylang Apr 07 '20 at 02:36
  • Documentation references for Dataclasses: https://docs.python.org/3/library/dataclasses.html https://www.python.org/dev/peps/pep-0557/ – Lekensteyn Feb 01 '22 at 22:24

1 Answers1

9

The magic source of NamedTuple class syntax is its metaclass NamedTupleMeta, behind the scene, NamedTupleMeta.__new__ creates a new class for you, instead of a typical one, but a class created by collections.namedtuple().

The problem is, when NamedTupleMeta creating new class object, it ignored bases classes, you could check the MRO of TestMixinFirst, there is no ObjectIdentityMixin:

>>> print(TestMixinFirst.mro())
[<class '__main__.TestMixinFirst'>, <class 'tuple'>, <class 'object'>]

you have to extend NamedTupleMeta to take care of base classes:

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
georgexsh
  • 15,984
  • 2
  • 37
  • 62
  • Great work. I was about to mark this answer correct but then came across a really subtle and damn confusing bug in it. If one of the fields has a default value then the value TestMixin is initialized with is used everywhere except when accessing the field by name. In your example, if you change the declaration of `b` to: `b: int = 0` then: `str(t1)` returns `TestMixin(a=1, b=2)`: correct; `t1[1]` returns 2: correct; `t.b` returns `0`: fail. – Damon Maria Nov 26 '17 at 11:28
  • @DamonMaria interesting, will look into it later. – georgexsh Nov 26 '17 at 18:55
  • @DamonMaria check updated code, `ns` has already attached to the base class created by `namedtuple()`, should not be overridden. – georgexsh Nov 27 '17 at 08:01
  • Thanks @georgexsh, just tried it and now all my tests are passing. Thank you. – Damon Maria Dec 02 '17 at 15:21
  • @DamonMaria very glad that I helped, in fact, I have spent many hours on this. – georgexsh Dec 02 '17 at 15:22
  • 1
    FYI: this doesn't work in Python 3.9: the first line in `NamedTupleMeta.__new__` now has an assert statement that the first base class is a `_NamedTuple` – alkasm Jan 29 '21 at 08:21