163

Can anyone amend namedtuple or provide an alternative class so that it works for mutable objects?

Primarily for readability, I would like something similar to namedtuple that does this:

from Camelot import namedgroup

Point = namedgroup('Point', ['x', 'y'])
p = Point(0, 0)
p.x = 10

>>> p
Point(x=10, y=0)

>>> p.x *= 10
Point(x=100, y=0)

It must be possible to pickle the resulting object. And per the characteristics of named tuple, the ordering of the output when represented must match the order of the parameter list when constructing the object.

Mazdak
  • 105,000
  • 18
  • 159
  • 188
Alexander
  • 105,104
  • 32
  • 201
  • 196
  • 4
    See also: http://stackoverflow.com/q/5131044/. Is there a reason you can't just use a dictionary? – senshin Mar 26 '15 at 23:01
  • 2
    @senshin Thanks for the link. I prefer not to use a dictionary for the reason pointed out in it. That response also linked to http://code.activestate.com/recipes/52308-the-simple-but-handy-collector-of-a-bunch-of-named/?in=user-97991, which is pretty close to what I'm after. – Alexander Mar 26 '15 at 23:47
  • 1
    Unlike with `namedtuple`s, it appears you have no need to be able to reference the attributes by index, i.e. so `p[0]` and `p[1]` would be alternate ways to reference `x` and `y` respectively, correct? – martineau Mar 31 '15 at 09:35
  • 1
    Ideally, yes, indexable by position like a plain tuple in addition to by name, and unpacks like a tuple. This ActiveState recipe is close, but I believe it uses a regular dictionary instead of an OrderedDict. http://code.activestate.com/recipes/500261/ – Alexander Mar 31 '15 at 14:50
  • 1
    OK, another question: Does it have to be created via a factory function a la `namedtuple` or would subclassing some base class be all right? BTW, you need to include @martineau in your responses to my questions. – martineau Mar 31 '15 at 16:01
  • The recipe http://code.activestate.com/recipes/500261 you reference doesn't use a regular dictionary (nor OrderedDict). It's a factory function which returns a class derived from `tuple` — in fact it's what is now included with Python in `collections.namedtuple`. – martineau Mar 31 '15 at 16:13
  • 11
    A mutable namedtuple is called a class. – gbtimmon Apr 10 '17 at 19:12
  • I would add a requirement to meaningfully support `__lt__` and `__eq__`. – Andris Birkmanis Apr 20 '17 at 01:56
  • I think that table with tests was pretty useful and interesting. Maybe you could post it separately here as a community wiki? – Georgy Apr 10 '19 at 12:28
  • 1
    As @gbtimmon says: "A mutable namedtuple is called a class." [Here is an example I just wrote and added as an answer](https://stackoverflow.com/a/66757021/4561887). – Gabriel Staples Mar 23 '21 at 04:09

14 Answers14

164

There is a mutable alternative to collections.namedtuplerecordclass. It can be installed from PyPI:

pip3 install recordclass

It has the same API and memory footprint as namedtuple and it supports assignments (It should be faster as well). For example:

from recordclass import recordclass

Point = recordclass('Point', 'x y')

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

recordclass (since 0.5) support typehints:

from recordclass import recordclass, RecordClass

class Point(RecordClass):
   x: int
   y: int

>>> Point.__annotations__
{'x':int, 'y':int}
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

There is a more complete example (it also includes performance comparisons).

Recordclass library now provides another variant -- recordclass.make_dataclass factory function. It support dataclasses-like API (there are module level functions update, make, replace instead of self._update, self._replace, self._asdict, cls._make methods).

from recordclass import dataobject, make_dataclass

Point = make_dataclass('Point', [('x', int), ('y',int)])
Point = make_dataclass('Point', {'x':int, 'y':int})

class Point(dataobject):
   x: int
   y: int

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> p.x = 10; p.y += 3; print(p)
Point(x=10, y=5)

recordclass and make_dataclass can produce classes, whose instances occupy less memory than __slots__-based instances. This can be important for the instances with attribute values, which has not intended to have reference cycles. It may help reduce memory usage if you need to create millions of instances. Here is an illustrative example.

intellimath
  • 2,396
  • 1
  • 12
  • 13
  • 5
    Like it. 'This library actually is a “proof of concept” for the problem of “mutable” alternative of named tuple.` – Alexander Apr 02 '15 at 19:29
  • 1
    `recordclass` is slower, takes more memory, and requires C-extensions as [compared](https://gist.github.com/grantjenks/a06da0db18826be1176c31c95a6ee572) with Antti Haapala's recipe and `namedlist`. – GrantJ Dec 20 '16 at 19:20
  • `recordclass` is a mutable version of `collection.namedtuple` that inherits it's api, memory footprint, but support assignments. `namedlist` is actually instance of python class with slots. It's more usefull if you don't need fast access to it's fields by index. – intellimath Dec 20 '16 at 19:34
  • Attribute access for `recordclass` instance (python 3.5.2) is about 2-3% slower than for `namedlist` – intellimath Dec 20 '16 at 19:54
  • When using `namedtuple` and simple class creation `Point = namedtuple('Point', 'x y')`, Jedi can autocomplete attributes, while this is not the case for `recordclass`. If I use the longer creation code (based on `RecordClass`), then Jedi understands the `Point` class, but not its constructor or attributes... Is there a way to get `recordclass` to work nicely with Jedi? – PhilMacKay Jan 10 '19 at 20:51
  • I’m not quite sure, but it seems possible to do this on the side of Jedi. In the case of namedtuple, this was done: https://github.com/davidhalter/jedi/issues/107 – intellimath Jan 11 '19 at 10:01
  • For Python newbies, you might want to add the installation instructions for this module: `pip3 install recordclass`. I veered away from this solution initially when I got an import error, and decided to see what solutions come with Python natively first. Others might just get stuck altogether on something relatively simple like installing a new module. – Gabriel Staples Mar 23 '21 at 07:47
71

types.SimpleNamespace was introduced in Python 3.3 and supports the requested requirements.

from types import SimpleNamespace
t = SimpleNamespace(foo='bar')
t.ham = 'spam'
print(t)
namespace(foo='bar', ham='spam')
print(t.foo)
'bar'
import pickle
with open('/tmp/pickle', 'wb') as f:
    pickle.dump(t, f)
olevegard
  • 5,294
  • 1
  • 25
  • 29
funky-future
  • 3,716
  • 1
  • 30
  • 43
  • 2
    I've been looking for something like this for years. Great replacement for a dotted dict library like dotmap – axwell Jun 21 '18 at 04:11
  • 3
    This needs more upvotes. It’s exactly what the OP was looking for, it’s in the standard library, and it could not be simpler to use. Thanks! – Tom Zych Nov 25 '18 at 13:33
  • 5
    -1 The OP made it very clear with his tests what he needs and `SimpleNamespace` fails tests 6-10 (access by index, iterative unpacking, iteration, ordered dict, in-place replacement) and 12, 13 (fields, slots). Note that the documentation (that you linked in the answer) specifically says *"`SimpleNamespace` may be useful as a replacement for `class NS: pass`. However, for a structured record type use `namedtuple()` instead."* – Ali Dec 24 '18 at 12:33
  • 3
    -1 too, `SimpleNamespace` creates an object, not a class constructor, and cannot be a replacement for namedtuple. Type comparison will not work, and the memory footprint will be much higher. – RedGlyph Jul 08 '20 at 18:32
70

As a Pythonic alternative for this task, since Python-3.7, you can use dataclasses module that not only behaves like a mutable NamedTuple, because they use normal class definitions, they also support other class features.

From PEP-0557:

Although they use a very different mechanism, Data Classes can be thought of as "mutable namedtuples with defaults". Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.

A class decorator is provided which inspects a class definition for variables with type annotations as defined in PEP 526, "Syntax for Variable Annotations". In this document, such variables are called fields. Using these fields, the decorator adds generated method definitions to the class to support instance initialization, a repr, comparison methods, and optionally other methods as described in the Specification section. Such a class is called a Data Class, but there's really nothing special about the class: the decorator adds generated methods to the class and returns the same class it was given.

This feature is introduced in PEP-0557 that you can read about it in more details on provided documentation link.

Example:

In [20]: from dataclasses import dataclass

In [21]: @dataclass
    ...: class InventoryItem:
    ...:     '''Class for keeping track of an item in inventory.'''
    ...:     name: str
    ...:     unit_price: float
    ...:     quantity_on_hand: int = 0
    ...: 
    ...:     def total_cost(self) -> float:
    ...:         return self.unit_price * self.quantity_on_hand
    ...:    

Demo:

In [23]: II = InventoryItem('bisc', 2000)

In [24]: II
Out[24]: InventoryItem(name='bisc', unit_price=2000, quantity_on_hand=0)

In [25]: II.name = 'choco'

In [26]: II.name
Out[26]: 'choco'

In [27]: 

In [27]: II.unit_price *= 3

In [28]: II.unit_price
Out[28]: 6000

In [29]: II
Out[29]: InventoryItem(name='choco', unit_price=6000, quantity_on_hand=0)
Mazdak
  • 105,000
  • 18
  • 159
  • 188
  • 2
    It was made very clear with the tests in the OP what is needed and `dataclass` fails tests 6-10 (access by index, iterative unpacking, iteration, ordered dict, in-place replacement) and 12, 13 (fields, slots) in Python 3.7.1. – Ali Dec 24 '18 at 13:05
  • 10
    although this may not be specifically what the OP was looking for, it's certainly helped me :) – Martin CR Apr 04 '20 at 08:50
26

The latest namedlist 1.7 passes all of your tests with both Python 2.7 and Python 3.5 as of Jan 11, 2016. It is a pure python implementation whereas the recordclass is a C extension. Of course, it depends on your requirements whether a C extension is preferred or not.

Your tests (but also see the note below):

from __future__ import print_function
import pickle
import sys
from namedlist import namedlist

Point = namedlist('Point', 'x y')
p = Point(x=1, y=2)

print('1. Mutation of field values')
p.x *= 10
p.y += 10
print('p: {}, {}\n'.format(p.x, p.y))

print('2. String')
print('p: {}\n'.format(p))

print('3. Representation')
print(repr(p), '\n')

print('4. Sizeof')
print('size of p:', sys.getsizeof(p), '\n')

print('5. Access by name of field')
print('p: {}, {}\n'.format(p.x, p.y))

print('6. Access by index')
print('p: {}, {}\n'.format(p[0], p[1]))

print('7. Iterative unpacking')
x, y = p
print('p: {}, {}\n'.format(x, y))

print('8. Iteration')
print('p: {}\n'.format([v for v in p]))

print('9. Ordered Dict')
print('p: {}\n'.format(p._asdict()))

print('10. Inplace replacement (update?)')
p._update(x=100, y=200)
print('p: {}\n'.format(p))

print('11. Pickle and Unpickle')
pickled = pickle.dumps(p)
unpickled = pickle.loads(pickled)
assert p == unpickled
print('Pickled successfully\n')

print('12. Fields\n')
print('p: {}\n'.format(p._fields))

print('13. Slots')
print('p: {}\n'.format(p.__slots__))

Output on Python 2.7

1. Mutation of field values  
p: 10, 12

2. String  
p: Point(x=10, y=12)

3. Representation  
Point(x=10, y=12) 

4. Sizeof  
size of p: 64 

5. Access by name of field  
p: 10, 12

6. Access by index  
p: 10, 12

7. Iterative unpacking  
p: 10, 12

8. Iteration  
p: [10, 12]

9. Ordered Dict  
p: OrderedDict([('x', 10), ('y', 12)])

10. Inplace replacement (update?)  
p: Point(x=100, y=200)

11. Pickle and Unpickle  
Pickled successfully

12. Fields  
p: ('x', 'y')

13. Slots  
p: ('x', 'y')

The only difference with Python 3.5 is that the namedlist has become smaller, the size is 56 (Python 2.7 reports 64).

Note that I have changed your test 10 for in-place replacement. The namedlist has a _replace() method which does a shallow copy, and that makes perfect sense to me because the namedtuple in the standard library behaves the same way. Changing the semantics of the _replace() method would be confusing. In my opinion the _update() method should be used for in-place updates. Or maybe I failed to understand the intent of your test 10?

Ali
  • 56,466
  • 29
  • 168
  • 265
  • There is important nuance. The `namedlist` store values in the list instance. The thing is that `cpython`'s `list` is actually a dynamic array. By design, it allocates more memory than necessary in order to make mutation of the list cheaper. – intellimath Jun 02 '16 at 08:35
  • 1
    @intellimath namedlist is a bit of misnomer. It does not actually inherit from `list` and by default uses `__slots__` optimization. When I measured, memory use was less than `recordclass`: 96 bytes vs 104 bytes for six fields on Python 2.7 – GrantJ Dec 20 '16 at 19:10
  • @GrantJ Yes. `recorclass` uses more memory because it's a `tuple`-like object with variable memory size. – intellimath Dec 20 '16 at 20:06
  • 2
    Anonymous downvotes are not helping anybody. What is wrong with the answer? Why the downvote? – Ali Dec 11 '18 at 20:25
  • I love the safety against typos that it provides with respect to `types.SimpleNamespace`. Unfortunately, pylint does not like it :-( – xverges Dec 21 '18 at 06:52
  • @Ali On your `p.x *= 10` line , https://www.pylint.org/ reports `Instance of '' has no 'x' member` – xverges Dec 23 '18 at 23:50
  • @xverges The OP made it very clear with his tests what he needs and `SimpleNamespace` fails tests 6-10 (access by index, iterative unpacking, iteration, ordered dict, in-place replacement) and 12, 13 (fields, slots). Note that the documentation (linked in that answer) specifically says *"`SimpleNamespace` may be useful as a replacement for `class NS: pass`. However, for a structured record type use `namedtuple()` instead."* I accept that for your use case SimpleNamespace is a nice solution, but it is not the answer to the OP; `recordclass` and `namedlist` are. – Ali Dec 24 '18 at 12:35
  • @xverges `SimpleNamespace` lets you add new attributes willy-nilly so there goes your safety against typos... `namedlist` will protect you against such typos, and will crash the program on the line with the typo. (Yes, it would be nicer to catch it statically.) As for pylint, I think you should be able to at least silence that bogus warning. As far as I know PyDev uses pylint for static analysis, and with the default settings I don't get bogus warnings at least. – Ali Dec 24 '18 at 12:44
  • @Ali I think that you are preaching to choir, here: I upvoted your answer and did not dispute anything about it. My comments only pointed out that pylint is giving bogus errors. Python 3.7. pylint 2.2.2. – xverges Dec 24 '18 at 16:10
  • @xverges OK, and I appreciate your feedback. As I said, I think it is just a matter of settings to silence that bogus warning from pylint since I don't see them in PyDev. Hope this helps. And thanks again for your feedback. I apologize if I was too harsh. – Ali Dec 24 '18 at 16:50
  • @Ali No problem. I don't think that PyDev uses pylint by default http://www.pydev.org/manual_adv_pylint.html Looks like https://stackoverflow.com/questions/35990313/avoid-pylint-warning-e1101-instance-of-has-no-member-for-class-with-dyn would be the right approach to silence pylint (a bit annoying that it seems that cannot be set within the source file https://stackoverflow.com/questions/50767469/pylint-ignored-classes-in-source-file ) – xverges Dec 24 '18 at 17:46
  • @xverges Interesting, thanks for the info! I thought PyDev used pylint but I stand corrected. And wouldn't one of the suggested solutions at the linked question work for you? – Ali Dec 24 '18 at 18:51
24

It seems like the answer to this question is no.

Below is pretty close, but it's not technically mutable. This is creating a new namedtuple() instance with an updated x value:

Point = namedtuple('Point', ['x', 'y'])
p = Point(0, 0)
p = p._replace(x=10) 

On the other hand, you can create a simple class using __slots__ that should work well for frequently updating class instance attributes:

class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

To add to this answer, I think __slots__ is good use here because it's memory efficient when you create lots of class instances. The only downside is that you can't create new class attributes.

Here's one relevant thread that illustrates the memory efficiency - Dictionary vs Object - which is more efficient and why?

The quoted content in the answer of this thread is a very succinct explanation why __slots__ is more memory efficient - Python slots

Community
  • 1
  • 1
kennes
  • 2,065
  • 17
  • 20
  • 1
    Close, but clunky. Let's say I wanted to do a += assignment, I would then need to do: p._replace(x = p.x + 10) vs. p.x += 10 – Alexander Mar 26 '15 at 23:19
  • 1
    yeah, it's not really changing the existing tuple, it's creating a new instance – kennes Mar 26 '15 at 23:25
10

The following is a good solution for Python 3: A minimal class using __slots__ and Sequence abstract base class; does not do fancy error detection or such, but it works, and behaves mostly like a mutable tuple (except for typecheck).

from collections import Sequence

class NamedMutableSequence(Sequence):
    __slots__ = ()

    def __init__(self, *a, **kw):
        slots = self.__slots__
        for k in slots:
            setattr(self, k, kw.get(k))

        if a:
            for k, v in zip(slots, a):
                setattr(self, k, v)

    def __str__(self):
        clsname = self.__class__.__name__
        values = ', '.join('%s=%r' % (k, getattr(self, k))
                           for k in self.__slots__)
        return '%s(%s)' % (clsname, values)

    __repr__ = __str__

    def __getitem__(self, item):
        return getattr(self, self.__slots__[item])

    def __setitem__(self, item, value):
        return setattr(self, self.__slots__[item], value)

    def __len__(self):
        return len(self.__slots__)

class Point(NamedMutableSequence):
    __slots__ = ('x', 'y')

Example:

>>> p = Point(0, 0)
>>> p.x = 10
>>> p
Point(x=10, y=0)
>>> p.x *= 10
>>> p
Point(x=100, y=0)

If you want, you can have a method to create the class too (though using an explicit class is more transparent):

def namedgroup(name, members):
    if isinstance(members, str):
        members = members.split()
    members = tuple(members)
    return type(name, (NamedMutableSequence,), {'__slots__': members})

Example:

>>> Point = namedgroup('Point', ['x', 'y'])
>>> Point(6, 42)
Point(x=6, y=42)

In Python 2 you need to adjust it slightly - if you inherit from Sequence, the class will have a __dict__ and the __slots__ will stop from working.

The solution in Python 2 is to not inherit from Sequence, but object. If isinstance(Point, Sequence) == True is desired, you need to register the NamedMutableSequence as a base class to Sequence:

Sequence.register(NamedMutableSequence)
Community
  • 1
  • 1
3

Tuples are by definition immutable.

You can however make a dictionary subclass where you can access the attributes with dot-notation;

In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:class AttrDict(dict):
:
:    def __getattr__(self, name):
:        return self[name]
:
:    def __setattr__(self, name, value):
:        self[name] = value
:--

In [2]: test = AttrDict()

In [3]: test.a = 1

In [4]: test.b = True

In [5]: test
Out[5]: {'a': 1, 'b': True}
Roland Smith
  • 42,427
  • 3
  • 64
  • 94
3

If you want similar behavior as namedtuples but mutable try namedlist

Note that in order to be mutable it cannot be a tuple.

agomcas
  • 695
  • 5
  • 12
  • Thanks for the link. This looks like the closest so far, but I need to evaluate it in more detail. Btw, I'm totally aware tuples are immutable, which is why I'm looking for a solution _like_ namedtuple. – Alexander Mar 31 '15 at 14:57
3

Let's implement this with dynamic type creation:

import copy
def namedgroup(typename, fieldnames):

    def init(self, **kwargs): 
        attrs = {k: None for k in self._attrs_}
        for k in kwargs:
            if k in self._attrs_:
                attrs[k] = kwargs[k]
            else:
                raise AttributeError('Invalid Field')
        self.__dict__.update(attrs)

    def getattribute(self, attr):
        if attr.startswith("_") or attr in self._attrs_:
            return object.__getattribute__(self, attr)
        else:
            raise AttributeError('Invalid Field')

    def setattr(self, attr, value):
        if attr in self._attrs_:
            object.__setattr__(self, attr, value)
        else:
            raise AttributeError('Invalid Field')

    def rep(self):
         d = ["{}={}".format(v,self.__dict__[v]) for v in self._attrs_]
         return self._typename_ + '(' + ', '.join(d) + ')'

    def iterate(self):
        for x in self._attrs_:
            yield self.__dict__[x]
        raise StopIteration()

    def setitem(self, *args, **kwargs):
        return self.__dict__.__setitem__(*args, **kwargs)

    def getitem(self, *args, **kwargs):
        return self.__dict__.__getitem__(*args, **kwargs)

    attrs = {"__init__": init,
                "__setattr__": setattr,
                "__getattribute__": getattribute,
                "_attrs_": copy.deepcopy(fieldnames),
                "_typename_": str(typename),
                "__str__": rep,
                "__repr__": rep,
                "__len__": lambda self: len(fieldnames),
                "__iter__": iterate,
                "__setitem__": setitem,
                "__getitem__": getitem,
                }

    return type(typename, (object,), attrs)

This checks the attributes to see if they are valid before allowing the operation to continue.

So is this pickleable? Yes if (and only if) you do the following:

>>> import pickle
>>> Point = namedgroup("Point", ["x", "y"])
>>> p = Point(x=100, y=200)
>>> p2 = pickle.loads(pickle.dumps(p))
>>> p2.x
100
>>> p2.y
200
>>> id(p) != id(p2)
True

The definition has to be in your namespace, and must exist long enough for pickle to find it. So if you define this to be in your package, it should work.

Point = namedgroup("Point", ["x", "y"])

Pickle will fail if you do the following, or make the definition temporary (goes out of scope when the function ends, say):

some_point = namedgroup("Point", ["x", "y"])

And yes, it does preserve the order of the fields listed in the type creation.

MadMan2064
  • 436
  • 3
  • 4
  • If you add an `__iter__` method with `for k in self._attrs_: yield getattr(self, k)`, that will support unpacking like a tuple. – snapshoe Apr 02 '15 at 00:06
  • It's also pretty easy to add `__len__`, `__getitem__`, and `__setiem__` methods to support getting valus by index, like `p[0]`. With these last bits, this seems like the most complete and correct answer (to me anyway). – snapshoe Apr 02 '15 at 00:35
  • `__len__` and `__iter__` are good. `__getitem__` and `__setitem__` can really be mapped to `self.__dict__.__setitem__` and `self.__dict__.__getitem__` – MadMan2064 Apr 02 '15 at 01:14
2

I can't believe nobody's said this before, but it seems to me Python just wants you to write your own simple, mutable class instead of using a namedtuple whenever you need the "namedtuple" to be mutable.

Quick summary

Just jump straight down to Approach 5 below. It's short and to-the-point, and by far the best of these options.

Various, detailed approaches:

Approach 1 (good): simple, callable class with __call__()

Here is an example of a simple Point object for (x, y) points:

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

    def __call__(self):
        """
        Make `Point` objects callable. Print their contents when they 
        are called.
        """
        print("Point(x={}, y={})".format(self.x, self.y))

Now use it:

p1 = Point(1,2)
p1()
p1.x = 7
p1()
p1.y = 8
p1()

Here is the output:

Point(x=1, y=2)
Point(x=7, y=2)
Point(x=7, y=8)

This is pretty similar to a namedtuple, except it is fully mutable, unlike a namedtuple. Also, a namedtuple isn't callable, so to see its contents, just type the object instance name withOUT parenthesis after it (as p2 in the example below, instead of as p2()). See this example and output here:

>>> from collections import namedtuple
>>> Point2 = namedtuple("Point2", ["x", "y"])
>>> p2 = Point2(1, 2)
>>> p2
Point2(x=1, y=2)
>>> p2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Point2' object is not callable
>>> p2.x = 7
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Approach 2 (better): use __repr__() in place of __call__()

I just learned you can use __repr__() in place of __call__(), to get more namedtuple-like behavior. Defining the __repr__() method allows you to define "the 'official' string representation of an object" (see the official documentation here). Now, just calling p1 is the equivalent of calling the __repr__() method, and you get identical behavior to the namedtuple. Here is the new class:

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

    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)

Now use it:

p1 = Point(1,2)
p1
p1.x = 7
p1
p1.y = 8
p1

Here is the output:

Point(x=1, y=2)
Point(x=7, y=2)
Point(x=7, y=8)

Approach 3 (better still, but a little awkward to use): make it a callable which returns an (x, y) tuple

The original poster (OP) would also like something like this to work (see his comment below my answer):

x, y = Point(x=1, y=2)

Well, for simplicity, let's just make this work instead:

x, y = Point(x=1, y=2)()

# OR
p1 = Point(x=1, y=2)
x, y = p1()

While we are at it, let's also condense this:

self.x = x
self.y = y

...into this (source where I first saw this):

self.x, self.y = x, y

Here is the class definition for all of the above:

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

    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)

    def __call__(self):
        """
        Make the object callable. Return a tuple of the x and y components
        of the Point.
        """
        return self.x, self.y

Here are some test calls:

p1 = Point(1,2)
p1
p1.x = 7
x, y = p1()
x2, y2 = Point(10, 12)()
x
y
x2
y2

I won't show pasting the class definition into the interpreter this time, but here are those calls with their output:

>>> p1 = Point(1,2)
>>> p1
Point(x=1, y=2)
>>> p1.x = 7
>>> x, y = p1()
>>> x2, y2 = Point(10, 12)()
>>> x
7
>>> y
2
>>> x2
10
>>> y2
12

Approach 4 (best so far, but a lot more code to write): make the class also an iterator

By making this into an iterator class, we can get this behavior:

x, y = Point(x=1, y=2)
# OR
x, y = Point(1, 2)
# OR
p1 = Point(1, 2)
x, y = p1

Let's get rid of the __call__() method, but to make this class an iterator we will add the __iter__() and __next__() methods. Read more about these things here:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. How to build a basic iterator?
  3. https://docs.python.org/3/library/exceptions.html#StopIteration

Here is the solution:

class Point():
    def __init__(self, x, y):
        self.x, self.y = x, y
        self._iterator_index = 0
        self._num_items = 2  # counting self.x and self.y

    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)

    def __iter__(self):
        return self

    def __next__(self):
        self._iterator_index += 1

        if self._iterator_index == 1:
            return self.x
        elif self._iterator_index == 2:
            return self.y
        else:
            raise StopIteration

