Descriptors of classes and objects in Python

  • 2020-04-02 14:45:22
  • OfStack

Descriptors are an abstruse but important part of the Python language. They are widely used in the Python language's kernel, and mastery of descriptors will add an extra trick to the Python programmer's toolbox. To set the stage for the discussion of descriptors that follows, I'll describe some of the scenarios that programmers might encounter in their everyday programming activities, and then I'll explain what descriptors are and how they provide elegant solutions to those scenarios. In this summary, I'll use the new style class to refer to the Python version.

1. Imagine a program where we need to perform strict type checking on an object property. Python, however, is a dynamic language, so it does not support type checking, but that does not prevent us from implementing our own version of, and more rudimentary, type checking. Traditional methods of object property type checking might take the following approach:


def __init__(self, name, age):
 if isinstance(str, name):
 self.name = name
 else:
 raise TypeError("Must be a string")
 if isinstance(int, age):
 self.age = age
 else:
 raise TypeError("Must be an int")

The above is one way to perform this type checking, but it gets tedious as the number of parameters increases. In addition, we can create a type_check(type, val) function to be called in s/s before assigning, but how can we simply implement this check when we want to set the property value elsewhere. One quick solution I came up with was getters and setters in Java, but this was not pythonic and cumbersome.

2. Suppose that in a program, we want to create properties that are immediately initialized at run time and then made read-only. One can also think of implementing it using a special method in Python, but this implementation is still clunky and cumbersome.

3. Finally, imagine a program where we want to somehow customize access to the properties of an object. For example, you need to record access to this property. Again, it is possible to think of a solution, even if it may be cumbersome and not reusable.

The above problems are all related to attribute references. Next, we will try to customize the access method for the property.
Python descriptor

Descriptors provide elegant, concise, robust, and reusable solutions to the problems listed above. In short, a descriptor is an object that represents the value of an attribute. This means that if an account object has a property "name", then the descriptor is another object that can be used to hold a value on behalf of the property "name". The descriptor is an object that implements one or more of these methods by "defining the special methods" of "s get__", "s set__" or "s delete__" in the descriptor protocol. The signature of each of these methods is shown below:


python descr.get(self,obj,type=None)->value . 
 
descr.__set__(self, obj, value) --> None
 
descr.__delete__(self, obj) --> None

The objects implementing the method are non-data descriptors, meaning that they can only be read after initialization. The object that implements both arbitration and arbitration is a data descriptor, meaning that this property is writable.

In order to better understand descriptors, we present descriptor-based solutions to the above problems. Implementing type checking for object properties using Python descriptors would be a very simple task. The decorator implements this type of checking as follows:


class TypedProperty(object):
 
 def __init__(self, name, type, default=None):
 self.name = "_" + name
 self.type = type
 self.default = default if default else type()
 
 def __get__(self, instance, cls):
 return getattr(instance, self.name, self.default)
 
 def __set__(self,instance,value):
 if not isinstance(value,self.type):
 raise TypeError("Must be a %s" % self.type)
 setattr(instance,self.name,value)
 
 def __delete__(self,instance):
 raise AttributeError("Can't delete attribute")
 
class Foo(object):
 name = TypedProperty("name",str)
 num = TypedProperty("num",int,42)
 
>> acct = Foo()
>> acct.name = "obi"
>> acct.num = 1234
>> print acct.num
1234
>> print acct.name
obi
# trying to assign a string to number fails
>> acct.num = '1234'
TypeError: Must be a <type 'int'>

In this example, we implement a descriptor TypedProperty, and the descriptor class performs type checking on any properties of the class it represents. It is important to note that descriptors can only be legally defined at the class level, not at the instance level. For example, in the preceding example of s/s method.

When any property of class Foo instance is accessed, the descriptor calls its s method. It is important to note that the first parameter of the method is the source object to which the property represented by the descriptor is referenced. When the property is assigned, the descriptor calls its successive set__ method. To understand why you can use descriptors to represent object attributes, we need to understand how attribute reference parsing is performed in Python. For an object, the attribute resolution mechanism is in object.getattribute__ (). This method converts b.x type (b) __dict__ [' x '] __get__ (b, type (b)). The parsing mechanism then searches for properties using a priority chain, in which the data descriptor found in the class dictionary has a higher priority than the instance variable, the instance variable has a higher priority than the non-data descriptor, and the priority chain assigns getattr() the lowest priority if getattr() is provided. For a given object class, you can override the priority chain by customing the getattribute__ method.

With a deep understanding of the priority chain, it is easy to come up with elegant solutions to the second and third problems presented earlier. That is, implementing a read-only property with a descriptor becomes a simple case of implementing a data descriptor, with no descriptor of the set__ method. Although not important in this example, the problem of defining the access mode simply requires adding the required functionality to the s/s methods.
Class attribute

Every time we want to use a descriptor, we have to define a descriptor class, which seems very tedious. Python features provide a concise way to add a data descriptor to an attribute. An attribute signature looks like this:
 


property(fget=None, fset=None, fdel=None, doc=None) -> property attribute

Fget, fset, and fdel are the getter, setter, and deleter methods of the class, respectively. Here's an example of how to create a property:


class Accout(object):
 def __init__(self):
 self._acct_num = None
 
 def get_acct_num(self):
 return self._acct_num
 
 def set_acct_num(self, value):
 self._acct_num = value
 
 def del_acct_num(self):
 del self._acct_num
 
 acct_num = property(get_acct_num, set_acct_num, del_acct_num, "Account number property.")

If acct is an instance of the Account, acct.acct_num will call the getter, acct.acct_num = value will call the setter, and del acct_num. Acct_num will call deleter.

In Python, attribute object and function as the (link: https://docs.python.org/2/howto/descriptor.html), use the descriptor protocol to implement, as shown below:


class Property(object):
 "Emulate PyProperty_Type() in Objects/descrobject.c"
 
 def __init__(self, fget=None, fset=None, fdel=None, doc=None):
 self.fget = fget
 self.fset = fset
 self.fdel = fdel
 if doc is None and fget is not None:
 doc = fget.__doc__
 self.__doc__ = doc
 
 def __get__(self, obj, objtype=None):
 if obj is None:
 return self
 if self.fget is None:
 raise AttributeError("unreadable attribute")
 return self.fget(obj)
 
 def __set__(self, obj, value):
 if self.fset is None:
 raise AttributeError("can't set attribute")
 self.fset(obj, value)
 
 def __delete__(self, obj):
 if self.fdel is None:
 raise AttributeError("can't delete attribute")
 self.fdel(obj)
 
 def getter(self, fget):
 return type(self)(fget, self.fset, self.fdel, self.__doc__)
 
 def setter(self, fset):
 return type(self)(self.fget, fset, self.fdel, self.__doc__)
 
 def deleter(self, fdel):
 return type(self)(self.fget, self.fset, fdel, self.__doc__)

Python also provides the @property decorator, which you can use to create read-only properties. A property object has getter, setter, and deleter decorator methods that can be used to create a copy of the property through the corresponding accessor function of the decorated function. This is best illustrated by the following example:


class C(object):
 def __init__(self):
 self._x = None
 
 @property
 # the x property. the decorator creates a read-only property
 def x(self):
 return self._x
 
 @x.setter
 # the x property setter makes the property writeable
 def x(self, value):
 self._x = value
 
 @x.deleter
 def x(self):
 del self._x

If we want the property to be read-only, then we can remove the setter method.

Descriptors are widely used in the Python language. Python functions, class methods, and static methods are all examples of non-data descriptors. The guide to descriptors gives a basic description of how enumerated Python objects are implemented using descriptors.


Related articles: