SoFunction
Updated on 2024-11-15

Proper use of attributes and descriptors in Python

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= -100Then 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 thescoreare 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 theticketFields 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 defaultgetsetbehavior, so that when we access or set theMovie().scoreare subject to the descriptorIntegerThe 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 deletingscorecap (a poem)ticketThe 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 Attributesscorecap (a poem)ticketAssociated?

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 descriptorIf so, use this.data descriptorProxies 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 descriptorThe 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.