7

I'm trying to make a dict-like class in Python.

When you make a class, you have certain methods that tell Python how to make a built-in class. For example, overriding the __int__ method tells Python what to return if the user uses int() on an instance of the class. Same for __float__. You can even control how Python would make an iterable object of the class by overriding the __iter__ method (which can help Python make lists and tuples of your class). My question is how would you tell Python how to make a dict of your custom class? There is no special __dict__ method, so how would you go about doing it? I want something like the following:

class Foo():
    def __dict__(self):
        return {
            'this': 'is',
            'a': 'dict'
        }

foo = Foo()
dict(foo) # would return {'this': 'is', 'a': 'dict'}

I've tried making the class inherit from dict, but it raises an error later in the code because of subclasses trying to inherit from dict and type, so inheriting from dict isn't a possibility. Is there any other way to do it?

Also, I've overridden the __iter__ method already so that it would return a dict_keyiterator object (what gets returned when you use iter() on a dict), but it still doesn't seem to work how it should.

  • Have you tried inheriting from `collections.abc.*Mapping` yet? – Ignacio Vazquez-Abrams Oct 08 '17 at 02:24
  • `collections.abc.Mapping` is a class of `ABCMeta`. A lot of my code was already built on top of the `type` metaclass, so a lot of it isn't compatible with `ABCMeta`. I could go through all my code trying to change it to work with `ABCMeta`, but I was just hoping there might be an easier way that doesn't rely on an abstract class like `Mapping`. – ChristianFigueroa Oct 08 '17 at 02:33
  • Just add a `@staticmethod` to your class, named something like `to_dict` for example, which you can explicitly call it to make the dictionary. i.e. `result = foo_obj.to_dict()`. – martineau Oct 08 '17 at 02:37
  • There is that workaround, but it seems like there should be an easier way to have Python natively convert a class to a `dict` considering there are `__int__`, `__str__`, and `__iter__` methods. I may have to resort to this, but it just seems more hack-ish than a regular `__dict__` method. – ChristianFigueroa Oct 08 '17 at 02:41
  • 1
    Using your analogy, I think it might be clearer to think "You can even control how Python would make an *iterable object* from the class by overriding the `__iter__` method." `dict`s are iterables, and like lists and tuples you can create them by passing in iterators. – ForeverWintr Oct 08 '17 at 02:53
  • 1
    @martineau, I don't think you'd want to use `@staticmethod` in this case, would you? Presumably your `to_dict` would need access to an instance in order to convert its attributes to a dictionary. – ForeverWintr Oct 08 '17 at 02:56
  • @ForeverWintr You're right. I edited the question to be more specific. – ChristianFigueroa Oct 08 '17 at 03:03
  • Ah. in that case I think @IgnacioVazquez-Abrams' suggestion is what you need. If you're unable to use `ABCMeta`, you can implement the `mapping` interface yourself. But why can't you inherit from `abc.Mapping`? Are you calling `type` explicitly? – ForeverWintr Oct 08 '17 at 03:16
  • I'm overriding the `__setattr__` method on my class. Once my script has finished executing, all the code inside the `__setattr__` method works fine. If something tries to get an attribute _before_ it's finished executing though, some errors are raised because the code references pointers that don't exist yet. If I inherit from `ABCMeta`, some code inside the `abc` module is trying to get an attribute from the class, so it's raising an error. I've tried fixing it already to work with `ABCMeta`, but then there's other `__instancecheck__` problems and the code just starts falling apart after that – ChristianFigueroa Oct 08 '17 at 03:24
  • 2
    I _kind of_ found an answer though by making a `keys` method on my class. If you use `dict` on a class, it'll try to get create a mapping first with the `keys()` method. I got it from here: https://stackoverflow.com/a/8601389/6212261 It doesn't use the `__iter__` method, so I can implement my own `__iter__` method _and still_ be able to make a `dict` with the same class. – ChristianFigueroa Oct 08 '17 at 03:26
  • 1
    ForeverWintr: Sorry...when I wrote that comment I meant a `@classmethod` which would have meant the function would need to be passed the object to operate upon, so calls to it would look like `result = Foo.to_dict(foo_obj)`—however you're right, it would be better to just make it a regular method, so calls could simply be something like `result = foo_obj.to_dict()`. – martineau Oct 08 '17 at 05:54

2 Answers2

5

dict can be called with an iterable of pairs, so if you design your __iter__ to return an iterable of tuples, your example works as you'd like:

class Foo:
    def __iter__(self):
        yield from {
            'this': 'is',
            'a': 'dict'
        }.items()

dict(Foo())
{'a': 'dict', 'this': 'is'}

If you want your class to behave like a python dictionary, in that iterating over an instance iterates over its keys, you can implement the interface defined by abc.Mapping.

You can do this either by implementing __getitem__, __iter__, and __len__, and inheriting from abc.Mapping, or by implementing all of __getitem__, __iter__, __len__ __contains__, keys, items, values, get, __eq__, and __ne__.

ForeverWintr
  • 5,492
  • 2
  • 36
  • 65
  • 2
    That does work correctly for creating a `dict` on the `Foo` object, but then you can't iterate through the keys of the items. Something like `for i in Foo()` wouldn't iterate through the _keys_ of the object, it would iterate through a _tuple_ of `(key, value)` tuples. – ChristianFigueroa Oct 08 '17 at 02:56
  • 1
    @Christian: Good point...however class `Foo` isn't a formal subclass of `dict`, so assuming it will behave exactly like one when it's iterated isn't completely reasonable. – martineau Oct 08 '17 at 06:03
  • ForeverWintr: FWIW, giving your class (just) a `__getitem__()` method would also allow built-in `dict` class objects to be created from instances of them. I still think it would be better to be more explicit and give the class a `to_dict()` method instead of relying on the somewhat obscure construction argument details of another class.. – martineau Oct 08 '17 at 06:29
  • 1
    ForeverWintr: As proof try replacing your `__iter__()` method with `def __getitem__(self, key):`, followed by `return [('this', 'is'), ('a', 'dict')][key]`—this makes it behave a little like a `list` of (key, value) pairs. – martineau Oct 08 '17 at 06:42
  • 1
    @martineau: Ah, of course! That's probably a better answer. Care to add it? – ForeverWintr Oct 08 '17 at 06:46
  • 1
    and re: to_dict(), I agree explicit is often preferable. But plumbing the depths of the language to answer these questions is interesting. – ForeverWintr Oct 08 '17 at 06:48
  • ForeverWintr: Agree completely about it being an interesting dive into the details of the language—but readability and maintainability shouldn't be thrown out the window as a result. – martineau Oct 08 '17 at 07:13
5

Although the approach in the answer from @ForeverWintr is fairly clever and works, I think it's a little obscure since it takes advantage of some of the arcane details about the attributes of the argument passed to the dict class constructor.

For that reason a better approach might be what I was saying in my comments about just adding a method that does what you want (plus shows how simply giving it a name indicating exactly what's going on makes it more understandable).

Example code:

class Foo:
    def to_dict(self):
        return {'this': 'is', 'more': 'clear'}

print( Foo().to_dict() )  # -> {'this': 'is', 'more': 'clear'}
martineau
  • 119,623
  • 25
  • 170
  • 301