Python: super () - as a proxy object that starts the MRO search in the specified class

According to docs, super(cls, obj) returns

proxy object that delegates method calls to parents or relatives class of type cls

I understand why super() offers this function, but I need something a little different: I need to create a proxy object that delegates method calls (and attribute searches) to the cls class itself; and as in super , if cls does not implement the method / attribute, my proxy should continue searching in the order of MRO (new, not the original class). Is there any function that I can write that achieves this?

Example:

 class X: def act(): #... class Y: def act(): #... class A(X, Y): def act(): #... class B(X, Y): def act(): #... class C(A, B): def act(): #... c = C() b = some_magic_function(B, c) # `b` needs to delegate calls to `act` to B, and look up attribute `s` in B # I will pass `b` somewhere else, and have no control over it 

Of course, I could do b = super(A, c) , but that depends on knowing the exact hierarchy of classes and the fact that B follows A in the MRO. He would be silent if either of these two assumptions changes in the future. (Note that super does not make such assumptions!)

If I just needed to call b.act() , I could use B.act(c) . But I pass B someone else and don’t know what to do with it. I need to make sure that this does not betray me and at some point starts acting like an instance of class C

A separate question, the documentation for super() (in Python 3.2) speaks only about its delegation of the method and does not specify that the search for attributes for the proxy server is also performed the same way. Is this a random omission?

EDIT

The updated Delegate approach also works in the following example:

 class A: def f(self): print('A.f') def h(self): print('A.h') self.f() class B(A): def g(self): self.f() print('B.g') def f(self): print('B.f') def t(self): super().h() a_true = A() # instance of A ends up executing Af a_true.h() b = B() a_proxy = Delegate(A, b) # *unlike* super(), the updated `Delegate` implementation would call Af, not Bf a_proxy.h() 

Please note that the updated class Delegate closer to what I want than super() for two reasons:

  • super() only performs proxying for the first call; subsequent calls will be made as usual, since by that time the object is used, and not its proxy.

  • super() does not allow access to the attribute.

So my assignment question has a (almost) perfect answer in Python.

It turns out that at a higher level I was trying to do something that I should not ( see my comments here ).

+4
source share
2 answers

This class should cover the most common cases:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self._delegate_obj) return x 

Use it as follows:

 b = Delegate(B, c) 

(with the names from your example code.)

Limitations:

  • You cannot get some special attributes like __class__ etc. from the class that you pass in the constructor through this proxy. (These adjustments also apply to super .)

  • This can lead to migration if the attribute you want to get is some kind of reliable handle.

Change If you want the code in updating your question to work as desired, you can use the following code:

 class Delegate: def __init__(self, cls): self._delegate_cls = cls def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self) return x 

This passes the proxy object as a self parameter to any called method, and it does not need the original object at all, so I removed it from the constructor.

If you also want instance attributes to be available, you can use this version:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): if name in vars(self._delegate_obj): return getattr(self._delegate_obj, name) x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self) return x 
+3
source

A separate issue, the documentation for super () (in Python 3.2) only talks about delegating its method and does not specify that Attributes are searched in the same way for a proxy server. Is this a random omission?

No, this is no coincidence. super() does nothing to search for attributes. The reason is that the attributes of the instance are not associated with a particular class, they are just there. Consider the following:

 class A: def __init__(self): self.foo = 'foo set from A' class B(A): def __init__(self): super().__init__() self.bar = 'bar set from B' class C(B): def method(self): self.baz = 'baz set from C' class D(C): def __init__(self): super().__init__() self.foo = 'foo set from D' self.baz = 'baz set from D' instance = D() instance.method() instance.bar = 'not set from a class at all' 

Which class owns foo , bar and baz ?

If I wanted to view instance as an instance of C, should it have a baz attribute before calling method ? How about later?

If I consider instance as an instance of A, what value should foo ? Should bar be invisible because it was added only to B, or visible because it was set to a value outside the class?

