1 动态属性

在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。

2 属性描述符(get/set/delete)

python 使用特性管理实例属性

(转)Python描述符(descriptor)解密

python理解描述符(descriptor)

python 描述符总结

Python描述符 (descriptor) 详解

在不可散列的类中使用描述符-python

python高级编程——描述符Descriptor详解(下篇)

2.1 基本概念

描述符是对多个属性运用相同存取逻辑的一种方式。例如,Django ORM 和 SQL Alchemy 等 ORM 中的字段类型是描述符,把数据库记录中字段里的数据与 Python 对象的属性对应起来。

为什么需要描述符:对property来说,最大的缺点就是它们不能重复使用。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。

描述符是property的升级版,允许你为重复的property逻辑编写单独的类来处理。

基本要求:描述符是实现了特定协议的类,这个协议包括 __get____set____delete__ 方法。property 类实现了完整的描述符协议。通常,可以只实现部分协议。其实,我们在真实的代码中见到的大多数描述符只实现了 __get____set__ 方法,还有很多只实现了其中的一个。

实现了 __get____set____delete__ 方法的类是描述符。

用法:描述符的用法是,创建一个描述符类,它的实例对象作为另一个类的属性。

为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get____set__方法。

大致流程

  1. 定义一个描述符类D,其内包含一个或多个__get__()__set__()__delete__()方法
  2. 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr=D()
  3. 之后访问、赋值、删除attr属性,将会自动触发描述符类中的__get__()__set__()__delete__()方法

实现

要定义描述符类很简单,只要某个类中包含了下面一个或多个方法,就算是满足描述符协议,就是描述符类,就可以作为属性操作的代理器。

class Descriptor():
    def __get__(self, instance, owner):...
    def __set__(self, instance, value):...
    def __delete__(self, instance):...

需要注意的是,__get__的返回值需要是属性值或抛异常,另外两个方法要返回None。

类属性描述符对象和实例属性同名时:描述符针对的是类属性,但是当一个类中,如果类属性是描述符对象,而实例属性由于这个描述符属性同名

class Person:
    character = CharacterDescriptor('乐观的')
    weight = WeightDescriptor(150)
    def __init__(self, character,weight):
        self.character = character
        self.weight = weight

p = Person('悲观的', 200)
print(p.character)  #属性的访问
print(p.weight)     #

从上面的运行结果可以看出,首先是访问了描述符的__set__方法,这是因为在构建对象的时候,相当于为character和weight赋值,然后再调用__get__方法,这是因为访问了类属性character和weight,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。

2.2 专有名词

描述符类: 实现了描述符协议的类,描述符类的一些协议(__get____set____delete__ )。

实现了__get____set____delete__ 方法的类是描述符,只要实现了其中一个就是。

托管类将描述符实例作为类属性的类,比如Fruits 类,他有 weight、price 两个类属性,且都被赋予了描述符类的实例。

描述符实例: 描述符类创建出描述符实例,通常来讲,描述符类的实例会被赋给托管类的类属性。

托管实例: 托管类创建出来的实例

托管属性: 托管类中由描述符实例处理的公开属性

存储属性: 可以粗略的理解为、托管实例的属性、在上例中使用 vars(apple) 得到的结果中 price 和 weight 实例属性就是存储属性,它们实际存储着*实例的*属性值

非数据描述符:一个类,如果只定义了__get__() 或者是__delete__()方法,而没有定义__set__()方法,则认为是非数据描述符(即没有定义__set__)

数据描述符:一个类,不仅定义了__get__() 方法,还定义__set__(), __delete__() 方法,则认为是数据描述符(即定义了__get__和__set__)

ps: 托管属性是类(Fruits)属性、存储属性是实例(apple)的属性。

img> Quantity 实例是描述符,因此有个放大镜,用于获取值(__get__),以及一个手抓,用于设置值(__set__)。

2.3 例子

2.3.1 要点

定义位置:为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get__和__set__方法。

独立实例:类使用了一个字典来单独保存专属于实例的数据。这个一般来说是没问题的,除非你用到了不可哈希(unhashable)的对象

不可哈希处理:list的子类是不可哈希的,因此它们不能为描述符类用做数据字典的key。有一些方法可以规避这个问题,但是都不完美。最好的方法可能就是给你的描述符加标签了。描述符可以安全的在这里存储数据。只是要记住,不要在别的地方也给这个描述符添加标签。这样的代码很脆弱也有很多微妙之处。但这个方法的确很普遍,可以用在不可哈希的所有者类上。

class Descriptor(object):
    def __init__(self, label):
        self.label = label

    def __get__(self, instance, owner):
        print('__get__', instance, owner)
        return instance.__dict__.get(self.label)

    def __set__(self, instance, value):
        print('__set__')
        instance.__dict__[self.label] = value

class Foo(list):
    x = Descriptor('x')
    y = Descriptor('y')

泄漏内存问题:WeakKeyDictionary可以保证描述符类不会泄漏内存:WeakKeyDictionary的特殊之处在于:如果运行期系统发现这种字典所持有的引用,是整个程序里面指向Exam实例的最后一份引用,那么,系统就会自动将该实例从字典的键中移除。

2.3.2 模拟ORM

代码-描述符类
#!/usr/bin/env Python
# -- coding: utf-8 --
import weakref
import numbers

