41

I have a class that only contains attributes and I would like packing/unpacking to work on it. What collections.abc should I implement to get this behaviour?

class Item(object):

    def __init__(self, name, age, gender)
        self.name = name
        self.age = age
        self.gender = gender

a, b, c = Item("Henry", 90, "male")

I would like to avoid using a namedtuple.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Har
  • 3,727
  • 10
  • 41
  • 75
  • 3
    You can unpack any [`Iterable`](https://docs.python.org/2/library/collections.html#collections.Iterable), so you need to implement `__iter__`. – jonrsharpe Jun 15 '16 at 13:57
  • 3
    That depends on whether you want `__iter__` to `return self` or not - you could just `return iter((self.name, self.age, self.gender))`, for example. It must be *iterable*, but not necessarily *an iterator*. – jonrsharpe Jun 15 '16 at 14:01
  • Okay thanks I think I got it. – Har Jun 15 '16 at 14:03

4 Answers4

49

You can unpack any Iterable. This means you need to implement the __iter__ method, and return an iterator. In your case, this could simply be:

def __iter__(self):
    return iter((self.name, self.age, self.gender))

Alternatively you could make your class an Iterator, then __iter__ would return self and you'd need to implement __next__; this is more work, and probably not worth the effort.

For more information see What exactly are Python's iterator, iterable, and iteration protocols?


Per the question I linked above, you could also implement an iterable with __getitem__:

def __getitem__(self, index):
    return (self.name, self.age, self.gender)[index]
Community
  • 1
  • 1
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • For a generic shallow unpacking (which is what you might want) use: `def __iter__(self): return iter(tuple(getattr(self, field.name) for field in dataclasses.fields(self)))`. – Adi Shavit May 27 '22 at 07:04
  • @AdiShavit that's not so relevant to this answer, which isn't about dataclasses – jonrsharpe May 27 '22 at 07:08
21

A better option for unpacking Python 3.7+ dataclasses is as follows:

from dataclasses import astuple, dataclass

@dataclass 
class Item:
    name: str
    age: int
    gender: str

    def __iter__(self):
        return iter(astuple(self))

a, b, c = Item("Henry", 90, "male")
phoenix
  • 7,988
  • 6
  • 39
  • 45
Keith
  • 543
  • 3
  • 8
  • the subclassing from `object` is redundant, but otherwise seems pretty legit. – rv.kvetch Jan 18 '22 at 15:43
  • 1
    True--I just copied the example above. Fixed. – Keith Jan 19 '22 at 17:37
  • 3
    Unfortunately, `dataclasses.astuple()` does deep unpacking which is often undesirable for nested structures -- (see https://stackoverflow.com/a/51802661/1190077 ). – Hugues Apr 14 '22 at 18:44
  • For a shallow unpacking (which is what you might want) use: `def __iter__(self): return iter(tuple(getattr(self, field.name) for field in dataclasses.fields(self)))`. – Adi Shavit May 27 '22 at 07:03
  • 1
    @AdiShavit why make the tuple _then_ the iterator? Just return the generator expression. You could add this as another answer. – jonrsharpe May 27 '22 at 07:26
  • 2
    Simpler and faster shallow unpacking `def __iter__(self): return iter(self.__dict__.values())` – crizCraig Jun 29 '22 at 22:33
  • 1
    self.__dict__.values() may not work if the dataclass is defined to use slots--a feature added in 3.10. – Keith Mar 22 '23 at 22:23
6

Another option would be a named tuple

Item = collections.namedtuple('Item', ['name', 'age', 'gender']) 

So this works out of the box:

a, b, c = Item("Henry", 90, "male")

If you're on Python 3.7+, you could also take advantage of dataclasses which are more powerful. But you would need to explicitly call astuple:

@dataclasses.dataclass
class Item(object):
    name: str
    age: int
    gender: str

a, b, c = dataclasses.astuple(Item("Henry", 90, "male"))
JBernardo
  • 32,262
  • 10
  • 90
  • 115
  • 1
    The named tuple can also be used as an (anonymous) base class, should you want to add other methods to the class. `class Item(collections.namedtuple('Item', ['name', 'age', 'gender'])): ...` – chepner Jan 02 '20 at 20:29
  • @chepner That is nice. I also added a similar solution with dataclasses, but they require you to be a bit more explicit – JBernardo Jan 02 '20 at 20:32
  • 2
    The question literally says "I would like to avoid using a namedtuple". – wim Jan 02 '20 at 20:38
  • @wim Sigh. I suggested he post the answer here since https://stackoverflow.com/q/59568853/1126841 was closed as a duplicate of this one. Maybe I should reopen that question, and the answer can be posted there? – chepner Jan 02 '20 at 20:46
  • @wim I had added a 2nd solution with data classes, so I find it is petty to complain about it. In any case, I was providing a solution to a similar question that was closed instead – JBernardo Jan 03 '20 at 12:35
  • Why not just use the same answer as @jonrsharpe with the dataclass? – yudhiesh Aug 18 '21 at 14:26
-1

You can overload Python's __new__() static method, as mentioned in the official Python documentation:

__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation.

So you can achieve a similar behavior by adapting the code like this

class Item(object):

    def __new__(self, name, age, gender):
        return (name, age, gender)


if __name__ == "__main__":
    a, b, c = Item("Henry", 90, "male")
    print(a,b,c)
    # output -> Henry 90 male
farch
  • 315
  • 4
  • 14