And here are some test calls and their output:

>>> x, y = Point(x=1, y=2)
>>> x
1
>>> y
2
>>> x, y = Point(3, 4)
>>> x
3
>>> y
4
>>> p1 = Point(5, 6)
>>> x, y = p1
>>> x
5
>>> y
6
>>> p1
Point(x=5, y=6)

Approach 5 (USE THIS ONE) (Perfect!--best and cleanest/shortest approach): make the class an iterable, with the yield generator keyword

Study these references:

  1. https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
  2. What does the "yield" keyword do?

Here is the solution. It relies on a fancy "iterable-generator" (AKA: just "generator") keyword/Python mechanism, called yield.

Basically, the first time an iterable calls for the next item, it calls the __iter__() method, and stops and returns the contents of the first yield call (self.x in the code below). The next time an iterable calls for the next item, it picks up where it last left off (just after the first yield in this case), and looks for the next yield, stopping and returning the contents of that yield call (self.y in the code below). Each "return" from a yield actually returns a "generator" object, which is an iterable itself, so you can iterate on it. Each new iterable call for the next item continues this process, starting up where it last left off, just after the most-recently-called yield, until no more yield calls exist, at which point the iterations are ended and the iterable has been fully iterated. Therefore, once this iterable has called for two objects, both yield calls have been used up, so the iterator ends. The end result is that calls like this work perfectly, just as they did in Approach 4, but with far less code to write!:

