3

I'd like to make a class that unpacks it's objects like a dictionary.

For example, with a dictionary you can do this

foo = {
  "a" : 1
  "b" : 2
}

def bar(a,b):
   return a + b

bar(**foo)

outputs 3

And I'd like to be able to do this

class FooClass:
    def __init__(self):
        self.a = a
        self.b = b

f = FooClass()
bar(**f)

and have it output 3

This is the most related question I could find but it doesn't address this so I'm thinking it might not be possible.


Currently what my solution would be this:

class FooClass:
    def __init__(self):
        self.a = a
        self.b = b

    def to_dict(self):
        return {
          "a" : self.a,
          "b" : self.b
        }
f = FooClass()
bar(**f.to_dict())
financial_physician
  • 1,672
  • 1
  • 14
  • 34
  • 2
    That's not really possible as far as I'm aware, unless you're actually subclassing a `Mapping`. Otherwise it'll be something like `**vars(f)` or `**f.__dict__` or `**f.as_dict()`. – deceze Feb 16 '22 at 16:17
  • @deceze I'll look into subclassing mapping, thanks! – financial_physician Feb 16 '22 at 16:18
  • @deceze is the recommended answer for [this](https://stackoverflow.com/questions/3387691/how-to-perfectly-override-a-dict) post what you'd recommend?" – financial_physician Feb 16 '22 at 16:20
  • Also https://stackoverflow.com/questions/8601268/class-that-acts-as-mapping-for-unpacking and https://stackoverflow.com/q/37400133/4046632 – buran Feb 16 '22 at 16:23
  • @buran the second link might, the first is linked in my question. So does ** call the `keys` function behind the scene? – financial_physician Feb 16 '22 at 16:26
  • All of the links, incl. the one you post suggest that you implement `__iter__` for your class. In this `__iter__` dunder you can return whatever you want – buran Feb 16 '22 at 16:28
  • There's a function `vars` Python, which returns the dict attributes of python. Then your case can be done with: ```bar(**vars(FooClass()))``` – PySoL Feb 16 '22 at 16:28
  • you might be looking for `dataclasses.astuple` – rv.kvetch Feb 16 '22 at 21:11

3 Answers3

8

As pointed out in the comments, writing a conformant subclass of the collections.abc.Mapping abstract class is the way to go. To (concretely) subclass this class, you need to implement __getitem__, __len__, and __iter__ to behave consistently like a dictionary would. So that means __getitem__ expects a string, __iter__ returns an iterable of strings, etc.

For a simple example, we'll simply delegate all of these to self.__dict__, but in real code you'd likely want to do something more refined.

from collections.abc import Mapping

class FooClass(Mapping):

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __getitem__(self, x):
        return self.__dict__[x]

    def __iter__(self):
        return iter(self.__dict__)

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

def bar(a, b):
    return a + b

foo = FooClass(40, 2)
print(bar(**foo))
Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 3
    Actually, only `__iter__` is needed in this case – Drecker Feb 16 '22 at 16:28
  • Do you have a documentation link where that's specified? All I could find was "`**kwargs` expects a dictionary", which is obviously an oversimplification since the above code works. So I wasn't sure exactly where the line is drawn. – Silvio Mayolo Feb 16 '22 at 16:35
  • @Drecker I get `Can't instantiate abstract class _____ with abstract methods ` when I don't include the three methods @Silvio specifies – financial_physician Feb 16 '22 at 16:55
  • @Drecker Or are you saying that you don't need to subclass mapping either? – financial_physician Feb 16 '22 at 16:58
  • 1
    Yeah if I don't subclass `Mapping`, then I get `TypeError: bar() argument after ** must be a mapping, not FooClass` (even with all of the same code from above, just w/o the superclass). I think you *have* to be a `Mapping`. But I'd love to know where that's specified in the Python docs. – Silvio Mayolo Feb 16 '22 at 17:13
  • 1
    My bad, apparently implementing just `__iter__` is not enough (I just tested it) – Drecker Feb 17 '22 at 07:26
1
def bar(a, b):
   return a + b
  
class FooClass:
    def __init__(self, a, b):
        self.a = a
        self.b = b

f = FooClass(1, 2)

print(bar(*f.__dict__.values()))

# print(bar(**f.__dict__)) # Also works

Output:

3
Synthase
  • 5,849
  • 2
  • 12
  • 34
1

Aside from reyling on vars(f) or f.__dict__, you could use a dataclass.

from dataclasses import dataclass, asdict

@dataclass
class FooClass:
    a: int
    b: int

Demo:

>>> f = FooClass(1, 2)
>>> asdict(f)
{'a': 1, 'b': 2}
timgeb
  • 76,762
  • 20
  • 123
  • 145
  • 2
    There are so many goodies in the `dataclasses` module. Someday I need to just read it start to finish. I had no idea `asdict` was a thing. – Silvio Mayolo Feb 16 '22 at 16:35