class Field:
    pass

class IntField(Field):
    # 数据描述符
    def __init__(self, db_column, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.db_column = db_column
        if min_value is not None:
            if not isinstance(min_value, numbers.Integral):
                raise ValueError("min_value must be int")
            elif min_value < 0:
                raise ValueError("min_value must be positive int")
        if max_value is not None:
            if not isinstance(max_value, numbers.Integral):
                raise ValueError("max_value must be int")
            elif max_value < 0:
                raise ValueError("max_value must be positive int")
        if min_value is not None and max_value is not None:
            if min_value > max_value:
                raise ValueError("min_value must be smaller than max_value")
        self._value = weakref.WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self._value.get(instance, 0)

    def __set__(self, instance, value):
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        if value < self.min_value or value > self.max_value:
            raise ValueError("value must between min_value and max_value")
        self._value[instance] = value


class CharField(Field):
    def __init__(self, db_column, max_length=None):
        # self._value = None
        self.db_column = db_column
        if max_length is None:
            raise ValueError("you must spcify max_lenth for charfiled")
        self.max_length = max_length
        self._value = weakref.WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self._value.get(instance, '0')

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("string value need")
        if len(value) > self.max_length:
            raise ValueError("value len excess len of max_length")
        self._value[instance] = value
代码-元类
class ModelMetaClass(type):
    def __new__(cls, name, bases, attrs, **kwargs):
        if name == "BaseModel":
            return super().__new__(cls, name, bases, attrs, **kwargs)
        fields = {}
        for key, value in attrs.items():
            if isinstance(value, Field):
                fields[key] = value
        attrs_meta = attrs.get("Meta", None)
        _meta = {}
        db_table = name.lower()
        if attrs_meta is not None:
            table = getattr(attrs_meta, "db_table", None)
            if table is not None:
                db_table = table
        _meta["db_table"] = db_table
        attrs["_meta"] = _meta
        attrs["fields"] = fields
        del attrs["Meta"]
        return super().__new__(cls, name, bases, attrs, **kwargs)


class BaseModel(metaclass=ModelMetaClass):
    def __init__(self, *args, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)
        return super(BaseModel, self).__init__()

    def save(self):
        fields = []
        values = []
        for key, value in self.fields.items():
            db_column = value.db_column
            if db_column is None:
                db_column = key.lower()
            fields.append(db_column)
            value = getattr(self, key)
            values.append(str(value))

        sql = "insert {db_table}({fields}) value({values})".format(db_table=self._meta["db_table"],
                                                                   fields=",".join(fields), values=",".join(values))
        print(sql)
代码-测试
class User(BaseModel):
    name = CharField(db_column="name", max_length=10)
    age = IntField(db_column="age", min_value=1, max_value=100)
    class Meta:
        db_table = "user"

if __name__ == '__main__':
    first_user = User(name="bobby", age=28)
    first_user.name = "bobby"
    first_user.age = 28
    second_user = User(name="bobby", age=23)
    print(first_user.name is second_user.name)
    second_user.name = 'okay'
    print(first_user.name is second_user.name)

    second_user.name = 'sec_boddy'
    print(first_user.name)
    print(second_user.name)
    print(first_user.name is second_user.name)
输出
True
False
bobby
sec_boddy
False

2.3.3 成绩管理

代码
#!/usr/bin/env Python
# -- coding: utf-8 --
import weakref

class Grade:
    def __init__(self):
        self._values = weakref.WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value

class Exam:
    # https://lingxiankong.github.io/2014-03-28-python-descriptor.html
    # 为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get__和__set__方法。
    # 确保实例的数据只属于实例本身
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

if __name__ == '__main__':
    first_exam = Exam()
    first_exam.writing_grade = 82
    second_exam = Exam()
    second_exam.writing_grade = 75
    logger.info(f'first {first_exam.writing_grade}')
    logger.info(f'second {second_exam.writing_grade}')
输出
2020-06-25 20:18:23 lazy_db INFO:  first 82
2020-06-25 20:18:23 lazy_db INFO:  second 75

2.3.4 成绩管理2.0

代码
class Grade:
    def __init__(self, name):
        self.name = name
        self.internal_name = '_' + self.name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        setattr(instance, self.internal_name, value)

class Exam:
    # https://lingxiankong.github.io/2014-03-28-python-descriptor.html
    # 为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get__和__set__方法。
    # 确保实例的数据只属于实例本身
    math_grade = Grade('math_grade')
    writing_grade = Grade('writing_grade')
    science_grade = Grade('science_grade')

if __name__ == '__main__':
    first_exam = Exam()
    first_exam.writing_grade = 82
    second_exam = Exam()
    second_exam.writing_grade = 75
    logger.info(f'first {first_exam.writing_grade}')
    logger.info(f'second {second_exam.writing_grade}')
输出
2020-06-25 20:18:23 lazy_db INFO:  first 82
2020-06-25 20:18:23 lazy_db INFO:  second 75

2.3.5 成绩管理2.1

代码
class Grade:
    def __init__(self):
        self.name = None
        self.internal_name = None

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        setattr(instance, self.internal_name, value)

class Meta(type):
    def __new__(cls, name, bases, class_dict):
        for key, value in class_dict.items():
            if isinstance(value, Grade):
                value.name = key
                value.internal_name = '_' + key
        cls = type.__new__(cls, name, bases, class_dict)
        return cls

class Exam(object,metaclass=Meta):
    # https://lingxiankong.github.io/2014-03-28-python-descriptor.html
    # 为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用__get__和__set__方法。
    # 确保实例的数据只属于实例本身
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

    def __init__(self, writing_grade):
        self.writing_grade = writing_grade

if __name__ == '__main__':
    first_exam = Exam(85)
    first_exam.writing_grade = 82
    second_exam = Exam(13)
    second_exam.writing_grade = 75
    logger.info(f'first {first_exam.writing_grade}')
    logger.info(f'second {second_exam.writing_grade}')

2.4 进阶

2.4.1 添加回调

描述符仅仅是类,也许你想要为它们增加一些方法。举个例子,描述符是一个用来回调property的很好的手段。比如我们想要一个类的某个部分的状态发生变化时就立刻通知我们。下面的大部分代码是用来做这个的:

class CallbackProperty(object):
    """A property that will alert observers when upon updates"""
    def __init__(self, default=None):
        self.data = WeakKeyDictionary()
        self.default = default
        self.callbacks = WeakKeyDictionary()

    def __get__(self, instance, owner):
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):        
        for callback in self.callbacks.get(instance, []):
            # alert callback function of new value
            callback(value)
        self.data[instance] = value

    def add_callback(self, instance, callback):
        """Add a new function to call everytime the descriptor updates"""
        #but how do we get here?!?!
        if instance not in self.callbacks:
            self.callbacks[instance] = []
        self.callbacks[instance].append(callback)

