Python Forum
[Classes] Classes [advanced]: Descriptors (managed attributes)
Thread Rating:
  • 3 Vote(s) - 3.33 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Classes] Classes [advanced]: Descriptors (managed attributes)
#1
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)
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
>>> 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:
class MyClass(object):
    def __init__(self):
        self.x = RevealAccess(10, 'var "x"')
        self.y = 5
This however won't work:
>>> 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.)
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
>>> 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.
__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.

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))
>>> 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
#2
This message left intentionally blank.


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Classes] Classes [advanced]: Dependent attributes (and Descriptors) Mekire 1 9,151 Oct-08-2016, 04:27 PM
Last Post: Kebap

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020