在类中强制规定编码约定
如果想对类的定义进行监控,我们通常可以用元类来进行解决。一个基本的元类通常可以通过从type中继承,然后重新定义它的__new__()或者__init__()方法就好,或者可以像这样定义__init__():
class MyMeta(type):
def __init__(self, clsname, bases, clsdict):
super().__init__(clsname, bases, clsdict)
# clsname is name of class being defined
# bases is tuple of base classes
# clsdict is class dictionary
要使用元类,一般来说会将其作用到一个顶层基类上,然后让其它子类继承之:
class Root(metaclass=MyMeta):
pass
class A(Root):
pass
class B(Root):
pass
元类的一个核心功能就是允许在自定义类的时候对类本身的内容进行检查。在重新定义的__init__方案中,我们可以自由地检查类字典,基类以及其它更多信息。此外,一旦为某个类指定了元类,这个类中的所有子类都会自动继承这个特性。
比如下面这个例子就可以用来恶心下java程序员hhhhhh,这里的元类可以用来拒绝类定义中包含大小写混用的方法名:
class NoMixedCaseMeta(type):
def __new__(cls, clsname, bases, clsdict):
for name in clsdict:
if name.lower() != name:
raise TypeError('Bad attribute name: ' + name)
return super().__new__(cls, clsname, bases, clsdict)
class Root(metaclass=NoMixedCaseMeta):
pass
class A(Root):
def foo_bar(self): # Ok
pass
class B(Root):
def fooBar(self): # TypeError
pass
作为一个更加高级而且有用的例子,下面定义的元类可以检查子类中是否有重新定义的方法,确保它们的调用签名和父类中的原始方法相同。
from inspect import signature
import logging
class MatchSignaturesMeta(type):
def __init__(self, clsname, bases, clsdict):
super().__init__(clsname, bases, clsdict)
sup = super(self, self)
for name, value in clsdict.items():
if name.startswith('_') or not callable(value):
continue
# Get the previous definition (if any) and compare the signatures
prev_dfn = getattr(sup,name,None)
if prev_dfn:
prev_sig = signature(prev_dfn)
val_sig = signature(value)
if prev_sig != val_sig:
logging.warning('Signature mismatch in%s.%s!=%s',
value.__qualname__, prev_sig, val_sig)
# Example
class Root(metaclass=MatchSignaturesMeta):
pass
class A(Root):
def foo(self, x, y):
pass
def spam(self, x, *, z):
pass
# Class with redefined methods, but slightly different signatures
class B(A):
def foo(self, a, b):
pass
def spam(self,x,z):
pass
在大型面向对象的程序中,通常将类的定义放在元类中控制是很有用的。 元类可以监控类的定义,警告编程人员某些没有注意到的可能出现的问题。
有人可能会说,像这样的错误可以通过程序分析工具或IDE去做会更好些。诚然,这些工具是很有用。 但是,如果你在构建一个框架或函数库供其他人使用,那么你没办法去控制使用者要使用什么工具。 因此,对于这种类型的程序,如果可以在元类中做检测或许可以带来更好的用户体验。
在元类中选择重新定义 __new__() 方法还是 __init__() 方法取决于你想怎样使用结果类。__new__() 方法在类创建之前被调用,通常用于通过某种方式(比如通过改变类字典的内容)修改类的定义。 而 __init__() 方法是在类被创建之后被调用,当你需要完整构建类对象的时候会很有用。 在最后一个例子中,这是必要的,因为它使用了 super() 函数来搜索之前的定义。 它只能在类的实例被创建之后,并且相应的方法解析顺序也已经被设置好了。
最后一个例子还演示了Python的函数签名对象的使用。 实际上,元类将每个可调用定义放在一个类中,搜索前一个定义(如果有的话), 然后通过使用 inspect.signature() 来简单的比较它们的调用签名。
最后一点,代码中有一行使用了 super(self, self) 并不是排版错误。 当使用元类的时候,我们要时刻记住一点就是 self 实际上是一个类对象。 因此,这条语句其实就是用来寻找位于继承体系中构建 self 父类的定义。
通过编程的方式类定义类
我们可以使用函数types.new_class()来初始化新的类对象。 你需要做的只是提供类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数。例如:
# stock.py
# Example of making a class manually from parts
# Methods
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
cls_dict = {
'__init__' : __init__,
'cost' : cost,
}
# Make a class
import types
Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))
Stock.__module__ = __name__
这种方式会构建一个普通的类对象,并且按照你的期望工作:
>>> s = Stock('ACME', 50, 91.1)
>>> s
>>> s.cost()
4555.0
>>>
这种方法中,一个比较难理解的地方是在调用完 types.new_class() 对 Stock.__module__ 的赋值。 每次当一个类被定义后,它的 __module__ 属性包含定义它的模块名。 这个名字用于生成 __repr__()方法的输出。它同样也被用于很多库,比如 pickle 。 因此,为了让你创建的类是“正确”的,你需要确保这个属性也设置正确了。
如果你想创建的类需要一个不同的元类,可以通过 types.new_class() 第三个参数传递给它。例如:
>>> import abc
>>> Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta},
... lambda ns: ns.update(cls_dict))
...
>>> Stock.__module__ = __name__
>>> Stock
>>> type(Stock)
>>>
参考书目:
《Python CookBook》作者:【美】 David Beazley, Brian K. Jones
Github地址:yidao620c/python3-cookbookgithub.com