class BankAccount(object):
    balance = CallbackProperty(0)

def low_balance_warning(value):
    if value < 100:
        print "You are poor"

ba = BankAccount()

# will not work -- try it
#ba.balance.add_callback(ba, low_balance_warning)

这是一个很有吸引力的模式——我们可以自定义回调函数用来响应一个类中的状态变化,而且完全无需修改这个类的代码。这样做可真是替人分忧解难呀。现在,我们所要做的就是调用ba.balance.add_callback(ba, low_balance_warning),以使得每次balance变化时low_balance_warning都会被调用。

但是我们是如何做到的呢?当我们试图访问它们时,描述符总是会调用__get__。就好像addcallback方法是无法触及的一样!其实关键在于利用了一种特殊的情况,即,当从类的层次访问时,`_get`方法的第一个参数是None。

class CallbackProperty(object):
    """A property that will alert observers when upon updates"""
    def __init__(self, default=None):
        self.data = WeakKeyDictionary()
        self.default = default
        self.callbacks = WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self        
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):
        for callback in self.callbacks.get(instance, []):
            # alert callback function of new value
            callback(value)
        self.data[instance] = value

    def add_callback(self, instance, callback):
        """Add a new function to call everytime the descriptor within instance updates"""
        if instance not in self.callbacks:
            self.callbacks[instance] = []
        self.callbacks[instance].append(callback)

class BankAccount(object):
    balance = CallbackProperty(0)

def low_balance_warning(value):
    if value < 100:
        print "You are now poor"

ba = BankAccount()
BankAccount.balance.add_callback(ba, low_balance_warning)

ba.balance = 5000
print "Balance is %s" % ba.balance
ba.balance = 99
print "Balance is %s" % ba.balance
Balance is 5000
You are now poor
Balance is 99

2.4.2 实现底层 @classmethod


class NewDefine_classmethod:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """
    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        #对传进函数进行加工,最后返回该函数
        def wrapper(*args, **kwargs):   #使用不定参数是为了匹配需要修饰的函数参数
            print("给函数添加额外功能")
            self.function(owner, *args, **kwargs)
        return wrapper

class Person:
    name='我有姓名'
    def __init__(self):
        pass

    @NewDefine_classmethod
    def study_1(cls):
        print(f'我的名字是:{cls.name},我会搞学习!')

    @NewDefine_classmethod
    def study_2(cls,score):
        print(f'我的名字是:{cls.name},我会搞学习!,而且这次考试考了 {score} 分')

print(Person.study_1())
print(Person.study_2(99))

可以分这样几步分析:

第一步:@NewDefine_classmethod本质上是一个“类装饰器”,从它的定义可知,它的定义为

class NewDefine_classmethod(function).我们发现,python系统定义的@classmethod其实它的定义也是一样的,如下,

class classmethod(function) .怎么样?它们二者的定义是不是一样?

第二步:NewDefineclassmethod本质上又是一个描述符,因为在它的内部实现了\_get__协议,由此可见,NewDefine_classmethod是“集装饰器-描述符”于一身的。

第三步:运行过程分析,因为study_1=NewDefine_classmethod(study_1),所以,study_1本质上是一个NewDefine_classmethod的对象,又因为NewDefine_classmethod本质上是实现了描述符的,所以,study_1本质上是一个定义在类中的描述符属性。

第四步:因为study1本质上是一个定义在类中的描述符属性。所以在执行Person.study_1的时候,相当于是访问类的描述符属性,所以会进入到描述符的\_get__方法。

现在是不是觉得原来python描述符还有这样神奇的使用呢?

注意:如果修饰的函数本身是具有返回值的,在__get__里面所定义的wrapper里面一定要返回,即return self.function(owner, args, *kwargs)。

还有一个地方需要注意的是,因为这是自定义的底层实现,所以一些集成IDE可能会显示有语法错误,但是这没有关系,这正是python灵活多变的地方,运行并不会出现错误。

2.4.3 实现底层 @staticmethod

staticmethod方法与classmethod方法的区别在于classmethod方法在使用需要传进一个类的引用作为参数。而staticmethod则不用。

class NewDefine_staticmethod:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """
    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        #对传进函数进行加工,最后返回该函数
        def wrapper(*args, **kwargs):   #使用不定参数是为了匹配需要修饰的函数参数
            print("给函数添加额外功能")
            self.function(*args, **kwargs)
        return wrapper

class Person:
    name='我有姓名'
    def __init__(self):
        pass

    @NewDefine_staticmethod
    def study_1(math,english):
        print(f'我数学考了 {math} 分,英语考了 {english} 分,我会搞学习!')

    @NewDefine_staticmethod
    def study_2(history,science):
        print(f'我历史考了 {history} 分,科学考了 {science} 分,我会搞学习!')

print(Person.study_1(99,98))
print(Person.study_2(88,89))

类方法classmethod必须第一个参数是cls,这个实际上就是判断所属的那个类,因此在__get__里面的function在调用的时候,第一个参数需要传递为owner,因为所属的“类cls等价于Person等价于owner”,但是因为静态方法不需要任何参数cls或者是self都不需要,因此在__get__实现的时候不能再传递owner参数,否则会显示参数错误。

2.4.4 实现底层 @property

class NewDefine_property:
    """
    使用“描述符”和“装饰器”结合起来,模拟@classmethod
    """
    def __init__(self, function):
        self.function = function

    def __get__(self, instance, owner):
        print("给函数添加额外功能")
        return self.function(instance)

class Person:
    name='我有姓名'
    def __init__(self):
        self.__study=100

    @NewDefine_property
    def study_1(self):  #使用property装饰的函数一般不要用“参数”,因为它的主要功能是对属性的封装
        return self.__study

p=Person()
print(p.study_1)

基本思想和前面分析的还是一样的,但是有几个地方有所区别,需要注意:

第一:@property的目的是封装一个方法,是这个方法可以被当做属性访问

第二:调用的方式与前面有所不同,__get__里面不能再定义wrapper了,否则不会调用wrapper。得不到想要的结果,为什么呢?

因为调用的方式不一样,根据前面的分析,study_1的本质是描述符属性,但是前面的调用均是使用的

Person.study_1()或者是p.study_1()的形式,还是当成方法去使用的。但是此处不一样了,直接就是当成属性去使用,

p.study1 ,不再是方法调用,因此wrapper函数得不到调用。所以\_get__方法得到了进一步简化。

3 按需生成属性

Python 魔法方法(三) __getattr__,__setattr__, __delattr__

Python高级用法之动态属性

使用__getattr__、__setattr__和__getattribute__来动态生成属性

Python 语言提供了一些挂钩,使得开发者很容易就能编写出通用的代码,以便将多个系统黏合起来。例如,我们要把数据库的行(row)表示为 Python 对象。由于数据库有自己的一套结构(schema),也称架构、模式、纲要、概要、大纲,所以在操作与行相对应的对象时,我们必须知道这个数据库的结构。然而,把 Python 对象与数据库相连接的这些代码,却不需要知道行的结构,所以,这部分代码应该写得通用一些。

那么,如何实现这种通用的代码呢?普通的实例属性、@property 方法和描述符,都不能完成此功能,因为它们都必须预先定义好,而像这样的动态行为,则可以通过 Python 的__getattr__特殊方法来做。如果某个类定义了__getattr__,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。

3.1 实例属性查找

python属性查找(attribute lookup)

首先需要明白的是实例属性查找的过程:

如果obj是某个类的实例,那么obj.name(以及等价的getattr(obj,'name'))首先调用__getattribute__。如果类定义了__getattr__方法,那么在__getattribute__抛出 AttributeError 的时候就会调用到__getattr__,而对于描述符(__get__)的调用,则是发生在__getattribute__内部的。官网文档是这么描述的

The implementation works through a precedence chain that gives data descriptors priority over instance variables, instance variables priority over non-data descriptors, and assigns lowest priority to __getattr__() if provided.

obj = Clz(), 那么obj.attr 顺序如下:

(1)如果“attr”是出现在Clz或其基类的__dict__中, 且attr是data descriptor, 那么调用其__get__方法, 否则

(2)如果“attr”出现在obj的__dict__中, 那么直接返回 obj.__dict__['attr'], 否则

(3)如果“attr”出现在Clz或其基类的__dict__中

(3.1)如果attr是non-data descriptor,那么调用其__get__方法, 否则

(3.2)返回 __dict__['attr']

(4)如果Clz有__getattr__方法,调用__getattr__方法,否则

(5)抛出AttributeError

程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发 __getattribute__ 方法。这样就可以在程序每次访问属性时,检查全局事务状态。

按照 Python 处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在 __getattr__ 和 __getattribute__ 里面抛出 AttributeError 异常。

实现通用的功能时,我们经常会在 Python 代码里使用内置的 hasattr 函数来判断对象是否已经拥有了相关的属性,并用内置的 __getattr__ 函数来获取属性值。这些函数会先在实例字典中搜索待查询的属性,然后再调用 __getattr__。

