The Python @property descriptor is used by developers as a neat way to create special class attributes. If you don’t know about Pythons descriptor protocol you should read up on it, there's plenty of great resources out there to describe how it works (especially the official Python docs). The goal of this article is to cover a slightly more "advanced" usage of @property descriptors which allows developers to create instance attributes that are derrived from a Python property class.
Remember that Python descriptors work on classes not instances! Therefore, all instances - of a specific Python class which contain "Python descriptor attributes" - will contain the same class attributes (aka the properties) that all share the same value. In other words, when you define an attribute for a class which is derrived from an @property descriptor, you are defining a class variable.
The following code snippet defines a (simplest) Python descriptor class:
class MyDescriptor:
def __init__(self, value):
self._value = value
def __get__(self, owner_instance, owner_type):
return self._value
def __set__(self, owner_instance, value):
self._value = value
You may think usage for this simple descriptor might look something like:
class MyClass:
bar = MyDescriptor("bar")
def __init__(self):
self.foo = MyDescriptor("foo")
Lets start with the class variable MyClass.bar
:
first_instance = MyClass()
second_instance = MyClass()
print(f"first_instance.bar = {first_instance.bar}")
# prints "bar"
first_instance.bar = "bar new"
print(f"second_instance.bar = {first_instance.bar}")
# prints "bar new"
This is exactly what we should expect - considering MyClass.bar
was defined on the class (so all instances share the same value for that attribute).
However, looking into the instance attribute MyClass.foo
may not yeild the expected result, for example:
first_instance = MyClass()
second_instance = MyClass()
print(f"first_instance.foo = {first_instance.foo}")
# prints "<__main__.MyDescriptor object at 0x7f87492899e8>"
print(f"second_instance.foo = {second_instance.foo}")
# prints "<__main__.MyDescriptor object at 0x7f87492897a9>"
Note: You may have expected to see the value held by the descriptor MyClass.foo
be the value printed. However - as I mentioned before - Python descriptors work on classes not instances! So what does the descriptor protocol do to get/set this value? Well, that's simple... it calls the __get__
and __set__
methods defined on your descriptor class! Armed with this knowledge you can simply "manually" implement this functionality yourself. How? By creating a custom base class:
class Base:
"""
A base class
"""
def __setattr__(self, attr, value):
try:
# Try invoking the descriptor protocol __set__ "manually"
got_attr = super().__getattribute__(attr)
got_attr.__set__(self, value)
except AttributeError:
# Attribute is not a descriptor, just set it:
super().__setattr__(attr, value)
def __getattribute__(self, attr):
# If the attribute does not exist, super().__getattribute__()
# will raise an AttributeError
got_attr = super().__getattribute__(attr)
try:
# Try "manually" invoking the descriptor protocol __get__()
return got_attr.__get__(self, type(self))
except AttributeError:
# Attribute is not a descriptor, just return it:
return got_attr
This base class will override the standard __setattr__
and __getattribute__
methods of the class and tries to manually invoke the descriptor protocol to get/set the attribute value, if that fails it treats the attribute as a "normal" attribute by letting the super class (in this case, object
) deal with the request (as normal).
Finally, the MyClass
needs to be modified to inherit from Base
:
class MyClass(Base):
bar = MyDescriptor("bar")
def __init__(self):
self.foo = MyDescriptor("foo")
And using the class to create some instances and get values held by instance owned descriptor attributes:
first_instance = MyClass()
second_instance = MyClass()
print(f"first_instance.foo = {first_instance.foo}")
# prints "foo"
print(f"second_instance.foo = {second_instance.foo}")
# prints "foo"
first_instance.foo = "foo new"
print(f"first_instance.foo = {first_instance.foo}")
# prints "foo new"
print(f"second_instance.foo = {second_instance.foo}")
# prints "foo"
And there you have it! A clean & simple solution if you want Python descriptor attributes to work on a Python instance, not just the class.
Top comments (2)
Nice. The Python docs about descriptors obscure the fact that descriptors are class variables, not instance variables.
There seem to be other ways to accomplish instance properties.
I have found dealing with descriptors very fragile, much to understand. Even your code. I cut and pasted your base class into my existing code that had class properties. And I do not get the same result: the first two lines of your final test still print something like <main.MyDescriptor object at 0x7f87492899e8>, but the last two lines print the same result as you show, as if you must set the instance property before getting it! Something strange I have done no doubt.
I've not looked into the full detail, but I'll give you an alternative approach using a metaclass:
The metaclass (this must be the base type) for your class:
And the class you want to use instance descriptors on, looks pretty normal:
A quick test: