In [2]:
class Dog:
def __init__(self, name, weight, owner):
self.name = name
self.weight = weight
self.owner = owner
rex = Dog('Rex', 30, 'Bob')
rex
Out[2]:
这段代码各个字段名都出现了三次,让人厌烦,字符串表现形式也不友好,我们编写一个 record_factory 类工厂函数解决这个问题
In [3]:
def record_factory(cls_name, field_names):
try:
# 这里体现了鸭子类型,尝试在都好或空格处拆分 field_names,如果失败,则假定 field_names 本身就是可迭代对象
field_names = field_names.replace(',', ' ').split()
except AttributeError: #不能调用 .replace 或 .split 方法
pass # 假定 field_names 本就是标识符组成的序列
field_names = tuple(field_names) #使用属性名构建元组,这将成为新建类的 __slots__属性
# __slots__变量,来限制该class能添加的属性
# 将变成新建类的 __init__ 方法
def __init__(self, *args, **kwargs):
attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
# 把类的实例变成可迭代对象,按照 __slots__ 设定的顺序产出字段值
def __iter__(self):
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self):
values = ', '.join('{}={!r}'.format(*i) for i
in zip(self.__slots__, self))
return '{}({})'.format(self.__class__.__name__, values)
# 组建类属性字典
cls_attrs = dict(__slots__ = field_names,
__init__ = __init__, # 相当于 '__init__': __init__
__iter__ = __iter__,
__repr__ = __repr__)
# 用 type 方法构造,构建新类,然后返回
return type(cls_name, (object,), cls_attrs)
In [4]:
Dog = record_factory('Dog', 'name weight owner')
rex = Dog('Rex', 30, 'Bob')
rex
Out[4]:
In [5]:
name, weight, _ = rex # 实例是可迭代对象,所以可以方便的拆包
name, weight
Out[5]:
In [6]:
"{2}'s dog weight {1}kg".format(*rex) # 实例是可迭代对象,所以可以方便的拆包
Out[6]:
In [7]:
rex.weight = 32 记录实例是可变的对象
rex
In [8]:
Dog.__mro__ # 新建的类继承 Object 类,和我们的工厂函数没有关系
Out[8]:
通常,我们将 type 视为函数,因为我们像函数那样使用它,type(my_object) 获取对象所属的类 -- 作用与 my_object.__class__
相同。然而,type 是一个类,当成类使用的时候传入三个参数可以新建一个类(是的,type 可以根据传入的不同参数有不同的用法
type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
比如下面的代码是等价的
class MyShinyClass:
pass
##############
type('MyShinyClass', (), {})
因此我们要新建如下类:
class Foo:
bar = True
可以写成:
In [9]:
Foo = type('Foo', (), {'bar':True})
Foo
Out[9]:
In [10]:
Foo.bar
Out[10]:
In [11]:
f = Foo()
f
Out[11]:
如果你继承 Foo 类,可以写成
FooChild = type('FooChild', (Foo,),{})
我们看到 type 函数可以创建一个类,因为 type 是元类,Python 中所有对象都是由 type 创建而来,注意,Python 中所有的东西都是对象,包括 整数,字符串、函数以及类,都由 type 创建而来
In [12]:
age = 35
print(age.__class__)
age.__class__.__class__
Out[12]:
In [13]:
name = 'bob'
print(name.__class__)
name.__class__.__class__
Out[13]:
In [14]:
def foo(): pass
foo.__class__
foo.__class__.__class__
Out[14]:
In [15]:
class Bar(object): pass
b = Bar()
b.__class__
b.__class__.__class__
Out[15]:
总之,前面的 record_factory 函数最后一行会构建一个类,类的名称是 cls_name
参数的值,唯一直接超类是 object,有 __slots__
, __init__
, __iter__
, __repr__
四个类属性,其中后 3 个是实例方法。
我们本来可以将 __slots__
类属性改成其它值,不过那样就要实现 __setattr__
方法,为属性赋值时验证属性的名称,而且顺序相同,然而第 9 章说过,__slots__
属性最主要特点就是节省内存,能处理数百万个实例,不过也有一些缺点。
把 3 个参数传给 type 是动态创建类的常用方式,如果查看 collections.namedtuple 源码会发现另一种方式,先声明一个 _class_template
变量,其值是字符串形式源码模板,然后在 namedtuple 函数中调用 _class_template.format(...)
方法,填充模板里的空白,最后,使用内置的 exec
函数计算得到源码字符串
在 Python 元编程时,最好不要使用 exec 和 eval 函数,如果接受字符串来自不可信的源,这两个函数会有严重的安全风险,Python 提供了足够的内省工具,大多数时候不需要这两个函数。
record_factory 函数创建的类不能够序列化,即不能使用 pikle 模块里的 dump/load 函数处理,
上一章的 LineItem 例子还有个问题,就是储存的属性不具有描述性,即属性 _Quantity#0
不便于调试,如果能存储成 _Quantity#weight
之类的就好多了,上一章说过,我们不能使用描述性的存储属性名称,因为实例化描述符时无法得知托管属性,如前面的 weight 的名称,可是如果组建好整个类,而且把描述符绑定到类属性后,我们就可以审查类,并为描述符设置合理的存储属性名称。LineItem 的 __new__
方法可以做到这一点,因此,在 __init__
方法中使用描述符时,存储属性已经设置了正确的名称。为了解决这个问题使用 __new__
方法属于白费力气,每次新建 LineItem 实例时都会运行 __new__
方法中的逻辑,可是一旦 LineItem 类构建好了,描述符与托管属性之间的绑定就不会变了。因此,我们要在创建类时设置存储属性的名称。使用类装饰器或元类可以做到这一点,我们先使用简单的方式。
类装饰器和函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类
In [16]:
import abc
class AutoStorage:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
setattr(instance, self.storage_name, value) # 不进行验证
class Validated(abc.ABC, AutoStorage): # 抽象类,也继承自 AutoStorage
def __set__(self, instance, value):
# __set__ 方法把验证委托给 validate 方法
value = self.validate(instance, value)
#返回的 value 值返回给超类的 __set__ 方法,存储值
super().__set__(instance, value)
@abc.abstractmethod
def validate(self, instance, value): # 抽象方法
'''return validated value or raise ValueError'''
class Quantity(Validated):
'''a number greater than zero'''
# 只需要根据不同的验证规则实现 validate 方法即可
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value
class NonBlank(Validated):
'''a string with at least one not-space character'''
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value
# class LineItem: # 托管类
# weight = Quantity()
# price = Quantity()
# description = NonBlank()
# def __init__(self, description, weight, price):
# self.description = description
# self.weight = weight
# self.price = price
# def subtotal(self):
# return self.weight * self.price
## --------------------
## 上面的和 上一章代码相同, LineItem 类只加了 1 行,在下面实现
## --------------------
def entity(cls):
for key, attr in cls.__dict__.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
return cls #返回修改后的类
In [17]:
@entity # 类装饰器,定义类的时候就会调用
class LineItem:
weight = Quantity()
price = Quantity()
description = NonBlank()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
In [18]:
raisins = LineItem('Golden raisins', 10, 6.95)
dir(raisins)[:3]
Out[18]:
In [19]:
LineItem.description.storage_name
Out[19]:
In [20]:
raisins.description
Out[20]:
In [21]:
getattr(raisins, '_NonBlank#description')
Out[21]:
In [ ]:
#!/usr/bin/env python
# encoding: utf-8
from evalsupport import deco_alpha
print('<[0]> evaltime module start')
def test():
class Test:
print('<[1]> evaltime test Test')
class ClassOne():
print('<[2]> ClassOne body')
def __init__(self):
print('<[3]> ClassOne.__init__')
def __del__(self):
print('<[4]> ClassOne.__del__')
def method_x(self):
print('<[5]> ClassOne.method_x')
class ClassTwo(object):
print('<[6]> ClassTwo body')
@deco_alpha
class ClassThree():
print('<[7]> ClassThree body')
def method_y(self):
print('<[8]> ClassThree.method_y')
class ClassFour(ClassThree):
print('<[9]> ClassFour body')
def method_y(self):
print('<[10]> ClassFour.method_y')
if __name__ == '__main__':
print('<[11]> ClassOne tests', 30 * '.')
one = ClassOne()
one.method_x()
print('<[12]> ClassThree tests', 30 * '.')
three = ClassThree()
three.method_y()
print('<[13]> ClassFour tests', 30 * '.')
four = ClassFour()
four.method_y()
print('<[14]> evaltime module end')
evalsupport.py
In [ ]:
#!/usr/bin/env python
# encoding: utf-8
print('<[100]> evalsupport module start')
def deco_alpha(cls):
print('<[200]> deco_alpha')
def inner_1(self):
print('<[300]> deco_alpha:inner_1')
cls.method_y = inner_1
return cls
class MetaAleph(type):
print('<[400]> MetaAleph body')
def __init__(cls, name, bases, dic):
print('<[500]> MetaAleph.__init__')
def inner_2(self):
print('<[600]> MetaAleph.__init__:inner_2')
cls.method_z = inner_2
print('<[700]> evalsupport module end')
In [1]: import evaltime
<[100]> evalsupport module start #evalsupport 模块中所有顶层代码在导入模块时执行,解释器会编译 deco_alpha 函数,但不会执行定义体
<[400]> MetaAleph body # 类定义体运行了
<[700]> evalsupport module end
<[0]> evaltime module start
<[2]> ClassOne body # 每个类的定义体都执行了
<[6]> ClassTwo body #包括嵌套的类
<[7]> ClassThree body
<[200]> deco_alpha # 先计算被装饰的 ClassThree 类定义体,然后运行装饰器函数
<[9]> ClassFour body
<[14]> evaltime module end #这里,evaltime 是被导入的,不会运行 if __name == '__main__'
(py35) kaka@kaka-deep:~/kaka$ python3 evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[0]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1 # 类装饰器改变了 ClassThree.method_y 方法
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end
<[4]> ClassOne.__del__ # 程序结束后,绑定在全局变量 one 上的 ClassOne 实例才会被垃圾回收
元类是制造类的工厂,不过不是函数,而是类。
根据 Python对象模型,类是对象,因此类肯定是另外某个类的实例,默认情况下,Python 中的类是 type 的实例,也就是说,type 是大多数内置的类和用户定义的类的元类,为了避免无限递归,type 是自身的实例。注意,我们没有说 str 或者 LineItem 继承自 type,而是说 str 和 LineItem 是 type 的实例。
object 类和 type 类之间的关系很独特,object 是 type 的实例,type 是 object 的子类,这种关系很独特,无法使用 Python 代码表述,因为其定义其中一个之前另一个必须存在,type 是自身的实例这一点也很神奇
除了 type,标准库中还有一些别的类,例如 ABCMeta 和 Enum。如下所示:
In [1]:
import collections
collections.Iterable.__class__
Out[1]:
In [2]:
import abc
abc.ABCMeta.__class__
Out[2]:
In [3]:
abc.ABCMeta.__mro__
Out[3]:
向上追溯,ABCMeta 最终所属的类也是 type,所有类都直接或间接的是 type 的实例,不过只有元类同事也是 type 的子类。若理解元类,一定要知道这种关系:元类(如 ABCMeta)从 type 类继承了构建类的能力。
我们要抓住的重点是,所有类都是 type 的实例,但元类还是 type 的子类,因此可以作为制造类的工厂,具体来说,元类可以通过实现 __init__
方法来定制。元类的 __init__
方法可以做到类装饰器能做的任何事情,但是作用更大
我们让 evalsupport.py 与原来相同,新建一个 evaltime_meta.py 作为主脚本:
In [ ]:
#!/usr/bin/env python
# encoding: utf-8
from evalsupport import deco_alpha
from evalsupport import MetaAleph
print('<[1]> evaltime module start')
@deco_alpha
class ClassThree():
print('<[2]> ClassThree body')
def method_y(self):
print('<[3]> ClassThree.method_y')
class ClassFour(ClassThree):
print('<[4]> ClassFour body')
def method_y(self):
print('<[5]> ClassFour.method_y')
class ClassFive(metaclass=MetaAleph):
print('<[6]> ClassFive body')
def __init__(self):
print('<[7]> ClassFive body')
def method_z(self):
print('<[8]> ClassFive.method_z')
class ClassSix(ClassFive):
print('<[9]> ClassSix body')
def method_z(self):
print('<[10]> ClassSix.method_z')
if __name__ == '__main__':
print('<[11]> ClassThree tests', 30 * '.')
three = ClassThree()
three.method_y()
print('<[12]> ClassFour tests', 30 * '.')
four = ClassFour()
four.method_y()
print('<[13]> ClassFive tests', 30 * '.')
five = ClassFive()
five.method_z()
print('<[14]> ClassSix tests', 30 * '.')
six = ClassSix()
six.method_z()
print('<[15]> evaltime module end')
引入操作:
In [1]: import evaltime_meta
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__ #与前面关键区别是,创建 ClassFive时调用了 MetaAleph.__init__ 方法
<[9]> ClassSix body
<[500]> MetaAleph.__init__ # 同上
<[15]> evaltime module end
Python 解释器计算 ClassFive 类的定义体时没有调用 type 构建具体的类定义体,而是调用 MetaAleph 类。MetaAleph 类的 __init__
有 4 个参数。
self: 要初始化的对象,例如 ClassFive name, bases, dic: 与构建类时传给 type 的参数一样
重新看一下这个类:
class MetaAleph(type):
print('<[400]> MetaAleph body')
def __init__(cls, name, bases, dic):
print('<[500]> MetaAleph.__init__')
def inner_2(self):
print('<[600]> MetaAleph.__init__:inner_2')
cls.method_z = inner_2
编写元类时候,通常把 self 参数改成 cls。__init__
方法的定义体中定义了 inner_2
函数,然后绑定给 cls.method_z
。MetaAleph.__init__
方法签名中的 cls 指代要创建的类(例如 ClassFive)。而 inner_2
函数签名中的 self 最终是指代我们创建的类的实例(例如 ClassFive 类的实例)
运行脚本:
(pytorch) kaka@kaka-dell:~/kaka/python$ python3 evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__
<[9]> ClassSix body
<[500]> MetaAleph.__init__
<[11]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1
<[12]> ClassFour tests ..............................
<[5]> ClassFour.method_y
<[13]> ClassFive tests ..............................
<[7]> ClassFive body
<[600]> MetaAleph.__init__:inner_2 # MetaAleph 类的 __init__ 方法把ClassFive.method_z 方法替换成 inner_2 函数。
<[14]> ClassSix tests ..............................
<[7]> ClassFive body
<[600]> MetaAleph.__init__:inner_2 # ClassFive 的子类 ClassSix 也是一样
<[15]> evaltime module end
注意,ClassSix 类没有直接引用 MetaAleph 类,但是却收到了影响,因为它是 ClassFive 的子类,进而也是 MetaAleph 类的实例,所以由 MetaAleph.__init__
实例化
In [22]:
class EntityMeta(type):
"""元类,用于创建带有验证字段的业务实体"""
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict) # 在超类(这里是 type)上调用 __init__
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
class Entity(metaclass=EntityMeta): # 这个类只是为了用起来便利,这个模块的用户直接继承它即可,不用关心元类
'''带有验证字段的业务实体'''
class LineItem(Entity):
weight = Quantity()
price = Quantity()
description = NonBlank()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
写成这种语法,用户完全不用知道描述符或元类,直接继承库中提供的类就能满足要求
__prepare__
在某些应用中,可能要知道类属性的定义顺序,例如读写 csv 文件的库,用户定义的类可能想要把类中按顺序声明的字段与 csv 文件中的各列对应起来
前面说过,type 构造方法以及元类的 __new__
和 __init__
都接收类的定义体,形式是一个名称到属性的字典,也就是说,当元类或装饰器获得映射时,属性的顺序已经丢失了。
在 Python 3 中可以使用 __prepare__
, 这个特殊方法只能在元类中使用,而且要声明为类方法(即,要使用 classmethod 类装饰器定义)。解释器调用元类 __new__
方法之前会调用 __prepare__
方法,使用类定义提中的属性创建映射。__prepare
第一个参数是元类,随后两个参数是类的名称以及组成的元祖,返回值是映射。元类构建新类时,__prepare__
方法返回的映射会传给 __new__
方法的最后一个参数,然后再传给 __init__
方法
In [24]:
import collections
class EntityMeta(type):
"""元类,用于创建带有验证字段的业务实体"""
@classmethod
def __prepare__(cls, name, bases):
return collections.OrderedDict() # 返回空的 OrderedDict 实例,存储类属性
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict) # 在超类(这里是 type)上调用 __init__
cls._field_names = []
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
cls._field_names.append(key) # 按顺序存储类属性
class Entity(metaclass=EntityMeta): # 这个类只是为了用起来便利,这个模块的用户直接继承它即可,不用关心元类
'''带有验证字段的业务实体'''
@classmethod
def field_names(cls):
for name in cls._field_names:
yield name # 按照添加字段的顺序产出字段名称
In [26]:
class LineItem(Entity):
weight = Quantity()
price = Quantity()
description = NonBlank()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
for name in LineItem.field_names():
print(name)
在现实世界中,框架和库会使用元类协助程序员执行很多任务,例如:
Python 模型为每个类定义了很多属性,例如
cls.__mro__
: 超类元组cls.__class__
: 所属类cls.__name__
: 类名cls.__bases__
: 由类的基类组成的元组cls.__qualname__
: 或函数的限定名称,即从模块的全局作用域到类的点分路径cls.__subclasses__()
: 这个方法返回一个列表,包含类的直接子类。cls.mro()
: 调用 cls.__mro__
In [ ]: