Python Property descriptor (i)

Source: Internet
Author: User

Descriptors are a way to apply the same access logic to multiple properties, which is the class that implements the feature protocol, which includes the __get__, __set__, and __delete__ methods. The property class implements the complete descriptor protocol. In general, you can implement only partial protocols, such as __get__ or __set__, without having to implement all __get__, __set__, and __delete__

Now let's use the Descriptor protocol to upgrade the previous section of the Python Dynamic properties and attributes (ii) of the LineItem class

Figure 1-1

We will define a quantity class, and the LineItem class will use two quantity instances: one for managing the weight property and the other for managing the price property. Weight This attribute occurs two times, but both are different, one is the LineItem class property, the other is the instance property of each LineItem object, and the same price

Now, let's look at some definitions:

    • Descriptor classes: Classes that implement descriptor protocols, such as __set__, __get__, or __delete__ methods, 1-1 of the quantity class
    • Managed class: A class that declares a descriptor instance as a class property, and both weight and price in the LineItem class in 1-1 are class properties and are instances of the quantity Descriptor class
    • Descriptor instance: each instance of a descriptor class declared as a class property of a managed class, such as the weight and price properties in the LineItem class
    • Managed instance: an instance of a managed class, in Figure 1-1, an instance of the LineItem class is a managed class instance
    • Store Properties: The properties of the managed instance that store their own managed properties. In Figure 1-1, the weight and price properties of the LineItem instance are storage properties. This property is different from the Descriptor property, and the descriptor attribute is a class property
    • Managed properties: The exposed property in the managed class that is handled by the descriptor instance, and the value is stored in the stored property. That is, the descriptor instance and the storage attribute establish the basis for the managed property

Now let's take a look at an example

Class Quantity:  # <3>    def __init__ (self, Storage_name):  # <4>        self.storage_name = Storage _name    def __set__ (self, instance, value):  # <5>        if value > 0:            instance.__dict__[self.storage _name] = value        else:            raise ValueError (' value must be > 0 ') class LineItem:    weight = Quantity (' weight ') c11/># <1> Price    = Quantity ("Price")    def __init__ (self, description, weight, price):  # <2>< c15/>self.description = description        self.weight = weight        Self.price = Price    def subtotal (self):        Return self.weight * Self.price

  

We have the above code corresponding to the previous definition, the first is the quantity class, as we have said before, as long as the implementation of the __set__, __get__ or __delete__ method of the class, is the descriptor class, so quantity is undoubtedly the descriptor class, Again, LineItem, according to the previous definition, the class attribute in the managed class is an instance of the descriptor class, and the weight and price two class properties of the LineItem class are instances of the quantity descriptor class, so the LineItem class is the managed class, and then the We analyze the code according to the label in the code:

    1. LineItem two attributes weight and price are descriptor instances
    2. When instantiating a LineItem object, pass in the weight and price parameters, because these two properties implement the descriptor protocol, so the read, value, or delete values of weight and price may be associated to a method in the quantity instance corresponding to the class property of the same name. Because only the __set__ method is implemented in the quantity class, reading and deleting values here do not trigger methods in the quantity instance
    3. Quantity as Descriptor class
    4. The quantity instance has a Storage_name property, which is the name of the property that stores the value in the managed instance
    5. When we want to set the weight or price property in the LineItem instance, the __set__ method is triggered, in which self is the descriptor instance, which is the weight instance of quantity or price in the LineItem class. Instance is a managed class instance, which is an instance of LineItem, value is what we want to set, and if value is greater than 0, set its property name and property value to the instance.__dict__ dictionary

Now let's test this class, we deliberately set the incoming price to 0:

Truffle = LineItem (' white truffle ', 100, 0)

  

Operation Result:

Traceback (most recent): ... Valueerror:value must be > 0

  

You can see that when the setting is worth it does trigger the __set__ method

Also repeat the declaration: arguments in the __set__ method, self and instance are both descriptor instances and managed class instances, instance represents the object to set the property, and self (the descriptor instance) saves the property name to set the property, in the last example, If we were to set the LineItem instance in the __set__ method only in this way:

Instance.__dict__[self.storage_name] = value

  

If you try to assign a value by using the SetAttr () method

Class Quantity:      def __init__ (self, storage_name):         self.storage_name = Storage_name    def __set__ (self, instance, value):          if value > 0:            setattr (instance, self.storage_name, value)        else:            raise ValueError (' value must be > 0 ')

  