x, y = Point(x=1, y=2)
# OR
x, y = Point(1, 2)
# OR
p1 = Point(1, 2)
x, y = p1

Here is the solution (a part of this solution can also be found in the treyhunner.com reference just above). Notice how short and clean this solution is!

Just the class definition code; no docstrings, so you can truly see how short and simple this is:

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

    def __repr__(self):
        return "Point(x={}, y={})".format(self.x, self.y)
    
    def __iter__(self):
        yield self.x
        yield self.y

With descriptive docstrings:

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

    def __repr__(self):
        """
        Obtain the string representation of `Point`, so that just typing
        the instance name of an object of this type will call this method 
        and obtain this string, just like `namedtuple` already does!
        """
        return "Point(x={}, y={})".format(self.x, self.y)

    def __iter__(self):
        """
        Make this `Point` class an iterable. When used as an iterable, it will
        now return `self.x` and `self.y` as the two elements of a list-like, 
        iterable object, "generated" by the usages of the `yield` "generator" 
        keyword.
        """
        yield self.x
        yield self.y

Copy and paste the exact same test code as used in the previous approach (Approach 4) just above, and you will get the exact same output as above as well!

References:

  1. https://docs.python.org/3/library/collections.html#collections.namedtuple
  2. Approach 1:
    1. What is the difference between __init__ and __call__?
  3. Approach 2:
    1. https://www.tutorialspoint.com/What-does-the-repr-function-do-in-Python-Object-Oriented-Programming
    2. Purpose of __repr__ method?
    3. https://docs.python.org/3/reference/datamodel.html#object.__repr__
  4. Approach 4:
    1. *****[EXCELLENT!] https://treyhunner.com/2018/06/how-to-make-an-iterator-in-python/
    2. How to build a basic iterator?
    3. https://docs.python.org/3/library/exceptions.html#StopIteration
  5. Approach 5:
    1. See links from Approach 4, plus:
    2. *****[EXCELLENT!] What does the "yield" keyword do?
  6. What is the meaning of single and double underscore before an object name?
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • 1
    This solution is similar to the one @kennes posted back in 2015. The original question was heavily edited over the years, but one of the requirements was tuple unpacking, e.g. `x, y = Point(x=1, y=2)`. Also, wouldn't it be simpler to user `__repr__` instead of `__call__`? – Alexander Mar 23 '21 at 04:24
  • @Alexander, thanks for pointing me to the `__repr__()` method. I wasn't familiar with it before. I've added it to my answer. I've massively improved and expanded my answer, adding Approaches 2 through 5, to address that plus fully address your tuple unpacking requirement. Approach 5 is the best. Both it and Approach 4 do that perfectly now, as far as I can tell, and based on my testing. – Gabriel Staples Mar 23 '21 at 07:11
  • @Alexander, I see you have 6 more years Python experience now than when you asked the question, and have a ton of experience in Python overall, whereas I am still learning a ton more about Python. What is your canonical solution to this problem now? What is your go-to solution for when you need a mutable `namedtuple`? What do you think about my Approach 5 in my answer? – Gabriel Staples Mar 23 '21 at 07:15
  • I would start by reviewing `recordclass` https://pypi.org/project/recordclass/. I'll try to review your response in more detail later this week. – Alexander Mar 23 '21 at 07:35
