pythonpython-descriptors

Why setting __set__ function for method descriptor does not make it data-descriptor?


I've started reading about descriptors in Python and created small test to see whether the order of attribute access is same as it is described. As I understand when attribute of object is accessed it is looked in following order:

  1. in type(instance).__dict__ - if data descriptor of given name is present, its __get__method call is returned
  2. in instance.__dict__
  3. if non-data descriptor is present in type(instance).__dict__ then __get__ method call is returned
  4. __getattr__ is called.

Now, if method is non-data descriptor of type instance of given class, than by giving this descriptor __set__ function/method it would make it data descriptor, hence overwriting such function could be for example blocked or manipulated. Unfortunately that does not work and my question is simply: why is that a case? Does Python "marks" data and non-data descriptors at class initialization or I've done something wrong?

class A:
    def d(self):
        pass

def __set__(self, instance, value):
    print("Nothing to see here...")



A.__dict__['d'].__set__ = __set__
a = A()
a.d = "value"
print(a.d) # prints "value" instead of <bound method ...>

Solution

  • A.__dict__['d'].__set__ = __set__, which is just a complicated way of writing A.d.__set__ = __set__ in this case, is setting a magic method on the instance.

    This is not working in Python, magic methods are only looked up on the type. And you can't set it on the type, because the type is just the builtin function type here.

    However, it is possible to change a non-data descriptor into a data descriptor dynamically:

    >>> class MyDescriptor:
    ...     def __get__(self, instance, owner=None):
    ...         return 'foo'
    ... 
    >>> class A:
    ...     f = MyDescriptor()
    ... 
    >>> a = A()
    >>> a.f
    'foo'
    >>> a.f = 'foo2'
    >>> a.f
    'foo2'
    >>> MyDescriptor.__set__ = lambda self, instance, value: None
    >>> a.f  # now the descriptor takes precedence again over a.__dict__
    'foo'