Test:

Truffle = LineItem (' white truffle ', 100, 10)

  

Operation Result:

Traceback (most recent): ... Recursionerror:maximum recursion depth exceeded

  

We will find that if we use the SetAttr () method to assign a value, it will produce a stack exception. Assuming that obj is an LineItem instance, Obj.price = 10 and SetAttr (obj, "price", 10) Call the __set__ method, and if you set the value with the SetAttr () method, the __set__ method is called continuously. Stack exception is eventually generated

In the example above, LineItem has the disadvantage of repeating the input property name every time a descriptor is instantiated in a managed class, now let's transform the LineItem class so that it doesn't need to enter a property name. To avoid repeating input property names in the descriptor instance, we generate a unique string for the Storage_name property in each quantity instance, plus a __get__ method for the Descriptor class

Import Uuidclass Quantity:    def __init__ (self):  # <1>        cls = self.__class__        prefix = cls.__name__< c4/>identity = str (UUID.UUID4 ()) [: 8]        self.storage_name = ' _{}#{} '. Format (prefix, identity)    def __get__ (self, instance, owner):  # <2>        return GetAttr (instance, self.storage_name)    def __set__ (self, instance, Value):  # <3>        if value > 0:            setattr (instance, self.storage_name, value)        else:            Raise ValueError (' value must is > 0 ') class LineItem:    weight = Quantity () Price    = Quantity ()    def __init_ _ (Self, description, weight, price):        self.description = description        self.weight = weight        Self.price = Price    def subtotal (self):        return self.weight * self.price

    

Test:

Raisins = LineItem (' Golden raisins ', ten, 6.95) print (Raisins.weight, raisins.description, Raisins.price)

  

Operation Result:

Ten Golden raisins 6.95

  

    1. When instantiating the quantity descriptor class here, we no longer require a storage_name to be passed in, but instead generate a storage_name in the initialization method, which consists of a random string generated by the class name and UUID.
    2. We know that if we assign a value to an attribute in an instance, if the property name is defined in the class as a descriptor instance, the __set__ method is automatically triggered when the value is assigned, and the __get__ method is automatically triggered when we read the value __get__ Methods in addition to the self (descriptor instance) will also pass two parameters, instance and Owner,instance is a managed class instance, owner is a managed class, in our example above instance is LineItem instance, owner is the LineItem class, When reading an attribute in an instance, if the attribute is defined as a descriptor instance in the class, the __get__ method is triggered
    3. In the __set__ method, we no longer call instance.__dict__[self.storage_name] = value to assign a value, but instead use the SetAttr () method directly to assign a value. In the previous example, we tested the stack overflow exception if we used the SetAttr () method to assign a value, so why are we here again? Because, when we really store property values, the property name is not the descriptor name of the class, but rather the Python interpreter generates a random string of Quantity_#_{uuid}, and this random string is not registered as a descriptor instance in the class. So we call SetAttr () and no more stack exceptions like we did before.

Here's another point, when we try to print lineitem.weight this descriptor instance

Lineitem.weight

  

Operation Result:

Traceback (most recent): ...    Return GetAttr (instance, self.storage_name) Attributeerror: ' Nonetype ' object has no attribute ' _quantity#f9860e73 '

  

We will find that accessing Lineitem.weight throws an Attributeerror exception, because the __get__ method is also called when the Lineitem.weight property is accessed, and at this time instance is passed a none, To solve this problem, we detect in the __get__ method that if the incoming instance is none, the current descriptor instance is returned, and if instance is not none, the instance property in instance is returned

Import Uuidclass Quantity:    def __init__ (self):        cls = self.__class__        prefix = cls.__name__        identity = STR (UUID.UUID4 ()) [: 8]        self.storage_name = ' _{}#{} '. Format (prefix, identity)    def __get__ (self, instance, Owner):        If instance is None:            return self         else:            return GetAttr (instance, self.storage_name)     def _ _set__ (self, instance, value):        if value > 0:            setattr (instance, self.storage_name, value)        else:            Raise ValueError (' value must be > 0 ')

  

Here we modify another section of the Python Dynamic properties and attributes (ii) of the quantity () attribute factory method so that it does not need to pass in the Storage_name

