5

Let's imagine I have a single class X. The purpose of X is to wrap a list or dict and provide event-listening capabilities. All works well.

class X(object):
    def __init__(self, obj)
        self._obj = obj

    def __getattr__(self, name):
        # do stuff with self._obj

    def __getitem__(self, key):
        return self._obj[key]

    def __setitem__(self, key, val):
        self._obj[key] = val

    # rest of functionality ...

So this can be used to wrap a dict like so:

x = X({
    'foo' : False
})

x.listen('foo', callback)

X['foo'] = True         # triggers event
X.update({
    'foo' : False       # triggers event
})

Or a list:

x = X([1,2])

x.listen(callback)

X.append(1)        # triggers event
X[0] = 10          # triggers event

Great. Almost to what I wanted to accomplish ...

Now the current issue is that, because X is for both list and dict objects, it can't inherit from either. This means I don't have the magic class functions, such as __contains__.

Which leads code like this

d = X({
        'foo' : True    
    })

    if 'foo' in d:
        print 'yahoo!'

Throwing a KeyError.

How can I work around this without defining every magic method I need inside of X. If I did it this way, for each of those definitions I would have to write two return values based off whether self._obj is a list or dict.

I thought I could do this with metaclasses at first but that doesn't seem to be a solution, since I need access to the values being passed to check whether it's a dict or list.

X33
  • 1,310
  • 16
  • 37
  • Are you sure you need capabilities for both list and dict in your program? This sounds like a great place to split into two classes. – ollien Sep 22 '17 at 15:17
  • https://stackoverflow.com/questions/9942536/how-to-fake-proxy-a-class-in-python https://stackoverflow.com/questions/26091833/proxy-object-in-python – Josh Lee Sep 22 '17 at 15:18
  • @JoshLee Those don't address special methods. – X33 Sep 22 '17 at 15:27
  • Perhaps this approach will work? [How can I intercept calls to python's “magic” methods in new style classes?](https://stackoverflow.com/questions/9057669/how-can-i-intercept-calls-to-pythons-magic-methods-in-new-style-classes). The example `DictWrapper` includes a test of `__contains__` – jq170727 Sep 24 '17 at 16:20
  • @jq170727 Sure, I thought about that approach but I am being a bit picky in the sense that I don't want to have to call `x = XDict({})` or `x = XList([])`. I just want to call `X({})` or `X([])` to keep things simple for the end user. Especially in the future if I decide to extend this event-listener to user-defined objects like classes. – X33 Sep 24 '17 at 16:31
  • What good would it do for your `X` to inherit from `list` or `dict` since you want to wrap an existing object? – Davis Herring Sep 24 '17 at 17:45
  • @DavisHerring I need to inherit the special/magic methods from `list` or `dict` depending on the case, because I need the special methods like I provided in my question. I can't intercept magic method calls, so I need to have them added to the class dynamically before it is initalized. – X33 Sep 24 '17 at 17:50
  • 1
    If you _inherit_ them, they will access `self` rather than (the desired) `self._obj`. – Davis Herring Sep 24 '17 at 18:05
  • Why does `X` have to be a class? If `X` were a function it could inspect the type of `obj` and dynamically return the appropriate wrapper instance. – jq170727 Sep 24 '17 at 18:09
  • @DavisHerring You can't inherit a both a `list` and `dict` in the same class. – X33 Sep 24 '17 at 18:19
  • I do know about that restriction. I meant that inheriting from `list` _or_ `dict` would not have the desired effect in this case (even for one type to be wrapped). – Davis Herring Sep 24 '17 at 19:48
  • Is there a reason that this has to be both a list and dict? what do you get from one that you can't get from the other? You could just build one lightweight class `X()` that looks at what you pass into `__init__(self,obj):` and looks at obj returning a private type of the object you want that defines the same behavior you need. We had a "similar" situation where we were doing some stuff in Couchbase for events and we abandoned Lists/Array's and "engineered" Dicts/Maps to behave similarly. – bR3nD4n Sep 28 '17 at 02:37
  • 1
    Magic methods are only looked up on the class, not the instance. There is no way to dynamically select a magic method per instance. Just define all the magic methods on your class. – BrenBarn Sep 28 '17 at 07:05

3 Answers3

7

An easy way would be to use a proxy class, for example wrapt.ObjectProxy. It will behave exactly like the "proxied" class except for the overridden methods. However instead of self._obj you can simply use self.__wrapped__ to access the "unproxied" object.

from wrapt import ObjectProxy

class Wrapper(ObjectProxy):
    def __getattr__(self, name):
        print('getattr')
        return getattr(self.__wrapped__, name)

    def __getitem__(self, key):
        print('getitem')
        return self.__wrapped__[key]

    def __setitem__(self, key, val):
        print('setitem')
        self.__wrapped__[key] = val

    def __repr__(self):
        return repr(self.__wrapped__)

This behaves like a dict if you wrap a dict:

>>> d = Wrapper({'foo': 10})
>>> d['foo']
getitem
10
>>> 'foo' in d   # "inherits" __contains__
True

and like a list, if a list is wrapped:

>>> d = Wrapper([1,2,3])
>>> d[0]
getitem
1
>>> for i in d:   # "inherits" __iter__
...     print(i)
1
2
3
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • 4
    All `wrapt.ObjectProxy` does is provide all the special methods and attributes (the latter as `property` objects) and just proxy those to the wrapped object. So `def __contains__(self, value): return self.__wrapped__.__contains__(value)`. – Martijn Pieters Sep 25 '17 at 07:08
-1

You can use UserList or UserDict from collections

from collections import UserList


class X(UserList):
    def __init__(self, obj):
        super().__init__()
        self.data = obj

    def __getattr__(self, name):
        pass
        # do stuff with self.data

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, val):
        self.data[key] = val


x0 = X([1, 2, 3])
x1 = X({1, 2, 3})
x2 = X({1: 1, 2: 2})

print(1 in x0)
print(1 in x1)
print(1 in x2)
ADR
  • 1,255
  • 9
  • 20
-2

Try something like

def contains (self, item):
    If isinstance (self._obj, list):
        return list.contains (self._obj, item)
    If isinstance (self._obj, dict):
        return dict.contains (self._obj, item)

Or what ever method it is you want to inherit. When an instance method is called, the instance is auto-passed with self. But if you call the method from the class decleration you need to pass an object because self requires a value.

Class Test:
    def print_cls (self):
        print self.__class__.__name__


t = Test ()

t.print_cls () # self is teat

Test.print_cls (5) 
# we replace self with another class object
# as long as the object you pass has the
# variables used in the method you call all should be good.

This will print out Test and int

Brandon Nadeau
  • 3,568
  • 13
  • 42
  • 65
  • 2
    It looks like the goal is that an instance of `X` will look exactly like whatever object was passed to it, not that you have to call some special function like `contains()` rather than using the `in` operator. You also wouldn't want to special case particular classes like `list` and `dict`. I'm not sure what your snippet with the `Test` class is about, but it clearly doesn't have much to do with the question. – Arthur Tacca Sep 24 '17 at 16:38