I'm trying to make a NamedTuple where one field defaults to an empty dictionary. This mostly works, however the default value is shared between instances of the NamedTuple:
from typing import NamedTuple, Dict
class MyTuple(NamedTuple):
foo: int
bar: Dict[str, str] = {}
t1 = MyTuple(1, {})
t2 = MyTuple(2)
t3 = MyTuple(3)
t2.bar["test2"] = "t2"
t3.bar["test3"] = "t3"
print(t2) # MyTuple(foo=2, bar={'test2': 't2', 'test3': 't3'})
print(t3) # MyTuple(foo=3, bar={'test2': 't2', 'test3': 't3'})
assert "test3" not in t2.bar # raises
How can I make sure the bar field is a new dict for each instance? All of the examples of dicts in PEP-526 seem to use ClassVar, but that's the opposite of what I want here.
I could potentially use a dataclass here with a default factory function (or the equivalent in attrs), but I currently need to support python 3.6.x and 3.7.x, so that would add some overhead.
For what it's worth, the version of python where I'm testing this is 3.7.3
typing.NamedTuple/collections.namedtuple don't support factory functions, and the mechanism that implements the defaults doesn't look to be overloadable in any reasonable way. Nor can you implement such a default manually by writing your own __new__ directly.
AFAICT, the only semi-reasonable way to do this is by writing a subclass of a namedtuple that implements its own __new__ that generates the default programmatically:
class MyTuple(NamedTuple):
foo: int
bar: Dict[str, str] = {} # You can continue to declare the default, even though you never use it
class MyTuple(MyTuple):
__slots__ = ()
def __new__(cls, foo, bar=None):
if bar is None:
bar = {}
return super().__new__(cls, foo, bar)
The overhead is relatively minimal, but it does involve half a dozen lines of boilerplate. You can reduce the boilerplate (at the expense of possibly over-dense code) one-line the definition of the parent MyTuple into the definition of the final MyTuple to reduce verbosity, e.g.:
class MyTuple(typing.NamedTuple('MyTuple', [('foo', int), ('bar', Dict[str, str])])):
__slots__ = ()
def __new__(cls, foo, bar=None):
if bar is None:
bar = {}
return super().__new__(cls, foo, bar)
but that will still make an inheritance hierarchy, not merely a single direct descendant of the tuple class like "inheriting" from NamedTuple does. This shouldn't impact performance meaningfully, just something to be aware of.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With