Import Uuiddef Quantity ():    storage_name = ' _{}:{} '. Format (' Quantity ', str (UUID.UUID4 ()) [: 8])    def qty_getter ( Instance):        return instance.__dict__[storage_name]    def qty_setter (instance, value):        if value > 0:            Instance.__dict__[storage_name] = value        else:            raise ValueError (' value must is > 0 ')    Return Property (Qty _getter, Qty_setter) class LineItem:    weight = quantity () Price    = Quantity ()    def __init__ (self, Description, weight, price):        self.description = description        self.weight = weight        Self.price = Price    def subtotal (self):        return self.weight * self.priceraisins = LineItem (' Golden raisins ', ten, 6.95) print ( Raisins.weight, Raisins.description, Raisins.price)

  

Operation Result:

Ten Golden raisins 6.95

  

Now, let's compare the descriptor class and the attribute factory, both of which can do some extra work on setting values or reading the property, which is better? There are two main reasons for using descriptor classes:

    1. Descriptor classes can use subclass extensions, and if you want to reuse the code in a factory function, there's no other way to do it than to copy the paste.
    2. Maintaining State in class properties and instance properties is easier to understand than using function properties and closure hold states

We use descriptor class quantity to perform additional operations when accessing and setting the weight and price of LineItem managed instances, now let's go a step further and add a description descriptor instance, Some actions are also added when you want to set and access the Description property of a LineItem instance. Here, we are going to add a descriptor class Notblank, in the process of designing Notblank, we find that it is much like the quantity descriptor class, except that the validation logic is different

Recalling the functionality of quantity, we noticed that it did two different things, managed the storage properties in the managed instance, and validated the values used to set those two properties. Thus, we can do this by inheriting the descriptor class, where we create two base classes:

    • Autostorage: Descriptor class for automatic management of storage properties
    • Validated: Extends the abstract subclass of the Autostorage class, overriding the __set__ method, calling the Validate method that must be implemented by the subclass

Later we will rewrite the quantity class and implement the Notblank class so that it inherits the validated class, writing only the Validate method, and the relationship between the classes 1-2:

Figure 1-2

Figure 1-2: A hierarchy of several descriptor classes. The Autostorage base class is responsible for automatically storing attributes, validated classes for validation, delegating responsibilities to abstract methods Validate;quantity and nonblank are specific subclasses of validated. The relationship between Validated, quantity, and nonblank three classes embodies the template method design pattern.  

Import Abcimport uuidclass autostorage: # <1> def __init__ (self): cls = self.__class__ prefix = cl s.__name__ identity = str (UUID.UUID4 ()) [: 8] self.storage_name = ' _{}#{} '. Format (prefix, identity) def __g Et__ (self, instance, owner): if instance was None:return self Else:return getattr (ins  Tance, Self.storage_name) def __set__ (self, instance, value): SetAttr (instance, self.storage_name, value) class Validated (ABC.  ABC, Autostorage): # <2> def __set__ (self, instance, value): # <3> value = Self.validate (instance, Value) Super (). __set__ (instance, value) @abc. Abstractmethod def validate (self, instance, value): # <4&gt        ; "" "return validated value or raise ValueError" "" Class Quantity (Validated): "" A number greater than zero "" Def Vali    Date (self, instance, value): # <5> if Value <= 0:raise valueerror (' value must is > 0 ')    Return Valueclass Notblank (Validated): "" "a string with at least one non-space character" "Def Validate (self, I Nstance, value): # <6> value = Value.strip () If Len (value) = = 0:raise ValueError (' Value C Annot be empty or blank ') return value

    

    1. The Autostorage class provides most of the functionality of the previous Quantity descriptor class
    2. The validated class is an abstract class, but it also inherits the Autostorage class
    3. Overriding the __set__ method in the validated class, first by validating the method and then calling the parent class's __set__ method to store the value
    4. Abstract methods, specifically implemented by subclasses
    5. Quantity implements the Validate method of the parent class validated, the value of the checksum setting must be greater than 0
    6. Notblank implements the Validate method of the parent class validated, the value of the checksum setting cannot be an empty string

LineItem class using the quantity and nonblank descriptors

Class LineItem:    description = Notblank ()    weight = Quantity () Price    = Quantity ()    def __init__ (self, Description, weight, price):        self.description = description        self.weight = weight        Self.price =    Price def subtotal (self):        return self.weight * self.price

  

Testing the new LineItem class

Raisins = LineItem ("  , 10, 6.95)

  

Operation Result:

Traceback (most recent): ... Valueerror:value cannot be empty or blank

  

Python Property descriptor (i)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.