python dynamic properties and features

Posted by weiwei on Fri, 07 Jan 2022 13:13:39 +0100

Article catalog

learn from fluent python

1. Use dynamic attributes to transform data

  • In Python, attributes of data and methods of processing data are collectively referred to as attributes. In fact, methods are just callable properties
  • We can also create properties
from urllib.request import urlopen
import warnings
import os
import json

URL = ''
JSON = './osconfeed.json'

def load():
    if not os.path.exists(JSON):
        msg = 'downloading {} to {}'.format(URL, JSON)
        warnings.warn(msg)  # Send a reminder
        with urlopen(URL) as remote, open(JSON, 'wb') as local:
            # Use two context managers
            # Read and save remote files
    with open(JSON) as fp:
        return json.load(fp)

feed = load()
for key, value in sorted(feed['Schedule'].items()):
    print('{:3} {}'.format(len(value), key))
# This syntax is too long... How to improve
from collections import abc

class FrozenJSON:
    # A read-only interface that uses attribute notation to access JSON class objects
    def __init__(self, mapping):
        self.__data = dict(mapping)

    def __getattr__(self, name):
        if hasattr(self.__data, name):  # If there are properties, get
            return getattr(self.__data, name)
            # Calling methods such as keys is handled in this way
        else:  # No, build frozen JSON

    @classmethod  # As an alternative construction method, this is often used by the @ classmethod decorator
    def build(cls, obj):
        if isinstance(obj, abc.Mapping):
            return cls(obj)  # Building frozen JSON
        elif isinstance(obj, abc.MutableSequence):
            # Is a sequence that build s each element
            return [ for item in obj]
            return obj

raw_feed = load()
feed = FrozenJSON(raw_feed)
# ['conferences', 'events', 'speakers', 'venues']
# Why Schools Don't Use Open Source to Teach Programming
p =[-1]
# <class '__main__.FrozenJSON'>
# Why Schools Don't Use Open Source to Teach Programming
# [157509]
# KeyError: 'age'
  • Handle invalid attribute names, such as built-in keywords, keyword iskeyword
grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
# print(grad.class) # invalid syntax
print(getattr(grad, 'class')) # 1982

Modify the constructor of the class:

def __init__(self, mapping):
    self.__data = {}
    for k,v in mapping.items():
        if keyword.iskeyword(k): # If it is a keyword, add an underscore suffix
            k += "_"
        self.__data[k] = v
  • Invalid naming, str.isidentifier()
grad = FrozenJSON({'2name': 'Jim Bo', 'class': 1982})
print(grad.2name) # SyntaxError: invalid syntax


def __init__(self, mapping):
    self.__data = {}
    for k,v in mapping.items():
        if keyword.iskeyword(k):
            k += "_"
        if not k.isidentifier(): # It's not a legal name. Change it
            k = "_" + k
        self.__data[k] = v
print(grad._2name) # Jim Bo
  • __ init__ Method is actually "initialization method". The real construction method is__ new__ (but you hardly need to write it yourself)
# Pseudo code for building objects 
def object_maker(the_class, some_arg): 
	new_object = the_class.__new__(some_arg)  # The new method can also return other class objects
	if isinstance(new_object, the_class): 
		the_class.__init__(new_object, some_arg) 
	return new_object