1

Provided performance is of little importance, one could use a silly hack like:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
  • 510
  • 4
  • 13
  • 1
    This answer isn't very well explained. It looks confusing if you don't understand the mutable nature of lists. --- In this example... to re-assign `z`, you have to call `mutable_z.z.pop(0)` then `mutable_z.z.append(new_value)`. If you get this wrong, you'll end up with more than 1 element and your program will behave unexpectedly. – byxor Sep 19 '17 at 13:30
  • 2
    @byxor that, or you could just: `mutable_z.z[0] = newValue`. It is indeed a hack, as stated. – Srg Apr 28 '18 at 15:05
  • Oh yeah, I'm surprised I missed the more obvious way to re-assign it. – byxor Apr 28 '18 at 15:18
  • I actually use lists as members for mutable attributes if I need a quick data class :) It works, but it's not a pretty solution for sure. – hochl Oct 15 '20 at 20:39
1

If you want to be able to create classes "on-site", I find the following very convenient:

class Struct:
    def __init__(self, **kw):
        self.__dict__.update(**kw)

That allows me to write:

p = Struct(x=0, y=0)
P.x = 10

stats = Struct(count=0, total=0.0)
stats.count += 1
Alexander
  • 105,104
  • 32
  • 201
  • 196
Igor Gatis
  • 4,648
  • 10
  • 43
  • 66
0

The most elegant way I can think of doesn't require a 3rd party library and lets you create a quick mock class constructor with default member variables without dataclasses cumbersome type specification. So it's better for roughing out some code:

# copy-paste 3 lines:
from inspect import getargvalues, stack
from types import SimpleNamespace
def DefaultableNS(): return SimpleNamespace(**getargvalues(stack()[1].frame)[3])

# then you can make classes with default fields on the fly in one line, eg:
def Node(value,left=None,right=None): return DefaultableNS()

node=Node(123)
print(node)
#[stdout] namespace(value=123, left=None, right=None)

print(node.value,node.left,node.right) # all fields exist 

A plain SimpleNamespace is clumsier, it breaks DRY:

def Node(value,left=None,right=None):
    return SimpleNamespace(value=value,left=left,right=right) 
    # breaks DRY as you need to repeat the argument names twice
Rian Rizvi
  • 9,989
  • 2
  • 37
  • 33
0

I will share my solution to this question. I needed a way to save attributes in the case that my program crashed or was stopped for some reason so that it would know where where in a list of inputs to resume from. Based on @GabrielStaples's answer:

import pickle, json
class ScanSession:
def __init__(self, input_file: str = None, output_file: str = None,
             total_viable_wallets: int = 0, total: float = 0,
             report_dict: dict = {}, wallet_addresses: list = [],
             token_map: list = [], token_map_file: str = 'data/token.maps.json',
             current_batch: int = 0):
    self.initialized = time.time()
    self.input_file = input_file
    self.output_file = output_file
    self.total_viable_wallets = total_viable_wallets
    self.total = total
    self.report_dict = report_dict
    self.wallet_addresses = wallet_addresses
    self.token_map = token_map
    self.token_map_file = token_map_file
    self.current_batch = current_batch

