I am building a typing.NamedTuple class (see typing.NamedTuple docs here, or the older collections.namedtuples docs it inherits from) that can accept different ways of being initialized.
Why NamedTuple in this case? I want it to be immutable and auto-hashable so it can be a dictionary key, and I don't have to write the hash function.
I understand that I need to use __new__
rather than __init__
due to NamedTuples being immutable (for example, see this Q&A. I've searched and there are some tidbits out there (e.g. answers to this question on setting up a custom hash for a namedtuple), but I can't get everything working, I'm getting an error about not being able to overwrite __new__
.
Here's my current code:
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)
But I'm getting the following error:
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__
Am I not able to create a separate __new__
function for a child class that inherits from NamedTuple? It appears from the message that it's attempting to overwrite __new__
for NamedTuple directly, rather than the TicTacToe class.
What's going on here?
You can avoid needing to define __new__()
by defining a classmethod
. In the sample code below, I've simply named it make()
. It's analogous to the class method named _make()
✶ that collections.namedtype
subclasses have.
✶This is a common way to provide "alternative constructors" to any class.
Note that I also changed the first call to the function so it passes the arguments properly to the make()
method.
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)
Output:
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=' ')
An alternative workaround to not being able to overload the NamedTuple
subclass' __new__()
method would be to split the derived class into two classes, one public and one private, so that the former is no longer a direct subclass of NamedTuple
.
An advantage of doing it this way, is there's no longer a need to create instances using a special-purpose classmethod like make()
above.
Here's what I mean:
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)
Note that this approach is an example of applying Andrew Koenig's fundamental theorem of software engineering, namely: "We can solve any problem by introducing an extra level of indirection."