class FrozenJSON:
    # A read-only interface that uses attribute notation to access JSON class objects
    def __new__(cls, arg):  # The first parameter is the class itself
        if isinstance(arg, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(arg, abc.MutableSequence):
            return [cls(item) for item in arg]
            return arg

    def __init__(self, mapping):
        self.__data = {}
        for k, v in mapping.items():
            if keyword.iskeyword(k):
                k += "_"
            if not k.isidentifier():
                k = "_" + k
            self.__data[k] = v

    def __getattr__(self, name):
        if hasattr(self.__data, name):  # If there are properties, get
            return getattr(self.__data, name)
            # Calling methods such as keys is handled in this way
        else:  # No, build frozen JSON
            return FrozenJSON(self.__data[name])

2. @property Please use @ property to add width and height attributes and a read-only attribute resolution to a Screen object

class Screen(object):
    def __init__(self):
        self._w = 0
        self._h = 0
        self._r = 786432
    @property # Turn the method into an attribute, and do not add () to the call
    def width(self):
        return self._w
    def height(self):
        return self._h

    @width.setter # You can set the property value. No method is a read-only property
    def width(self, v):
        self._w = v
    def height(self, v):
        self._h = v

    def resolution(self):
        return self._r
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
    print('Test passed!')
    print('Test failed!')
  • Features are class properties, but they manage the access of instance properties
class Class:
    data = "class data attr"
    def prop(self):
        return "prop value"

obj = Class()
print(vars(obj)) # {}, the vars function returns the value of obj__ dict__  attribute
print( # class data attr = "changed"
print(vars(obj)) # {'data': 'changed'}
print( # class data attr
# The instance modified data, but the class attribute was not modified

print(Class.prop) # <property object at 0x0000021A91E4A680>
print(obj.prop) # prop value
# obj.prop = "changed prop"  # Error can't set attribute
obj.__dict__["prop"] = "changed prop1"
print(vars(obj)) # {'data': 'changed', 'prop': 'changed prop1'}
print(obj.prop) # prop value #
# Read obj Prop will still run the read value method of the property. Attributes are not masked by instance properties
Class.prop = "haha" # Override class Prop properties, destroying property objects
print(obj.prop) # changed prop1
# Now, obj Prop gets instance properties.
# Class.prop is not a feature anymore, so obj will no longer be overwritten prop. 
print( # changed
print( # class data attr = property(lambda self : "data prop value")
# Override class with new features data
print( # data prop value
# is class Data characteristics cover up
del # Delete property
print( # changed
# Restore as is, obj Data gets the instance property data

obj. Expressions such as attr do not start looking for attr from obj, but from obj__ class__ At first, and only if there is no attribute named attr in the class, Python will look for it in the obj instance.

This rule applies not only to features, but also to an entire class of descriptors - overriding descriptor s

2.1 help() document

When you create a property object using the decorator, the document string of the value reading method (the method with the @ property decorator) as a whole becomes the document of the property

>>> class Foo:
	def bar(self):
		return self.__dict__['bar']
	def bar(self, val):
		self.__dict__['bar'] = val

>>> help(Foo)
Help on class Foo in module __main__:

class Foo(builtins.object)
 |  Data descriptors defined here:
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  bar
 |      Documentation

>>> help(
Help on property:

  • Classic writing method, pass in doc parameter
weight = property(get_weight, set_weight, doc='weight in kilograms')

3. Characteristic factory function

To reduce writing getter s and setter s, you can use feature factory functions

def quantity(storage_name):
    def qty_getter(instance):
        return instance.__dict__[storage_name]

    def qty_setter(instance, value):
        if value > 0:
            instance.__dict__[storage_name] = value
            raise ValueError("value must be > 0")

    return property(qty_getter, qty_setter)
class LineItem:
    weight = quantity('weight') # Use the attribute factory function to define the weight class attribute
    price = quantity('price') # price attribute

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight # Activate the property to ensure that it is not negative and 0
        self.price = price

    def subtotal(self):
        return self.weight * self.price # Use values stored in properties

line1 = LineItem("name1", 8, 13.5)
print(line1.weight, line1.price) # 8 13.5
# [('description', 'name1'), ('price', 13.5), ('weight', 8)]

The weight attribute overrides the weight instance attribute, so for self Weight or obj Each reference to weight is handled by the attribute function, with only direct access__ dict__ Property to skip the processing logic of the attribute

4. Delete attribute

del operation, deleting attributes is rare, but python supports this operation

class BlackKnight:
    def __init__(self):
        self.members = ['an arm',
                        'another arm',
                        'a leg',
                        'another leg']
        self.phrases = ["'Tis but a scratch.",
                        "It's just a flesh wound.",
                        "I'm invincible!",
                        "All right, we'll call it a draw."]
    def member(self):
        print("next member is:")
        return self.members[0]
    def member(self):
        text = 'BLACK KNIGHT (loses {})\n-- {}'
        print(text.format(self.members.pop(0), self.phrases.pop(0)))

knight = BlackKnight()
# next member is:
# an arm
del knight.member
# BLACK KNIGHT (loses an arm)
# -- 'Tis but a scratch.
del knight.member
# BLACK KNIGHT (loses another arm)
# -- It's just a flesh wound.
del knight.member
# BLACK KNIGHT (loses a leg)
# -- I'm invincible!
del knight.member
# BLACK KNIGHT (loses another leg)
# -- All right, we'll call it a draw.
del knight.member
# IndexError: pop from empty list
  • Classic writing, fdel parameter setting, delete function
member = property(member_getter, fdel=member_deleter)
  • If you do not use features, you can also implement low-level special features__ delattr__ Method handles deleting attributes

5. Important attributes and functions of processing attributes

  • __ class__ Reference to the class to which the object belongs (i.e. obj. _class_ has the same function as type(obj)) Some special methods of Python, such as__ getattr__, Look only in the class of the object, not in the instance
  • __ dict__ A mapping that stores the writable properties of an object or class. Yes__ dict__ Property, you can set new properties at any time If the class has__ slots__ Property, its instance may not have__ dict__ attribute
  • __slots__ Class can define this property to limit which properties an instance can have __ slots__ The value of an attribute is a tuple of strings indicating the allowed attributes If__ slots__ 'not in__ dict__', Then there is no instance of this class__ dict__ Property. An instance can only have a property with the specified name

5.1 built in functions for processing attributes

  • dir([object]) Most of the properties of the object are listed, and the dir function does not list several special properties of the class, such as__ mro__,__ bases__ And__ name__
>>> dir(Foo)
['__class__', '__delattr__', '__dict__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', 
'__getattribute__', '__gt__', '__hash__', '__init__',
 '__init_subclass__', '__le__', '__lt__', '__module__',
  '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
  '__repr__', '__setattr__', '__sizeof__', '__str__', 
  '__subclasshook__', '__weakref__', 'bar']
  • getattr(object, name[, default]) Get the attribute corresponding to the name string from the object object The property obtained may come from the class or superclass to which the object belongs If there is no specified attribute, the getattr function throws an AttributeError exception or returns the value of the default parameter (if this parameter is set)
  • hasattr(object, name), call the above function to see if it returns an exception
  • setattr(object, name, value) may create a new attribute or overwrite the existing attribute
  • vars([object]) returns the name of the object__ dict__ attribute If the class to which the instance belongs is defined__ slots__ Property, no instance__ dict__ Property, then the vars function cannot handle that instance

5.2 special methods for handling attributes

  • Using the dot or the built-in getattr, hasattr, and setattr functions to access properties will trigger the corresponding special methods in the following list
  • However, directly through the instance__ dict__ Property reading and writing properties do not trigger these special methods, and special methods are usually skipped in this way
  • Special methods are not masked by instance properties with the same name