3.2 四个魔法函数

访问时机

如果某个类定义了__getattr__ ,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。

程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发__getattribute__方法。这样就可以在程序每次访问属性

按照Python处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在__getattr__ 和__getattribute__ 里面抛出AttributeError异常。

只要对实例的属性赋值,无论是直接赋值,还是通过内置的setattr函数赋值,都会触发__setattr__方法 。

3.2.1 __getattr__

当我们访问一个不存在的属性的时候,会抛出异常,提示我们不存在这个属性。而这个异常就是__getattr__方法抛出的,其原因在于他是访问一个不存在的属性的最后落脚点,作为异常抛出的地方提示出错再适合不过了。

看例子,我们找一个存在的属性和不存在的属性。

class A(object):
    def __init__(self, value):
        self.value = value

    def __getattr__(self, item):
        print "into __getattr__"
        return  "can not find"

a = A(10)
print a.value
# 10
print a.name
# into __getattr__
# can not find

可以看出,访问存在的属性时,会正常返回值,若该值不存在,则会进入最后的兜底函数__getattr__。

3.2.2 __setattr__

在对一个属性设置值的时候,会调用到这个函数,每个设置值的方式都会进入这个方法。

class A(object):
    def __init__(self, value):
        print "into __init__"
        self.value = value

    def __setattr__(self, name, value):
        print "into __setattr__"
        if value == 10:
            print "from __init__"
        object.__setattr__(self, name, value)


a = A(10)
# into __init__
# into __setattr__
# from __init__
print a.value
# 10
a.value = 100
# into __setattr__
print a.value
# 100

在实例化的时候,会进行初始化,在__init__里,对value的属性值进行了设置,这时候会调用__setattr__方法。

在对a.value重新设置值100的时候,会再次进入__setattr__方法。

需要注意的地方是,在重写__setattr__方法的时候千万不要重复调用造成死循环。

class A(object):
    def __init__(self, value):
        self.value = value

    def __setattr__(self, name, value):
        self.name = value

这是个死循环。当我们实例化这个类的时候,会进入__init__,然后对value进行设置值,设置值会进入__setattr__方法,而__setattr__方法里面又有一个self.name=value设置值的操作,会再次调用自身__setattr__,造成死循环。

除了上面调用object类的__setattr__避开死循环,还可以如下重写__setattr__避开循环。

class A(object):
    def __init__(self, value):
        self.value = value

    def __setattr__(self, name, value):
        self.__dict__[name] = value


a = A(10)
print a.value
# 10

3.2.3 __delattr__

__delattr__是个删除属性的方法

class A(object):
    def __init__(self, value):
        self.value = value

    def __delattr__(self, item):
        object.__delattr__(self, item)

    def __getattr__(self, item):
        return "when can not find attribute into __getattr__"



a = A(10)
print a.value
# 10
del a.value
print a.value
# when can not find attribute into __getattr__

__delattr__也要避免死循环的问题,就如__setattr__一样,在重写__delattr__,避免重复调用。

3.2.4 __getattribute__

使用__getattribute__对属性的访问做额外处理

假设我们需要在数据库中实现事物(transaction)处理,即每次在访问属性时,需要额外调用特殊方法检查数据库中对应的行是否有效,以及相关的事务是否依然开放。此时使用__getattr__无法实现这种功能,因为第二次访问属性时,Python会直接返回上首次调用时存储在__dict__中的属性值,而不会再次调用__getattr__插寻属性的状态。此种情况下我们需要使用__getattribute__,该方法在用户每次访问属性是都会被调用。

class LazyDB(object):
    def __init__(self):
        self.exist = 1

    def __getattribute__(self, item):
        print('__getattribute__ (%s) called' % item)
        try:
            return super().__getattribute__(item)
        except AttributeError:
            value = ' '.join(['default value: ', item])
            setattr(self, item, value)
            return value

data = LazyDB()
print(data.foo) ##每次访问类属性时都会被调用,此处是第1次调用
print(data.foo) ##每次访问类属性时都会被调用,此处是第2次调用
print(data.__dict__)  ##每次访问类属性时都会被调用,此处是第3次待用

###输出如下:
__getattribute__ (foo) called
default value:  foo
__getattribute__ (foo) called
default value:  foo
__getattribute__ (__dict__) called
{'exist': 1, 'foo': 'default value:  foo'}

3.3 要点

  • __getattr__ 和 __setatr__,我们可以用惰性的方式来加载并保存对象的属性。
  • 要理解 __getattr__ 与 __getattribute__ 的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性时触发。
  • 如果要在 __getattribute__ 和 __setattr__ 方法中访问实例属性,那么应该直接通过super()(也就是object类的同名方法)来做,以避免无限递归。

总结:

(1)对于类装饰器属性,只要出现属性访问(不管是通过对象访问还是类名访问),都会优先调用装饰器的__get__方法;

(2)对于类装饰器属性,若出现属性修改(不管是通过对象访问还是类名访问),都会优先调用装饰器的__set__方法;

(3)对于类装饰器属性,若出现属性删除(不管是通过对象访问还是类名访问),都会优先调用装饰器的__delete__方法

3.4 例子

4 元类

第33/34条用元类核实和登记子类,3334,验证,注册

