本节书摘来自异步社区《Python面向对象编程指南》一书中的第2章,第2.9节,作者[美]Steven F. Lott, 张心韬 兰亮 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.9 new()方法和元类型
__new__
()方法的另一种用途,作为元类型的一部分,主要是为了控制如何创建一个类。这和之前的如何用__new__()控制一个不可变对象是完全不同的。
一个元类型创建一个类。一旦类对象被创建,我们就可以用这个类对象创建不同的实例。所有类的元类型都是type,type()函数被用来创建类对象。
另外,type()函数还可以被用作显示当前对象类型。
下面是一个很简单的例子,直接使用type()作为构造器创建了一个新的但是几乎完全没有任何用处的类:
Useless= type("Useless",(),{})
一旦我们创建了这个类,我们就可以开始创建这个类的对象。但是,这些对象什么都做不了,因为我们没有定义任何方法和属性。
为了最大化利用这个类,在下面的例子中,我们使用这个新创建的Useless类来创建对象。
>>> Useless()
<__main__.Useless object at 0x101001910>
>>> u=_
>>> u.attr= 1
>>> dir(u)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
'__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attr']
我们可以向这个类的对象中增加属性。至少,作为一个对象,它工作得很好。
这样的类定义与使用types.SimpleNamespace或者像下面这样定义一个类的方式几乎相同。
class Useless:
pass
这带来一个重要的问题:为什么我们一开始要复杂化定义一个类的方法呢?
答案是,类中一些默认的特性无法应用到一些特殊的类上。下面,我们会列举4种应该使用元类型的场景。
- 我们可以使用元类型来保留一个类源码中的文本信息。一个使用内置的type创建的类型会使用dict来存储不同的方法和类级属性。因为字典是无序的,所以属性和方法没有特别的排列顺序。所以极有可能这些信息会以和源码中不同的顺序出现。我们会在第1个例子中讲解这点。
- 在第4~7章中我们会看到元类型被用来创建抽象基类。一个抽象基类基于__new__()方法来确定子类的完整性。在第4章“抽象基类设计的一致性”中,我们会介绍这点。
- 元类型可以被用来简化对象序列化的某些方面。在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中,我们会详细介绍这一点。
- 作为最后一个也是最简单的例子,我们会看看一个类中对自己的引用。我们会设计一个引用了master类的类。这不是一种基类—子类的关系。这是一些平行的子类,但是引用了这些子类中的一个作为master。为了和它平行的类保持一致,主类需要包含一个指向自身的引用,如果不用元类型,不可能实现这样的行为。这是我们的第2个例子。
2.9.1 元类型示例1——有序的属性
这是Python Language Reference 3.3.3节“自定义Python的类创建”中的经典例子,这个元类型会记录属性和方法的定义顺序。
下面是实现的3个具体步骤。
1.创建一个元类型。元类型的__prepare__()和__new__()方法会改变目标类创建的方式,会将原本的dict类替换为OrderedDict类。
2.创建一个基于此元类型的抽象基类。这个抽象类简化了其他类继承这个元类型的过程。
3.创建一个继承于这个抽象基类的子类,这样它就可以获得元类型的默认行为。
下面是使用该元类型的例子,它将保留属性创建的顺序。
import collections
class Ordered_Attributes(type):
@classmethod
def __prepare__(metacls, name, bases, **kwds):
return collections.OrderedDict()
def __new__(cls, name, bases, namespace, **kwds):
result = super().__new__(cls, name, bases, namespace)
result._order = tuple(n for n in namespace if not
n.startswith('__'))
return result
这个类用自定义的__prepare__()和__new__()方法扩展了内置的默认元类型type。
__prepare__()方法会在类创建之前执行,它的工作是创建初始的命名空间对象,类定义最后被添加到这个对象中。这个方法可以用来处理任何在类的主体开始执行前需要的准备工作。
__new__()静态方法在类的主体被加入命名空间后开始执行。它的参数是要创建的类对象、类名、基类的元组和创建好的命名空间匹配对象。这个例子很经典:它将__new__()的真正工作委托给了基类;一个元类型的基类是内置的type;然后我们使用type.__new__()创建一个稍后可以修改的默认类。
这个例子中的__new__()方法向类中增加了一个_order属性,用于存储原始的属性创建顺序。
当我们定义新的抽象基类时,我们可以用这个元类型而非type。
class Order_Preserved( metaclass=Ordered_Attributes ):
pass
然后,我们可以将这个新的抽象基类作为任何其他自定义类的基类,如下所示。
class Something( Order_Preserved ):
this= 'text'
def z( self ):
return False
b= 'order is preserved'
a= 'more text'
我们可以用下面的代码来介绍Something类的使用。
>>> Something._order
>>> ('this', 'z', 'b', 'a')
我们可以考虑利用这些信息来正确序列化对象或者用于提供原始代码定义的调试信息。
2.9.2 元类型示例2——自引用
接下来,我们看看一个关于单位换算的例子。例如,长度单位包括米、厘米、英寸、英尺和许多其他的单位。正确地管理单位换算是非常有挑战性的。表面上看,我们需要一个表示不同单位间转换因子的矩阵。例如,英尺转换为米、英尺转换为英寸、英尺转换为码、米转换为英寸、米转换为码等可能的组合。
但是,在实践中,一个更好的方案是定义一个长度的标准单位。我们可以把任何其他单位转换为标准单位,也可以把标准单位转换为任何其他单位。通过这种方式,我们可以很容易地将单位转换变成一致的两步操作,而不用再考虑包含了所有可能转换的复杂矩阵:英尺转换为标准单位,英寸转换为标准单位,码转换为标准单位,米转换为标准单位。
在下面的例子中,我们不准备继承float或者numbers.Number。相比于将单位和数值绑定在一起,我们更倾向于允许让每一个值仅仅代表一个简单的数字。这是享元模式的一个例子,类中不会定义包含相关值的对象,对象中仅仅包括转换因子。
另一种方案(将值和单位绑定)会造成需要相当复杂的三围分析。虽然这很有趣,但是太复杂了。
我们会定义两个类:Unit和Standard_Unit。我们可以很容易保证每个Unit类中都正确地包含一个指向它的Standard_Unit的引用。但是,我们如何能够保证每一个Standard_Unit类中都有一个指向自己的引用呢?在类定义中实现子引用是不可能的,因为此时都还没有定义类。
下面是我们的Unit类的定义。
class Unit:
"""Full name for the unit."""
factor= 1.0
standard= None # Reference to the appropriate StandardUnit
name= "" # Abbreviation of the unit's name.
@classmethod
def value( class_, value ):
if value is None: return None
return value/class_.factor
@classmethod
def convert( class_, value ):
if value is None: return None
return value*class_.factor
这个类的目的是Unit.value()可以将一个值从给定的单位转换为标准单位,而Unit.convert()方法可以将一个值从标准单位转换为给定的单位。
这让我们可以用下面的方式转换单位。
>>> m_f= FOOT.value(4)
>>> METER.convert(m_f)
1.2191999999999998
创建的值类型是内置的float类型。对于温度的计算,我们需要重载默认的value()和convert()方法,因为简单的乘法运算不能满足实际物景。
对于Standard_Unit,我们可能会使用下面这样的代码:
class INCH:
standard= INCH
但是,这段代码无效。因为INCH还没有定义在INCH类中。在完成定义之前,这个类都是不存在的。
我们可以用下面的备用方法来处理这种情况。
class INCH:
pass
INCH.standard= INCH
但是,这样的做法相当丑陋。
我们还可以像下面这样定义一个修饰符。
@standard
class INCH:
pass
这个修饰符方法可以用来向类定义中加入一个属性。在第8章“装饰器和mixin——横切方面”中,我们再详细探讨这种方法。
现在,我们会定义一个可以向类定义中插入一个循环引用的元类型,如下所示。
class UnitMeta(type):
def __new__(cls, name, bases, dict):
new_class= super().__new__(cls, name, bases, dict)
new_class.standard = new_class
return new_class
这段代码强制地将变量standard作为类定义的一部分。
对大多数单位,SomeUnit.standard引用了TheStandardUnit类。类似地,我们也让TheStandardUnit.standard引用TheStandardUnit类。Unit和Standard_Unit类之间这种一致的结构能够帮助我们书写文档和自动化单位转换。
下面是Standard_Unit类:
class Standard_Unit( Unit, metaclass=UnitMeta ):
pass
从Unit继承的单位转换因子是1.0,所以它并没有提供任何值。它包括了特殊的元类型定义,这样它就会有自引用,这个自引用表明这个类是这一特定维度的测量标准。
作为一种优化的手段,我们可以重载value()和convert()方法来禁止乘法和除法运算。
下面是一些单位类的例子。
class INCH( Standard_Unit ):
"""Inches"""
name= "in"
class FOOT( Unit ):
"""Feet"""
name= "ft"
standard= INCH
factor= 1/12
class CENTIMETER( Unit ):
"""Centimeters"""
name= "cm"
standard= INCH
factor= 2.54
class METER( Unit ):
"""Meters"""
name= "m"
standard= INCH
factor= .0254
我们将INCH定为标准单位,其他单位需要转换成英寸或者从英寸转换而来。
在每一个单位类中,我们都提供了一些文档信息:全名写在docstring中并且用name属性记录缩写。从Unit继承而来的convert()和value()方法会自动应用转换因子。
有了这些类的定义,我们就可以在程序中像下面这样编码。
>>> x_std= INCH.value( 159.625 )
>>> FOOT.convert( x_std )
13.302083333333332
>>> METER.convert( x_std )
4.054475
>>> METER.factor
0.0254
我们可以根据给定的英寸值设置一种特定的测量方式并且可以将该值转换为任何兼容的单位。
由于元类型的存在,我们可以像下面这样从单位类中查询。
>>> INCH.standard.__name__
'INCH'
>>> FOOT.standard.__name__
'INCH'
这种引用方式让我们可以追踪一个指定维度上的不同单位。