![]() |
[Classes] Classes [advanced]: Descriptors (managed attributes) - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: General (https://python-forum.io/forum-1.html) +--- Forum: Tutorials (https://python-forum.io/forum-4.html) +---- Forum: Fundamentals (https://python-forum.io/forum-40.html) +---- Thread: [Classes] Classes [advanced]: Descriptors (managed attributes) (/thread-361.html) |
Classes [advanced]: Descriptors (managed attributes) - Mekire - Oct-06-2016 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" 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 RE: Classes [advanced]: Descriptors (managed attributes) - Kebap - Oct-08-2016 This message left intentionally blank. |