Classes [advanced]: Descriptors (managed attributes)

A place where you can post Python-related tutorials you made yourself, or links to tutorials made by others.

Classes [advanced]: Descriptors (managed attributes)

Postby Mekire » Mon Jul 29, 2013 6:14 am

This is related to my other tutorial Classes [advanced]: Dependent attributes (and Descriptors), but covers a different aspect of descriptor usage.

I was reading a thread recently in which there was some confusion on how to use descriptors with instance variables. This confusion is brought about by an example given in the official descriptor how-to guide.

(Print statements changed for compatibility)
Code: Select all
class RevealAccess(object):
    """A data descriptor that sets and returns values
       normally and prints a message logging their access.
    """

    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print("Retrieving {}".format(self.name))
        return self.val

    def __set__(self, obj, val):
        print("Updating {}".format(self.name))
        self.val = val


class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5
Code: Select all
>>> a = MyClass()
>>> b = MyClass()
>>> a.x
Retrieving var "x"
10
>>> b.x
Retrieving var "x"
10
>>> a.x = 7
Updating var "x"
>>> a.x
Retrieving var "x"
7
>>> b.x
Retrieving var "x"
7
The example creates a simple managed attribute which prints a message when its value is accessed or assigned. The issue is that in the above, x is a class attribute, not an instance attribute. If you create two instances of the same class, changing x in one changes x in the other.

The obvious solution to this seems it should be to make x an instance attribute instead:
Code: Select all
class MyClass(object):
    def __init__(self):
        self.x = RevealAccess(10, 'var "x"')
        self.y = 5
This however won't work:
Code: Select all
>>> a = MyClass()
>>> a.x
<__main__.RevealAccess object at 0x0289D450>
This is where the problem lies, and it is also where the theories on how to address this problem start flying around. There is a rather hackish solution that I have seen proposed which involves creating a mixin which you inherit from in any class you would like to have this functionality.

(Note: I didn't write the following mixin; the original comes from here.)
Code: Select all
class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print("Retrieving {}".format(self.name))
        return self.val

    def __set__(self, obj, val):
        print("Updating {}".format(self.name))
        self.val = val


class InstanceDescriptorMixin(object):
    def __getattribute__(self, name):
        value = object.__getattribute__(self, name)
        if hasattr(value, '__get__'):
            value = value.__get__(self, self.__class__)
        return value

    def __setattr__(self, name, value):
        try:
            obj = object.__getattribute__(self, name)
        except AttributeError:
            pass
        else:
            if hasattr(obj, '__set__'):
                return obj.__set__(self, value)
        return object.__setattr__(self, name, value)


class MyClass(InstanceDescriptorMixin):
    def __init__(self):
        self.x = RevealAccess(10, 'var "x"')
        self.y = 5
Code: Select all
>>> a.x
Retrieving var "x"
10
>>> b.x
Retrieving var "x"
10
>>> a.x = 5
Updating var "x"
>>> a.x
Retrieving var "x"
5
>>> b.x
Retrieving var "x"
10
This works perfectly. We can now assign descriptors directly to instance variables in the way we would normally expect. All that aside, I would rather not do it. Firstly the mixin is quite a hack; secondly we have to remember to inherit from this mixin any time we want to use descriptors; and finally (and this is the point), there is a much simpler solution.

You might have noticed that the __get__ method of a descriptor takes three arguments.
Code: Select all
__get__(self, obj, objtype)
The first self, as we would expect, refers to the instance of the descriptor being created. The second is the specific instance from which the descriptor's __get__ was called. And the third is the actual class. We can take full advantage of the second argument to suit our needs here.

Code: Select all
class RevealAccess(object):
    def __init__(self,variable):
        self.var = variable

    def __get__(self,instance,owner):
        print 'Retrieving var "{}"'.format(self.var)
        return getattr(instance,"_{}".format(self.var))

    def __set__(self, instance, value):
        print 'Updating var "{}"'.format(self.var)
        setattr(instance,"_{}".format(self.var),value)


class MyClass(object):
    x = RevealAccess("x")
    y = RevealAccess("y")

    def __init__(self,x,y):
        self._x = x
        self._y = y

    def __repr__(self):
        return "MyClass({_x}, {_y})".format(**vars(self))
Code: Select all
>>> a = MyClass(5,8)
>>> b = MyClass(3,7)
>>> a.x = 6
Updating var "x"
>>> a
MyClass(6, 8)
>>> b
MyClass(3, 7)
>>> b.y = 13
Updating var "y"
>>> a
MyClass(6, 8)
>>> b
MyClass(3, 13)

No inheritance necessary, and no need for a complicated overloading of __getattribute__. The descriptors themselves remain class attributes as they were intended to; and their __get__ and __set__ methods modify the “real” instance variables. From the viewpoint of the user, functionality is identical.

-Mek
User avatar
Mekire
 
Posts: 1010
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Return to Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest