About the @property decorator
In Python we use @property decorators to disguise calls to functions as access to properties.
So why do it this way? Because @property lets us tie our custom code to variable access/setting, while maintaining a simple interface for your class to access properties.
For example, suppose we have a class that needs to represent a movie:
class Movie(object): def __init__(self, title, description, score, ticket): = title = description = scroe = ticket
You start using this class elsewhere in the project, but then you realize: what if you accidentally give the movie a negative score? You feel that this is wrong behavior, and hope that the Movie class will prevent this mistake. The first thing you think of is to change the Movie class to look like this:
class Movie(object): def __init__(self, title, description, score, ticket): = title = description = ticket if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) = scroe
But that won't work. This is because the other parts of the code are directly passed through theto assign the value. This newly modified class will only be used in the
__init__
method catches erroneous data, but it can't do anything about class instances that already exist. If someone tries to run the= -100
Then there's nothing anyone can do to stop it. So what to do?
Python's property solves this problem.
We can do this.
class Movie(object): def __init__(self, title, description, score): = title = description = score = ticket @property def score(self): return self.__score @ def score(self, score): if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) self.__score = score @ def score(self): raise AttributeError("Can not delete score")
This modifies anywhere thescore
are tested to see if it is less than 0.
Shortcomings of property
The biggest drawback for properties is that they can't be reused. For example, suppose you want to add a new value to theticket
Fields also add non-negative checks.
Here is the new class with modifications:
class Movie(object): def __init__(self, title, description, score, ticket): = title = description = score = ticket @property def score(self): return self.__score @ def score(self, score): if score < 0: raise ValueError("Negative value not allowed:{}".format(score)) self.__score = score @ def score(self): raise AttributeError("Can not delete score") @property def ticket(self): return self.__ticket @ def ticket(self, ticket): if ticket < 0: raise ValueError("Negative value not allowed:{}".format(ticket)) self.__ticket = ticket @ def ticket(self): raise AttributeError("Can not delete ticket")
You can see that the code has increased quite a bit, but the duplicate logic has also appeared quite a bit. While property can make a class look neat and pretty externally in terms of interface, it can't do the same internally.
Descriptors on the scene
What are descriptors?
In general, a descriptor is an object property with bound behavior, and access to its properties is overridden by descriptor protocol methods. These methods are__get__()
、 __set__()
respond in singing__delete__()
, an object is said to be a descriptor as long as it contains at least one of these three methods.
What do descriptors do?
The default behavior for attribute access is to get, set, or delete the attribute from an object's dictionary. For instance, has a lookup chain starting witha.__dict__[‘x'], then type(a).__dict__[‘x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.—–Excerpts from official documents
put it simplyDescriptors change the basic get, set, and delete methods for an attribute.。
Let's first see how we can use descriptors to solve the problem of duplicate property logic above.
class Integer(object): def __init__(self, name): = name def __get__(self, instance, owner): return instance.__dict__[] def __set__(self, instance, value): if value < 0: raise ValueError("Negative value not allowed") instance.__dict__[] = value class Movie(object): score = Integer('score') ticket = Integer('ticket')
Because the descriptor has a high priority and will change the defaultget
、set
behavior, so that when we access or set theMovie().score
are subject to the descriptorInteger
The limitations of the
But we can't always create instances in the following way.
a = Movie() = 1 = 2 = ‘test' = ‘…'
That's too raw, so we're still missing a constructor.
class Integer(object): def __init__(self, name): = name def __get__(self, instance, owner): if instance is None: return self return instance.__dict__[] def __set__(self, instance, value): if value < 0: raise ValueError('Negative value not allowed') instance.__dict__[] = value class Movie(object): score = Integer('score') ticket = Integer('ticket') def __init__(self, title, description, score, ticket): = title = description = score = ticket
This way, when getting, setting, and deletingscore
cap (a poem)ticket
The time is all going into theInteger
(used form a nominal expression)__get__
、 __set__
, thus reducing the duplication of logic.
Now while the problem is solved, you may be wondering how exactly this descriptor works. Specifically, the__init__
function accesses its owncap (a poem)
How and Class Attributes
score
cap (a poem)ticket
Associated?
How Descriptors Work
See the official description
If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).
Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance's dictionary. If an instance's dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance's dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.
The important points to remember are:
descriptors are invoked by the __getattribute__() method
overriding __getattribute__() prevents automatic descriptor calls
object.__getattribute__() and type.__getattribute__() make different calls to __get__().
data descriptors always override instance dictionaries.
non-data descriptors may be overridden by instance dictionaries.
class call__getattribute__()
The time was probably like this below:
def __getattribute__(self, key): "Emulate type_getattro() in Objects/" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v
The following is an excerpt from a foreign blog.
Given a Class “C” and an Instance “c” where “c = C(…)”, calling “” means looking up an Attribute “name” on the Instance “c” like this:
Get the Class from Instance
Call the Class's special method getattribute__. All objects have a default __getattribute
Inside getattribute
Get the Class's mro as ClassParents
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a data descriptor
Return the result from calling the data descriptor's special method __get__()
Break the for each (do not continue searching the same Attribute any further)
If the Attribute is in Instance's dict
Return the value as it is (even if the value is a data descriptor)
For each ClassParent in ClassParents
If the Attribute is in the ClassParent's dict
If is a non-data descriptor
Return the result from calling the non-data descriptor's special method __get__()
If it is NOT a descriptor
Return the value
If Class has the special method getattr
Return the result from calling the Class's special method__getattr__.
My understanding of the above is that accessing an instance's attributes is done by first traversing it and its parent class, looking for their__dict__
Do you have the same name?data descriptor
If so, use this.data descriptor
Proxies the property, and if it doesn't then looks for the instance's own__dict__
If it does, it returns it. Anyone who doesn't then look up it and its parent class in thenon-data descriptor
The last thing to look for is whether or not the__getattr__
Application Scenarios for Descriptors
python's property, classmethod modifiers are themselves descriptors, and even ordinary functions are descriptors (non-data discriptor)
Descriptors are also available in django model and SQLAlchemy.
class User(): id = (, primary_key=True) username = ((80), unique=True) email = ((120), unique=True) def __init__(self, username, email): = username = email def __repr__(self): return '<User %r>' %
summarize
Properties should only be used when there is a real need to do some additional processing when accessing the property, otherwise the code will become more verbose and it will slow down the program a lot. The above is the entire content of this article, due to limited personal capacity, if there are errors in the text, logical errors or even conceptual errors, please put forward and correct.