Which Python dunder/magic methods do you need to implement to correctly proxy an object?
Asked Answered
F

1

27

I'm trying to create an object proxy. Attribute/property lookup can be done by simply implementing the __getattribute__, __setattr__ and __delattr__ methods. However, other functionalities like len(x), x[], bool(x) require other dunder methods like __len__, __getitem__, __bool__ to be implemented. If you don't implement these on the proxy class, but the object you're proxying supports them, your proxy will be incomplete and cause runtime errors.

I would therefore like to have a comprehensive list of all the things I need to implement, but I couldn't find any reliable list online.

Here's 97 unique dunder method names of them I got from the typing and builtins modules. I know what a lot of them do, but there are some that I have no clue about. It will be a pain to implement all or most of them for my proxy class, so I would be glad if there is a workaround.

__abs__
__add__
__aenter__
__aexit__
__aiter__
__and__
__anext__
__await__
__bool__
__bytes__
__call__
__class__
__cmp__
__complex__
__contains__
__delattr__
__delete__
__delitem__
__delslice__
__dir__
__div__
__divmod__
__enter__
__eq__
__exit__
__float__
__floordiv__
__format__
__fspath__
__ge__
__get__
__getattribute__
__getitem__
__getnewargs__
__getslice__
__gt__
__hash__
__iadd__
__iand__
__import__
__imul__
__index__
__init__
__init_subclass__
__instancecheck__
__int__
__invert__
__ior__
__isub__
__iter__
__ixor__
__le__
__len__
__lshift__
__lt__
__mod__
__mul__
__ne__
__neg__
__new__
__next__
__nonzero__
__or__
__pos__
__pow__
__prepare__
__radd__
__rand__
__rdiv__
__rdivmod__
__reduce__
__reduce_ex__
__repr__
__reversed__
__rfloordiv__
__rlshift__
__rmod__
__rmul__
__ror__
__round__
__rpow__
__rrshift__
__rshift__
__rsub__
__rtruediv__
__rxor__
__set__
__setattr__
__setitem__
__setslice__
__sizeof__
__str__
__sub__
__subclasscheck__
__subclasses__
__truediv__
__xor__
Ferebee answered 21/5, 2019 at 12:26 Comment(5)
this is going to be problematic even if you find them all. For instance, parsing only finds radd on the second operand of a '+' when it can't find add on the first one. So if you implement add, and your proxied object implements only radd, you will not see the same behavior. Your proxy would have to match the list of attributes of the given target object, not just all of them. At which point it knows so much about the target object that it is no longer a very useful proxy.Benco
You can use __getattr__ and/or __getattribute__ to catch any method which is not implemented directly in your proxyGrout
Do you even need all of them ? maybe it would have been better if you just implemented those you care about and use only them?Achitophel
Why would you want to do that?Precede
The methods you need to implement would depend on what makes sense for the class you are implementing. Only implement those methods which are applicable to the attributes of your class. For example, if your class is Person having attributes name, age, Idnumber, userid, user_rights. You don't need to implement such methods as slice, gt, lt, lte, gte, sizeof, etc.Cleodell
C
5

To proxy an object, you only need to implement the dunder methods that the object has, so in the simplest world, you wouldn't need to do anything special to proxy them that you're not already doing to proxy the object's other attributes.

However, the wrinkle is that dunder methods are looked up on the class, not on the object, so while for example Foo().bar will look up bar on the instance before falling back to the class if the instance has no bar attribute, Foo() + 5 will look up __add__ on the class Foo, completely ignoring the instance. That is, if the instance does have an instance attribute named __add__, then Foo() + 5 still won't use that instance attribute.

So to proxy those dunder methods, they need to be proxied at the class level, not the instance level.

from functools import wraps

def proxy_function(name, f):
    @wraps(f)
    def proxied_f(*args, **kwargs):
        print('Proxying function:', name)
        return f(*args, **kwargs)
    return proxied_f

def proxy_object(obj):
    class Proxy:
        def __getattr__(self, name):
            print('Proxying getattr:', name)
            return getattr(obj, name)
        def __hasattr__(self, name):
            print('Proxying hasattr:', name)
            return hasattr(obj, name)
        def __setattr__(self, name, value):
            print('Proxying setattr:', name, '=', repr(value))
            setattr(obj, name, value)
        def __delattr__(self, name):
            print('Proxying delattr:', name)
            delattr(obj, name)
    
    for name, f in obj.__class__.__dict__.items():
        # don't try to overwrite __class__, __getattr__, etc.
        if callable(f) and name not in Proxy.__dict__:
            f = proxy_function(name, f)
            setattr(Proxy, name, f)
    
    return Proxy()

Usage:

>>> class Foo:
...     def __add__(self, other):
...         return 'Adding with ' + repr(other)
... 
>>> foo = Foo()
>>> proxy_foo = proxy_object(foo)
>>> foo + 5
'Adding with 5'
>>> proxy_foo + 5
Proxying function: __add__
'Adding with 5'
Conde answered 31/8, 2021 at 1:8 Comment(3)
Note: This makes a unique class for each proxied object. That's fine if you're only proxying a handful of objects, but classes are heavyweight; if you do this for many objects of the same type, you'll want to implement some sort of per-wrapped-type proxy class cache, so wrapping 10,000 instances of type T doesn't involve 10,000 unique proxy classes, one for each instance.Hallelujah
@Hallelujah Yes, very good point. I did it this way in my answer because closing over obj is a lot simpler than giving the Proxy class a hidden attribute for obj and then trying to get the proxying mechanism to ignore that attribute internally. So this is just the simplest I can make it to demonstrate the idea, but the consequence is that you need a different class for each closure over a different obj.Conde
No worries. My comment wasn't intended as a criticism, just letting the OP know that they'll need to go a little further if they're planning to wrap a lot of stuff.Hallelujah

© 2022 - 2024 — McMap. All rights reserved.