Allow `obj[key]` to fallback to attribute-based `__getitem__`

Ok, seems like one never stops learning, so thanks for informing me.

You don’t need to enumerate all possible dunders.

Instead, use a namespace of methods bound to the proxied object:

def proxy(obj):
    class ProxyMeta(type):
        @classmethod
        def __prepare__(metacls, name, bases):
            return {name: getattr(obj, name) for name in dir(type(obj))}

    class Proxy(metaclass=ProxyMeta):
        pass

    return object.__new__(Proxy)

or:

def proxy(obj):
    return object.__new__(
        type(
            'Proxy',
            (),
            {name: getattr(obj, name) for name in dir(type(obj))}
        )
    )

d = {"a": 1}
p = proxy(d)
print(p["a"]) # 1
d["a"] = 2
print(p["a"]) # 2
p["a"] = 3 # write-back works too
print(d["a"]) # 3
4 Likes

Here’s a little more versatile solution:

def make_forward(name):
    if name in {"__new__", "__init__", "__getattribute__"}:
        return getattr(object, name)

    @property
    def forward(self):
        return self.__getattr__(name)

    return forward
proxies = {}

def proxy(obj):
    try:
        ProxyMeta, Proxy = proxies[type(obj)]
    except KeyError:
        class ProxyMeta(type):
            @classmethod
            def __prepare__(metacls, name, bases):
                return {name: make_forward(name) for name in dir(type(obj))}

        class Proxy(metaclass=ProxyMeta):
            def __init__(self, target):
                super().__setattr__("_target", target)

            def __getattr__(self, name):
                print(type(self), name)
                return getattr(super().__getattribute__("_target"), name)

        proxies[type(obj)] = ProxyMeta, Proxy

    return Proxy(obj)
>>> d = {"a": 1}
>>> p = proxy(d)
>>> p["a"]
<class '__main__.proxy.<locals>.Proxy'> __getitem__
1
1 Like

Note that the above code prevents any customization of the proxy object, since even the __getattribute__ method is bound to the proxied object. So if the Proxy class is defined with a custom method such as:

class Proxy(metaclass=ProxyMeta):
    def method(self):
        print(4)

then an AttributeError would be raised from trying to access method from the proxy because the method doesn’t exist in the proxied object:

d = {"a": 1}
p = proxy(d)
p.method() # AttributeError: 'dict' object has no attribute 'method'

So it’s likely more desirable to wrap __getattribute__ with a function that tries getting the requested attribute from the proxy object first before falling back to the proxied object:

def proxy(obj):
    def __getattribute__(self, name):
        try:
            return object.__getattribute__(proxy_obj, name)
        except AttributeError:
            return obj.__getattribute__(name)

    class ProxyMeta(type):
        @classmethod
        def __prepare__(metacls, name, bases):
            ns = {name: getattr(obj, name) for name in dir(type(obj))}
            ns['__getattribute__'] = __getattribute__
            return ns

    class Proxy(metaclass=ProxyMeta):
        def method(self):
            print(4)

    return (proxy_obj := object.__new__(Proxy))

d = {"a": 1}
p = proxy(d)
print(p["a"]) # 1
d["a"] = 2
print(p["a"]) # 2
p["a"] = 3
print(d["a"]) # 3
p.method() # 4