Oct-06-2016, 10:12 PM
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)
The obvious solution to this seems it should be to make x an instance attribute instead:
(Note: I didn't write the following mixin; the original comes from here.)
You might have noticed that the __get__ method of a descriptor takes three arguments.
-Mek
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" 7The 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 = 5This 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" 10This 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