1 动态属性
在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。
2 属性描述符(get/set/delete)
2.1 基本概念
描述符是对多个属性运用相同存取逻辑的一种方式。例如,Django ORM 和 SQL Alchemy 等 ORM 中的字段类型是描述符,把数据库记录中字段里的数据与 Python 对象的属性对应起来。为什么需要描述符:对property来说,最大的缺点就是它们不能重复使用。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。
描述符是property的升级版,允许你为重复的property逻辑编写单独的类来处理。
基本要求:描述符是实现了特定协议的类,这个协议包括
__get__
、__set__
和__delete__
方法。property
类实现了完整的描述符协议。通常,可以只实现部分协议。其实,我们在真实的代码中见到的大多数描述符只实现了__get__
和__set__
方法,还有很多只实现了其中的一个。实现了
__get__
、__set__
或__delete__
方法的类是描述符。用法:描述符的用法是,创建一个描述符类,它的实例对象作为另一个类的属性。
为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用
__get__
和__set__
方法。大致流程:
- 定义一个描述符类D,其内包含一个或多个
__get__()
、__set__()
、__delete__()
方法- 将描述符类D的实例对象d赋值给另一个要代理的类中某个属性attr,即attr=D()
- 之后访问、赋值、删除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)的属性。
>
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__
使用__getattr__、__setattr__和__getattribute__来动态生成属性
Python 语言提供了一些挂钩,使得开发者很容易就能编写出通用的代码,以便将多个系统黏合起来。例如,我们要把数据库的行(row)表示为 Python 对象。由于数据库有自己的一套结构(schema),也称架构、模式、纲要、概要、大纲,所以在操作与行相对应的对象时,我们必须知道这个数据库的结构。然而,把 Python 对象与数据库相连接的这些代码,却不需要知道行的结构,所以,这部分代码应该写得通用一些。
那么,如何实现这种通用的代码呢?普通的实例属性、@property 方法和描述符,都不能完成此功能,因为它们都必须预先定义好,而像这样的动态行为,则可以通过 Python 的__getattr__特殊方法来做。如果某个类定义了__getattr__,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。
3.1 实例属性查找
首先需要明白的是实例属性查找的过程:
如果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 元类
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 :编写一个多边形类,当边数小于
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模块的前提下避免内存泄漏。