All these questions are nonsense in Python. There is no way to create a system with Python semantics that can provide reasonable answers to them. __init__ is not even special in terms of adding attributes to class instances; this is just a completely normal method, which is considered to be called part of the instantiation protocol. Any method (or, indeed, code from another class at all or not from any class at all) can create attributes on any instance to which it refers.

In fact, all instance attributes are stored in the same place:

 >>> instance.__dict__ {'baz': 'baz set from C', 'foo': 'foo set from D', 'bar': 'not set from a class at all'} 

It is not possible to determine which ones were originally given by the class or were the last given by which class or whatever measure of ownership you would like. Of course, there is no way to get " A.foo D.foo ", as you would expect from C ++; they are one and the same attribute, and any records on it by one class (or from another place) will compress the value remaining in it by another class.

The consequence of this is that super() does not perform attribute searches in the same way as method searches; he cannot, and no code can write.


In fact, starting with some experiments, neither super nor Sven Delegate actually support direct attribute searches at all!

 class A: def __init__(self): self.spoon = 1 self.fork = 2 def foo(self): print('A.foo') class B(A): def foo(self): print('B.foo') b = B() d = Delegate(A, b) s = super(B, b) 

Then both work as expected for the methods:

 >>> d.foo() A.foo >>> s.foo() A.foo 

But:

 >>> d.fork Traceback (most recent call last): File "<pyshell#43>", line 1, in <module> d.fork File "/tmp/foo.py", line 6, in __getattr__ x = getattr(self._delegate_cls, name) AttributeError: type object 'A' has no attribute 'fork' >>> s.spoon Traceback (most recent call last): File "<pyshell#45>", line 1, in <module> s.spoon AttributeError: 'super' object has no attribute 'spoon' 

Thus, they both really only work for calling some methods, and not for passing to an arbitrary third code, in order to claim the role of an instance of the class that you want to delegate.

They do not behave the same in the presence of multiple inheritance. Given:

 class Delegate: def __init__(self, cls, obj): self._delegate_cls = cls self._delegate_obj = obj def __getattr__(self, name): x = getattr(self._delegate_cls, name) if hasattr(x, "__get__"): return x.__get__(self._delegate_obj) return x class A: def foo(self): print('A.foo') class B: pass class C(B, A): def foo(self): print('C.foo') c = C() d = Delegate(B, c) s = super(C, c) 

Then:

 >>> d.foo() Traceback (most recent call last): File "<pyshell#50>", line 1, in <module> d.foo() File "/tmp/foo.py", line 6, in __getattr__ x = getattr(self._delegate_cls, name) AttributeError: type object 'B' has no attribute 'foo' >>> s.foo() A.foo 

Because Delegate ignores the full MRO of any _delegate_obj class, it is an instance only using the MRO _delegate_cls . While super does what you asked in the question, but the behavior seems rather strange: it does not complete the instance of C to give it an instance of B, because direct instances of B do not have foo .

Here is my attempt:

 class MROSkipper: def __init__(self, cls, obj): self.__cls = cls self.__obj = obj def __getattr__(self, name): mro = self.__obj.__class__.__mro__ i = mro.index(self.__cls) if i == 0: # It at the front anyway, just behave as getattr return getattr(self.__obj, name) else: # Check __dict__ not getattr, otherwise we'd find methods # on classes we're trying to skip try: return self.__obj.__dict__[name] except KeyError: return getattr(super(mro[i - 1], self.__obj), name) 

I rely on the __mro__ attribute of classes to correctly determine where to start, and then just use super . You can go through the MRO chain from now on by checking the __dict__ class instead of methods if it is too difficult to go back to one step to use super .

I did not try to handle unusual attributes; those that were implemented with descriptors (including properties), or those magic methods that looked behind the scenes of Python, which often begin in the class, and not directly with the instance. But this behaves the same way you asked moderately well (with the warning set out in the commercial break in the first part of my message, searching for attributes in this way will not give you any other results than finding them directly in the instance).

+2
source

Source: https://habr.com/ru/post/1390699/


All Articles