3

I am learning about namedtuple. I would like to find a way, using the ._replace method, to update all the appearances of a namedtuple wherever they are.

Say I have a list of nodes, and lists of elements (two-node beams, four-node quads) and boundaries ("one node" elements) defined by these nodes.

I am playing around with doing this:

from collections import namedtuple as nt
Node = nt('Node', 'x y')
Beam = nt('Beam', 'i j')
Quad = nt('Quad', 'i j k l')
Boundary = nt('Boundary', 'b')
#Define some nodes:
n1 = Node(0,0)
n2 = Node(0,1)
n3 = Node(1,1)
n4 = Node(1,0)
#And some other things using those nodes:
q1 = Quad(n1,n2,n3,n4)
b1 = Boundary(n1)
be1 = Beam(n1,n4)

Now, if I replace n1 with a new Node:

n1 = n1._replace(x=0.5,y=0.5)
print(n1)  # Node(x=0.5,y=0.5)

None of the other items are updated:

print(b1)  # Boundary(b=Node(x=0, y=0))

I understand the Python naming and object model and the why behind this: b1.b has been set to the object Node(0,0), not the name n1. So when n1 is changed, the other namedtuples still contain the same object as before, while n1 gets a new object.

What I would like to do is change this behavior so that when I change n1, the changes are "felt" in b1, be1, q1, etc. How can I do this?

Rick
  • 43,029
  • 15
  • 76
  • 119

2 Answers2

5

All instances of namedtuple-produced classes are immutable. ._replace() creates a new instance, it doesn't even update the one instance you call this on.

Because the instances are immutable you cannot do what you want with a namedtuple. You'll have to provide such functionality in a subclass, effectively breaking the immutability. Or just provide your own Node custom class that allows the attributes to be mutated directly:

class Node(object):
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return '{0.__class__.__name__}({0.x!r}, {0.y!r})'.format(self)

Like a namedtuple, this class uses __slots__ to cut back on memory use. You can set the x and y attributes directly on instances, and any other references to the instance will see the change:

>>> class Node(object):
...     __slots__ = ('x', 'y')
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return '{0.__class__.__name__}({0.x!r}, {0.y!r})'.format(self)
... 
>>> n1 = Node(10, 20)
>>> n2 = n1
>>> n2
Node(10, 20)
>>> n1.x = 42
>>> n1.y = 81
>>> n2
Node(42, 81)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • This is bad news! Do you think I should subclass namedtuple, or do something else entirely? – Rick Oct 31 '14 at 13:49
  • @RickTeachey: creating your own class is easy enough. – Martijn Pieters Oct 31 '14 at 13:51
  • Yeah I know *how* to make my own, I'm just *very bad* at it. I get bogged down in trying to predict everything I'm going to need to avoid having to go back and change it later. – Rick Oct 31 '14 at 13:53
  • I know what `__repr__` is for. What is `'{0.__name__}({0.x!r}, {0.y!r})'.format(self)` doing? Is that a regular expression or something? – Rick Oct 31 '14 at 13:53
  • 1
    @RickTeachey: it is a [formatted string](https://docs.python.org/3/library/string.html#formatstrings); the `str.format()` method drives it. – Martijn Pieters Oct 31 '14 at 13:55
  • @RickTeachey: I pass `self` to `str.format()` as a positional argument, the format string accesses that as `0`. `0.x` then is the `x` attribute on `self`, and `0.__class__.__name__` is the current class name. `!r` forces a `repr()` call so you always get a nice Python representation of the attribute values. – Martijn Pieters Oct 31 '14 at 13:58
  • Gotcha. Instead of writing all those classes (there are others I didn't mention), what about making one parent class that accepts a tuple argument defining the slots? Then I can just say `Node = MyFancyTupleClass('x','y')`, `n1 = Node(0,0)`, etc? – Rick Oct 31 '14 at 14:00
  • 1
    @RickTeachey: you'll need to define `__slots__` at the class level; this is what the `namedtuple()` class factory does too. You could write your own, generating a class on the fly is not that hard (just produce a `class` statement in a function). – Martijn Pieters Oct 31 '14 at 14:05
  • Any suggestions on videos or reading about generating classes this way in Python? – Rick Oct 31 '14 at 14:14
  • 1
    @RickTeachey: this one is very comprehensive: [What is a metaclass in Python?](http://stackoverflow.com/a/6581949) – Martijn Pieters Oct 31 '14 at 14:22
2

4 and 1/2 years later: If I were doing this today, I'd probably reach for the dataclasses module first:

from dataclasses import make_dataclass as md

Node = md('Node', 'x y')
Beam = md('Beam', 'i j')
Quad = md('Quad', 'i j k l')
Boundary = md('Boundary', 'b')

...and since these objects are mutable (unlike namedtuple), instead of replacing a node with a new instance, we can simply update the node position:

n1.x = 0.5
n1.y = 0.5
print(n1)  # Node(x=0.5,y=0.5)

This solves the problem entirely.

Rick
  • 43,029
  • 15
  • 76
  • 119