动态属性

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

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

python 使用特性管理实例属性

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

python理解描述符(descriptor)

python 描述符总结

Python描述符 (descriptor) 详解

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

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

基本概念

描述符是对多个属性运用相同存取逻辑的一种方式。例如,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__()方法

实现

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

1
2
3
4
class Descriptor():
def __get__(self, instance, owner):...
def __set__(self, instance, value):...
def __delete__(self, instance):...

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

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

1
2
3
4
5
6
7
8
9
10
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,但是最终打印出来值却并不是类属性的值,这是因为,实例属性实际上是在“描述符类属性”后面访问的,所以覆盖掉了。

专有名词

描述符类: 实现了描述符协议的类,描述符类的一些协议(__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__)。

例子

要点

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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实例的最后一份引用,那么,系统就会自动将该实例从字典的键中移除。

模拟ORM

代码-描述符类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/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
代码-元类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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)
代码-测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
输出
1
2
3
4
5
True
False
bobby
sec_boddy
False

成绩管理

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/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}')
输出
1
2
2020-06-25 20:18:23 lazy_db INFO:  first 82
2020-06-25 20:18:23 lazy_db INFO: second 75

成绩管理2.0

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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}')
输出
1
2
2020-06-25 20:18:23 lazy_db INFO:  first 82
2020-06-25 20:18:23 lazy_db INFO: second 75

成绩管理2.1

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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}')

进阶

添加回调

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

实现底层 @classmethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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灵活多变的地方,运行并不会出现错误。

实现底层 @staticmethod

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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参数,否则会显示参数错误。

实现底层 @property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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__方法得到了进一步简化。

按需生成属性

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

Python高级用法之动态属性

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

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

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

实例属性查找

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__。

四个魔法函数

访问时机

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

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

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

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

__getattr__

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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__。

__setattr__

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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__方法的时候千万不要重复调用造成死循环。

1
2
3
4
5
6
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__避开循环。

1
2
3
4
5
6
7
8
9
10
11
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

__delattr__

__delattr__是个删除属性的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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__,避免重复调用。

__getattribute__

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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'}

要点

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

总结:

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

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

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

例子

元类

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

基本概念

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

元类的定义

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
1
2
3
当前类名 MyClass
父类 (<class 'object'>,)
全部类属性 {'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 33, 'foo': <function MyClass.foo at 0x0000019E028315E8>}

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

元类的本质

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

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

小结

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

验证子类

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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时,其类报错,实现其验证逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

注册子类

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
###建立类名与该类对象的映射关系,维护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

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

获取__init__的默认参数

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

元类定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/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)

抽象父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/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

子类实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/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}

测试类:

1
2
3
4
5
6
7
8
9
10
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__默认参数的效果

注解类的属性

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

1
2
3
4
5
6
7
8
9
10
11
12
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类,定义该类的时候,我们要为每个类属性指定对应的列名。

1
2
3
4
5
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 构造器。

1
2
3
4
5
6
7
8
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作为其元类。凡是代表数据库里面某一行的类,都应该从这个基类中继承,以确保它们能够利用元类所提供的功能:

1
2
class DatabaseRow(object, metaclass=Meta):
pass

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

1
2
3
4
class Field:
def __init__(self):
self.name = None
self.internal_name = None

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

1
2
3
4
5
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()

ORM例子

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

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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()
输出
1
2
3
4
5
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')

要点

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

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

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