@property
def __dict__(self):
    """
    Obtain the string representation of `Point`, so that just typing
    the instance name of an object of this type will call this method
    and obtain this string, just like `namedtuple` already does!
    """
    return {'initialized': self.initialized, 'input_file': self.input_file,
            'output_file': self.output_file, 'total_viable_wallets': self.total_viable_wallets,
            'total': self.total, 'report_dict': self.report_dict,
            'wallet_addresses': self.wallet_addresses, 'token_map': self.token_map,
            'token_map_file':self.token_map_file, 'current_batch': self.current_batch
            }

def load_session(self, session_file):
    with open(session_file, 'r') as f:
        _session = json.loads(json.dumps(f.read()))
        _session = dict(_session)
        for key, value in _session.items():
            setattr(self, key, value)

def dump_session(self, session_file):
    with open(session_file, 'w') as f:
        json.dump(self.__dict__, fp=f)

Using it:

session = ScanSession()
session.total += 1
session.__dict__
{'initialized': 1670801774.8050613, 'input_file': None, 'output_file': None, 'total_viable_wallets': 0, 'total': 10, 'report_dict': {}, 'wallet_addresses': [], 'token_map': [], 'token_map_file': 'data/token.maps.json', 'current_batch': 0}
pickle.dumps(session)
b'\x80\x04\x95\xe8\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x0bScanSession\x94\x93\x94)\x81\x94}\x94(\x8c\x0binitialized\x94GA\xd8\xe5\x9a[\xb3\x86 \x8c\ninput_file\x94N\x8c\x0boutput_file\x94N\x8c\x14total_viable_wallets\x94K\x00\x8c\x05total\x94K\n\x8c\x0breport_dict\x94}\x94\x8c\x10wallet_addresses\x94]\x94\x8c\ttoken_map\x94]\x94\x8c\x0etoken_map_file\x94\x8c\x14data/token.maps.json\x94\x8c\rcurrent_batch\x94K\x00ub.'
Chev_603
  • 313
  • 3
  • 14