Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Intercepting __getitem__ calls on an object attribute

Tags:

python

oop

Question: How can I Intercept __getitem__ calls on an object attribute?

Explanation:

So, the scenario is the following. I have an object that stores a dict-like object as an attribute. Every time the __getitem__ method of this attribute gets called, I want to intercept that call and do some special processing on the fetched item depending on the key. What I want would look something like this:

class Test:

    def __init__(self):
        self._d = {'a': 1, 'b': 2}

    @property
    def d(self, key):
        val = self._d[key]
        if key == 'a':
            val += 2
        return val
t = Test()
assert(t.d['a'] == 3) # Should not throw AssertionError

The problem is that the @property method doesn't actually have access to the key in the __getitem__ call, so I can't check for it at all to do my special postprocessing step.

Important Note: I can't just subclass a MutableMapping, override the __getitem__ method of my subclass to do this special processing, and store an instance of the subclass in self._d. In my actual code self._d is already a subclass of MutableMapping and other clients of this subclass need access to the unmodified data.

Thanks for any and all help!

like image 943
Mr. Frobenius Avatar asked Jul 03 '18 16:07

Mr. Frobenius


1 Answers

One solution would be a Mapping that proxies the underlying mapping. The d property would wrap the underlying self._d mapping in the proxy wrapper and return it, and use of that proxy would exhibit the necessary behaviors. Example:

from collections.abc import Mapping

class DProxy(Mapping):
    __slots__ = ('proxymap',)
    def __init__(self, proxymap):
        self.proxymap = proxymap
    def __getitem__(self, key):
        val = self.proxymap[key]
        if key == 'a':
            val += 2
        return val
    def __iter__(self):
        return iter(self.proxymap)
    def __len__(self):
        return len(self.proxymap)

Once you've made that, your original class can be:

class Test:
    def __init__(self):
        self._d = {'a': 1, 'b': 2}

    @property
    def d(self):
        return DProxy(self._d)

Users would then access instances of Test with test.d[somekey]; test.d would return the proxy, which would then modify the result of __getitem__ as needed for somekey. They could even store off references with locald = test.d and then use locald while preserving the necessary proxy behaviors. You can make it a MutableMapping if needed, but a plain Mapping-based proxy avoids complexity when the goal is reading the values, never modifying them through the proxy.

Yes, this makes a new DProxy instance on each access to d; you could cache it if you like, but given how simple the DProxy class's __init__ is, the cost is only meaningful if qualified access via the d attribute is performed frequently on the hottest of code paths.

like image 139
ShadowRanger Avatar answered Oct 20 '22 23:10

ShadowRanger