4.1 基本概念

类元编程是指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,而无需使用 class 关键字。类装饰器也是函数,不过能够审查、修改,甚至把被装饰的类替换成其他类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类。

4.1.1 元类的定义

Python定义元类时,需要从 type类中继承,然后重写 __new__方法,便可以实现意想不到的功能。

class Meta(type):
    def __new__(meta,name,bases,class_dict):
        #...各种逻辑实现1
        cls = type.__new__(meta,name,bases,class_dict)
        print('当前类名',name)
        print('父类',bases)
        print('全部类属性',class_dict)
        #...各种逻辑实现2
        return cls

class MyClass(object,metaclass=Meta):
    stuff = 33
    def foo(self):
        pass
当前类名 MyClass
父类 (<class 'object'>,)
全部类属性 {'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 33, 'foo': <function MyClass.foo at 0x0000019E028315E8>}

元类可以获知那个类的名称、其所继承的父类,以及定义在class语句体中的全部类属性

4.1.2 元类的本质

在Python当中万物皆对象,我们用 class关键字定义的类本身也是一个对象, 负责产生该对象的类称之为元类 ,元类可以简称为类的类, 元类的主要目的是为了控制类的创建行为

  • type是Python的一个内建元类,用来直接控制生成类,在Python中任何 class定义的类其实都是 type类实例化的结果。
  • 只有继承了 type类才能称之为一个元类,否则就是一个普通的自定义类,自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程。

4.1.3 小结

元类的各种操作可以实现类的验证和注册逻辑,均可以在元类的 __new__方法中实现,主要原因是当子类对象构建时,会先调用元类的 __new__方法,产生一个空对象,然后再调用子类的 __init__方法给对象属性进行赋值。

4.2 验证子类

元类是python比较高级的用法,简而言之,元类就是创建类的类。

而type就是一个元类,是用来创建类对象的类。

因此,要定义元类就要使其继承type类。

通常情况下,开发者在使用OOP的方式编程时,往往会用到__init__方法,即构造函数。

该方法会在类初始化时运行。但是我们可以将验证的时机提前,以至于提前到类创建之时,因此就会用到__new__方法。

class Base(type):
    def __new__(cls, name, param, dicts):
        print(cls)
        print(name)
        print(param)
        print(dicts)
        return super().__new__(cls, name, param, dicts)

class Meta(metaclass=Base):
    name = 'yang'

    def person(self):
        pass
Meta()

<class '__main__.Base'>
Meta
()
{'__module__': '__main__', '__qualname__': 'Meta', 'name': 'yang', 'person': <function Meta.person at 0x10c6492f0>}

元类中所编写的验证逻辑,针对的是该基类的子类,而不是基类本身

__new__()方法接收到的参数依次是:

  1. 当前准备创建的类的对象;
  2. 类的名字;
  3. 类继承的父类集合;
  4. 类的方法集合。

案例1 :编写一个多边形类,当边数小于 3时,其类报错,实现其验证逻辑。

class ValidatePolygon(type):
    ## __new__当中放入验证逻辑
    def __new__(meta,name,bases,class_dict):
        if bases!=(object,): ##针对子类而不对基类
            if class_dict['sides'] < 3:
                raise ValueError('Polygons need 3+ sides')
        return type.__new__(meta,name,bases,class_dict)

class Polygons(object,metaclass=ValidatePolygon):
    sides = None

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

class Triangle(Polygons):
    sides = 3

### 类设计报错。
class Line(Polygons):
    sides = 1

4.3 注册子类

元类还有一个用途,就是在程序中 自动注册类型。开发者每次从基类中继承子类时,基类的元类都可以自动运行注册代码。

案例2 :实现对象的序列化与反序列化

###建立类名与该类对象的映射关系,维护registry字典。
registry = {}
def register_class(target_class):
    registry[target_class.__name__] = target_class

def deserialize(data):
    params = json.loads(data)
    name = params['class']
    target_class = registry[name]
    return target_class

class Meta(type):
    def __new__(meta,name,bases,class_dict):
        cls = type.__new__(meta,name,bases,class_dict)
        register_class(cls) ##注册子类
        return cls

class BetterSerializable(object):
    def __init__(self,*args):
        self.args = args

    def serialize(self):
        return json.dumps({'class':self.__class__.__name__,
                            'args':self.args,})
    def __repr__(self):
        pass

class RegisterSerializabel(BetterSerializable,metaclass=Meta):
    pass

通过元类来实现类的注册,可以确保所有的子类都不会遗漏,从而避免后续的错误。

4.4 获取__init__的默认参数

获取__init__的默认参数,并在classmethod方法中为没有给定的属性赋默认值,提升代码的健壮性

元类定义:

#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v0.1
@author: narutohyc
@file: meta_interface.py
@Description: 
@time: 2020/6/15 20:29
"""
import collections
from abc import (ABC,
                 abstractmethod,
                 ABCMeta)
import inspect

class DicMetaClass(ABCMeta):
    def __new__(cls, name, bases, attrs, **kwargs):
        if name == 'DicMeta':
            return super().__new__(cls, name, bases, attrs, **kwargs)
        # 获取__init__函数的 默认值
        argspec = inspect.getfullargspec(attrs["__init__"])
        init_defaults = dict(zip(argspec.args[-len(argspec.defaults):], argspec.defaults))
        cls.__init_defaults = init_defaults
        attrs['__init_defaults__'] = init_defaults
        return super().__new__(cls, name, bases, attrs, **kwargs)

抽象父类:

#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v0.1
@author: narutohyc
@file: meta_interface.py
@Description: 
@time: 2020/6/15 20:29
"""

from abc import (ABC,
                 abstractmethod,
                 ABCMeta)

class DicMeta(ABC, metaclass=DicMetaClass):
    def __init__(self):
        pass

    @abstractmethod
    def to_dict(self):
        '''
        返回字典
        '''
        pass

    @classmethod
    def load_from_mapping(cls, mapping_datas):
        '''
        用字典来构建实例对象
        '''
        assert isinstance(mapping_datas, collections.abc.Mapping)
        obj = cls.__new__(cls)
        [setattr(obj, k, v) for k, v in mapping_datas.items()]
        return obj

子类实现:

#!/usr/bin/env Python
# -- coding: utf-8 --

"""
@version: v0.1
@author: narutohyc
@file: text_meta.py
@Description: 
@time: 2020/5/22 14:55
"""
from augmentation.meta_class.meta_interface import DicMeta
from utils.utils_func import gen_md5, str2bool
import re

class TaskMeta(DicMeta):
    '''
    数据包装类的bean结构
    '''
    def __init__(self, text, doc_id, sentence_id, reg_lst,
                 has_reg=True,
                 flag=None,
                 dataset='train',
                 text_source="primitive"):
        super(TaskMeta, self).__init__()
        self.text = text
        self.doc_id = doc_id
        self.sentence_id = sentence_id
        if reg_lst and isinstance(reg_lst[0], list):
            reg_lst = ['%s %s %s' % (tag, start_idx, value) for tag, start_idx, value in reg_lst]
        self.reg_lst = sorted(reg_lst, key=lambda reg: int(re.sub(' +', ' ', reg).split(" ", 2)[1])) if reg_lst else []
        self.flag = list(set(i.split(' ', 2)[0] for i in self.reg_lst)) if flag is None else flag
        self.has_reg = str2bool(has_reg)
        self.dataset = dataset
        self.text_source = text_source
        self._id = gen_md5(self.text)

    @classmethod
    def load_from_mapping(cls, mapping_datas):
        '''
        用字典来构建 TaskMeta实例
        '''
        obj = super(TaskMeta, cls).load_from_mapping(mapping_datas)
        obj._id = gen_md5(obj.text)
        [setattr(obj, k, v) for k, v in obj.__init_defaults__.items() if not hasattr(obj, k)]
        if obj.flag is None:
            obj.flag = list(set(i.split(' ', 2)[0] for i in obj.reg_lst))
        obj.has_reg = str2bool(obj.has_reg)
        return obj

    @property
    def to_dict(self):
        '''
        当该类没有其他多余属性时  可以直接返回self.__dict__的副本
        '''
        return {"text": self.text,
                "doc_id": self.doc_id,
                "sentence_id": self.sentence_id,
                "reg_lst": self.reg_lst,
                "flag": list(self.flag),
                "has_reg": self.has_reg,
                "dataset": self.dataset,
                "text_source": self.text_source,
                "_id": self._id}

测试类:

task_meta_0 = TaskMeta.load_from_mapping({'text': '斯坦福大学开发的基于条件随机场的命名实体识别系统,该系统参数是基于CoNLL、MUC-6、MUC-7和ACE命名实体语料训练出来的。', 
                                          'doc_id': 'id1', 'sentence_id': 'id1', 
                                          'reg_lst': ['学校 0 斯坦福大学', '标注 33 CoNLL', '标注 39 MUC-6', '标注 45 MUC-7', '标注 51 ACE']})
task_meta_1 = TaskMeta.load_from_mapping({'text': '斯坦福大学开发的基于条件随机场的命名实体识别系统,该系统参数是基于CoNLL、MUC-6、MUC-7和ACE命名实体语料训练出来的。', 
                                          'doc_id': 'id1', 'sentence_id': 'id1', 
                                          'reg_lst': ['学校 0 斯坦福大学', '标注 33 CoNLL', '标注 39 MUC-6', '标注 45 MUC-7', '标注 51 ACE'], 
                                          'flag': ['学校', '标注'], 'has_reg': True, 
                                          'dataset': 'train', 'text_source': 'primitive', '_id': '3b895befc659345be8686bd7de4d7693'})
task_meta_0.to_dict == task_meta_1.to_dict
Out[33]: True

可以看出,taskmeta_0和task_meta_1两者的 值是完全相同的,这里就可以做到共享\_init__默认参数的效果

4.5 注解类的属性

元类还有一个更有用处的功能,那就是可以在某个类刚定义好但是尚未使用的时候,提前修改或注解该类的属性。这种写法通常会与描述符(descriptor) 搭配起来(参见本书第31条),令这些属性可以更加详细地了解自己在外围类中的使用方式。 例如,要定义新的类,用来表示客户数据库里的某- -行。同时,我们还希望在该类的相关属性与数据库表的每一列之间, 建立对应关系。于是,用下面这个描述符类,把属性与列名联系起来。

class Field:
       def __init__(self, name):
           self.name = name
           self.internal_name = '_' + self.name

       def __get__(self, instance, owner):
           if instance is None:
               return self
           return getattr(instance, self.internal_name, '')

       def __set__(self, instance, value):
           setattr(instance, self.internal_name, value)

由于列的名称已经保存到了Field描述符中,所以我们可以通过内置的setattr和getattr函数,把每个实例的所有状态都作为protected字段,存放在该实例的字典里面。 在本书前面的例子中,为了避免内存泄漏,我们曾经用weakref字典来构建描述符,而刚才的那段代码,目前看来,似乎要比weakref方案便捷得多。 接下来定义表示数据行的Customer类,定义该类的时候,我们要为每个类属性指定对应的列名。

class Customer:
       first_name = Field('first_name')
       last_name = Field('last_name')
       prefix = Field('prefix')
       suffix = Field('suffix')

问题在于,上面这种写法显得有些重复。在Customer类的class语句体中,我们既然要将构建好的Field对象赋给Customer.first name, 那为什么还要把这个字段名(本例中是'first name') 再传给Field的构造器呢? 之所以还要把字段名传给Field构造器,是因为定义Customer类的时候,Python 会以从右向左的顺序解读赋值语句,这与从左至右的阅读顺序恰好相反。首先,Python 会以Field(first name') 的形式来调用Field 构造器,然后,它把调用构造器所得的返回值,赋给Customer.field name。 从这个顺序来看,Field 对象没有办法提前知道自己会赋给Customer类里的哪一个属性。 为了消除这种重复代码,我们现在用元类来改写它。使用元类,就相当于直接在class语句上面放置挂钩,只要class语句体处理完毕,这个挂钩就会立刻触发。于是,我们可以借助元类,为Field描述符自动设置其Field.name和Field.internal_ name, 而不用再像刚才那样,把列的名称手工传给Field 构造器。

class Meta(type):
       def __new__(meta, name, bases, class_dict):
           for key, value in class_dict.items():
               if isinstance(value, Field):
                   value.name = key
                   value.internal_name = '_' + key
           cls = type.__new__(meta, name, bases, class_dict)
           return cls

下面定义一一个基类,该基类使用刚才定义好的Meta作为其元类。凡是代表数据库里面某一行的类,都应该从这个基类中继承,以确保它们能够利用元类所提供的功能:

class DatabaseRow(object, metaclass=Meta):
    pass

采用元类来实现这套方案时,Field 描述符类基本上是无需修改的。唯一 要调整的地方就在于:现在不需要再给构造器传人参数了,因为刚才编写的Meta.__new__ 方法会自动把相关的属性设置好。

class Field:
       def __init__(self):
           self.name = None
           self.internal_name = None

有了元类、新的DatabaseRow基类以及新的Field描述符之后,我们在为数据行定义DatabaseRow子类时,就不用再像原来那样,编写重复的代码了。

class BetterCustomer(DatabaseRow):
       first_name = Field()
       last_name = Field()
       prefix = Field()
       suffix = Field()

4.5.1 ORM例子

ORM 是 python编程语言后端web框架 Django的核心思想,“Object Relational Mapping”,即对象-关系映射,简称ORM。 一个句话理解就是:创建一个实例对象,用创建它的类名当做数据表名,用创建它的类属性对应数据表的字段,当对这个实例对象操作时,能够对应MySQL语句

代码
class ModelMetaclass(type):
    def __new__(cls, name, bases, attrs):
        mappings = dict()
        for k, v in attrs.items():
            if isinstance(v, tuple):
                print('Found mapping :%s ==> %s' % (k, v))
                mappings[k] = v
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings
        attrs['__table__'] = name
        return type.__new__(cls, name, bases, attrs)


class Model(object, metaclass=ModelMetaclass):
    def __init__(self, **kwargs):
        for name, value in kwargs.items():
            setattr(self, name, value)

    def save(self):
        fields, args = [], []
        for k, v in self.__mappings__.items():
            fields.append(v[0])
            args.append(getattr(self, k, None))
        args_temp = list()
        for temp in args:
            if isinstance(temp, int):
                args_temp.append(str(temp))
            elif isinstance(temp, str):
                args_temp.append("""'%s'""" % temp)

        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(args_temp))
        print(sql)

class User(Model):
    uid = ('uid', 'int unsigned')
    name = ('username', 'varchar(30)')
    email = ('email', 'varchar(30)')
    password = ('password', 'varchar(30)')

user = User(uid=1234, name='naruto', email='1832044042@qq.mail', password='hycpass')
user.save()
输出
Found mapping :uid ==> ('uid', 'int unsigned')
Found mapping :name ==> ('username', 'varchar(30)')
Found mapping :email ==> ('email', 'varchar(30)')
Found mapping :password ==> ('password', 'varchar(30)')
insert into User (uid,username,email,password) values (1234,'naruto','1832044042@qq.mail','hycpass')

4.5.2 要点

借助元类,我们可以在某个类完全定义好之前,率先修改该类的属性。

描述符与元类能够有效地组合起来,以便对某种行为做出修饰,或在程序运行时探查相关信息。

如果把元类与描述符相结合,那就可以在不使用weakref模块的前提下避免内存泄漏。

Copyright © narutohyc.com 2021 all right reserved,powered by Gitbook该文件修订时间: 2024-07-29 02:32:54

results matching ""

    No results matching ""