9.15 定义有可选参数的元类
- 问题:想要定义一个元类,它的参数是可选的,这样可以控制或者配置类型的创建过程
- 方案:使用“metaclass”关键字参数指定特定的元类
from abc import ABCMeta,abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self,maxsize=None):
pass
@abstractmethod
def write(self,data):
pass
- 也可以使用其它关键字,但是必须在__prepare__()和__new__()、__init__()中都是用强制关键字参数:
class MyMeta(type):
@classmethod
def __prepare__(cls, name, bases, *, debug=False, synchronize=False):
pass
return super().__prepare__(name,bases)
def __new__(cls, name, bases, ns, *, debug=False, synchronize=False):
pass
return super().__new__(cls, name, bases, ns)
def __init__(cls, name, bases, *, debug=False, synchronize=False):
pass
super().__init__(name, bases, ns)
- __prepare__():在所有类定义开始执行之前首先被调用,用来创建类命名空间
- __new__():用来实例化类对象,他在类的主体被执行完后开始执行
- __init__():最后被调用,用来给对象进行初始化
- 当我们要定义元类时,只需要定义new()或者init()方法,但并不是都定义。但是当要接受其它关键字参数的时候,就需要两个都定义并提供对应的参数签名。默认的 prepare() 方法接受任意的关键字参数,但是会忽略它们,所以只有当这些额外的参数可能会影响到类命名空间的创建时你才需要去定义 prepare() 方法
9.16 * args 和** kwargs的强制参数签名
- 问题:有一个函数或者方法,它的参数为 * args和** kwargs,这样使得它比较通用,但是想要对传进来的参数进行类型检查
- 方案:对于任何涉及到操作函数调用签名的问题,都应该使用inspect模块中的签名特性
from inspect import Signature, Parameter
parms = [Parameter('x',Parameter.POSITIONAL_OR_KEYWORD),
Parameter('y',Parameter.POSITIONAL_OR_KEYWORD,default=42),
Parameter('z',Parameter.KEYWORD_ONLY,default=None)]
sig = Signature(parms)
print(sig)
(x, y=42, *, z=None)
- 一旦有了签名对象,就可以使用它的bind()方法将它绑定到* args和** kwargs上去
def func(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
print(name,value)
func(1,2,z=3)
x 1
y 2
z 3
func(1)
x 1
func(1,z=3)
x 1
z 3
func(y=2,x=1)
x 1
y 2
- 下面的例子更加具体,首先在基类中定义一个通用的初始化方法,然后强制所有的子类必须提供一个特定的参数签名。
from inspect import Signature,Parameter
def make_sig(*names):
parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names]
return Signature(parms)
class Structure:
__signature__ = make_sig()
def __init__(self, *args, **kwargs):
bound_values = self.__signature__.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
setattr(self,name,value)
class Stock(Structure):
__signature__ = make_sig('name','shares','price')
class Point(Structure):
__signature__ = make_sig('x','y')
import inspect
print(inspect.signature(Stock))
(name, shares, price)
s1 = Stock('ACME', 100, 389.1)
s2 = Stock('ACME', 100)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-24-39f0ea31793d> in <module>()
----> 1 s2 = Stock('ACME', 100)
<ipython-input-20-8adc4a21dcf0> in __init__(self, *args, **kwargs)
7 __signature__ = make_sig()
8 def __init__(self, *args, **kwargs):
----> 9 bound_values = self.__signature__.bind(*args, **kwargs)
10 for name, value in bound_values.arguments.items():
11 setattr(self,name,value)
d:\program filles\python\lib\inspect.py in bind(*args, **kwargs)
2966 if the passed arguments can not be bound.
2967 """
-> 2968 return args[0]._bind(args[1:], kwargs)
2969
2970 def bind_partial(*args, **kwargs):
d:\program filles\python\lib\inspect.py in _bind(self, args, kwargs, partial)
2881 msg = 'missing a required argument: {arg!r}'
2882 msg = msg.format(arg=param.name)
-> 2883 raise TypeError(msg) from None
2884 else:
2885 # We have a positional argument to process
TypeError: missing a required argument: 'price'
9.17在类上强制使用编程规约
- 问题:程序中包含很大的类继承体系,希望强制执行某些编程规约
- 方案:入股想要监控类的定义,通常可以定义一个元类,一个基本元类通常是继承自type并重新定义new方法或者init方法
class MyMeta(type):
def __new__(self, clsname, bases, clsdict):
return super().__new__(cls, clsname, bases, clsdict)
# 另一种是定义__ini__()
class MyMeta(type):
def __init__(self, clsname, bases, clsdict):
super().__init__(clsname,bases, clsdict)
# 为了使用上面的元类,通常把他放到一个顶级父类定义之中,然后其他的类继承这个顶级父类
class Root(metaclass=MyMeta):
pass
class A(Root):
pass
class B(Root):
pass
- 元类的一个关键特点是它允许在定义的时候检查类的内容。下面的例子定义了一个元类,它会拒绝任何有混合大小写名字作为方法的类定义
class NoMixCaseMeta(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=NoMixCaseMeta):
pass
class A(Root):
def foo_bar(self):
pass
class B(Root):
def fooBar(self):
pass
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-30-bc6fbee49cec> in <module>()
----> 1 class B(Root):
2 def fooBar(self):
3 pass
<ipython-input-29-e74c44098507> in __new__(cls, clsname, bases, clsdict)
3 for name in clsdict:
4 if name.lower() != name:
----> 5 raise TypeError(" Bad attribute name: ", name)
6 return super().__new__(cls, clsname, bases,clsdict)
7 class Root(metaclass=NoMixCaseMeta):
TypeError: (' Bad attribute name: ', 'fooBar')
- 作为一个更加高级的例子,下面的元类用来检测重载方法,确保他调用的参数和父类中原始方法有着相同的参数签名。
from inspect import signature
import logging
class MatchSignatureMeta(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
pre_dfn = getattr(sup, name, None)
if pre_dfn:
prev_sig = signature(pre_dfn)
val_sig = signature(value)
if prev_sig != val_sig:
logging.warning("Signature mismatch in %s. %s != %s", value.__qualname__, prev_sig, val_sig)
class Root(metaclass=MatchSignatureMeta):
pass
class A(Root):
def foo(self,x, y):
pass
def spam(self,x,*,z):
pass
class B(A):
def foo(self, a, b):
pass
def spam(self,x,z):
pass
WARNING:root:Signature mismatch in B.foo. (self, x, y) != (self, a, b)
WARNING:root:Signature mismatch in B.spam. (self, x, *, z) != (self, x, z)
-
在元类中选择重新定义 __new__() 方法还是 __init__() 方法取决于你想怎样使
用结果类。 __new__() 方法在类创建之前被调用,通常用于通过某种方式(比如通过改
变类字典的内容)修改类的定义。而 __init__() 方法是在类被创建之后被调用,当你
需要完整构建类对象的时候会很有用。 -
代码中有一行使用了 super(self, self) ,当使用元类的时候,我们要时刻记住一点就是 self 实际上是一个类对象。因此,这条语句其实
就是用来寻找位于继承体系中构建 self 父类的定义。
9.18 以编程的方式定义类
- 问题:写了一段代码,需要创建一个新的类对象。想将类的定义源代码以字符串的形式发布出去。并且使用函数比如 exec() 来执行它,但是你想寻找一个更加优雅的解决方案。
- 方案:使用函数 types.new_class() 来初始化新的类对象。你需要做的只是提供
类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数。
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,
}
import types
Stock = types.new_class('Stock',(),{},lambda ns:ns.update(cls_dict))
Stock.__module__ = __name__
s = Stock("ACME",50,91.1)
s
<__main__.Stock at 0xbfdeb0>
s.cost()
4555.0
- 如果你创建的类需要一个不同的元类,可以通过设置types.new_class()的第三个参数
import abc
Stock = types.new_class('Stock',(),{'metaclass': abc.ABCMeta},lambda ns: ns.update(cls_dict))
Stock.__module__ = __name__
Stock
__main__.Stock
type(Stock)
abc.ABCMeta
- 第三个参数还可以包含其他的关键字参数:
# class Spam(Base,debug=True, typecheck=False):
# pass
# 上面的代码相当于
# Spam = types.new_class('Spam',Base,{'debug'= True, 'typecheck' = False},lambda ns:ns.update(cls_dict))
-
new_class() 第四个参数是一个用来接受类命名空间的映射对象的函
数。通常这是一个普通的字典,但是它实际上是 __prepare__() 方法返回的任意对象, -
也可以调用collections.namedtuple()
import collections
Stock = collections.namedtuple('Stock',['name', 'shares','price'])
Stock
__main__.Stock
import operator
import types
import sys
def named_tuple(classname, fieldnames):
cls_dict = {name:property(operator.itemgetter(n)) for n, name in enumerate(fieldnames)}
def __new__(cls, *args):
if len(args) != len(fieldnames):
raise TypeError("Excepted {} arguments".format(len(fieldnames)))
return tuple.__new__(cls,args)
cls = types.new_class(classname, (tuple,),{},lambda ns:ns.update(cls_dict))
cls.__module__ = sys._getframe(1).f_globals['__name__']
return cls
- 上面的代码中使用了一个所谓的“框架魔法”,通过sys._getframe() 来获取调用者的模块名
Point = named_tuple("point", ['x','y'])
Point
__main__.point
p = Point((4,5))
len(p)
2
p.x
4
p.y
5
p.x = 2
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-12-6bf7f157f4bc> in <module>()
----> 1 p.x = 2
AttributeError: can't set attribute
print('%s %s'%p )
4 5