实现单例的另一种方法

上次实现单例用到了__new__,在创建实例的时候检测是否已经有创建过实例,如果有,则将其拿过来直接使用。这次是使用元类来实现,在之前的 类元编程 中提到过类是元类的实例,而type类是大部分类的元类,因此元类的__call__方法,就会在调用指定类(元类的实例)的实例化方法的时候被调用。

比如:

1
2
3
4
5
6
7
8
9
10
class NoInstances(type):
def __call__(self, *args, **kwargs):
raise TypeError("Can't instantiate directly")
# Example
# Spam是NoInstances的实例,所以Spam()会调用NoInstances的__call__方法
class Spam(metaclass=NoInstances):
@staticmethod
def grok(x):
print('Spam.grok')

这样,用户只能调用这个类的静态方法或者类方法,而不能创建它的实例。

如果是创建单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
# Example
class Spam(metaclass=Singleton):
def __init__(self):
print('Creating Spam')

也可以根据不同的参数创建缓存实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import weakref
class Cached(type):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cache = weakref.WeakValueDictionary()
def __call__(self, *args):
if args in self.__cache:
return self.__cache[args]
else:
obj = super().__call__(*args)
self.__cache[args] = obj
return obj
# Example
class Spam(metaclass=Cached):
def __init__(self, name):
print('Creating Spam({!r})'.format(name))
self.name = name

缓存实例

如果有同样的参数,那么返回的是同一个对象,即它的缓存引用。比如logging模块,相同参数创建的对象是单例的。

1
2
3
4
5
6
7
8
>>> import logging
>>> a = logging.getLogger('foo')
>>> b = logging.getLogger('bar')
>>> a is b
False
>>> c = logging.getLogger('foo')
>>> a is c
True

只需要使用一个和类本身分开的工厂函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# The class in question
class Spam:
def __init__(self, name):
self.name = name
# Caching support
import weakref
_spam_cache = weakref.WeakValueDictionary()
def get_spam(name):
if name not in _spam_cache:
s = Spam(name)
_spam_cache[name] = s
else:
s = _spam_cache[name]
return s

这里使用弱引用技术是因为,保存缓存实例的时候,只需在程序使用到它的时候才保存,也就是只有在其他地方还在使用的时候才会保存这个实例,比如上面实例化的logger,如果删除这个logger,del a,那么字典中就会移除这个实例。如果是普通字典的话,就不会删除这个key对应的实例。也就是说,创建的这个弱引用字典,在垃圾回收的时候不计算引用。

捕获类的属性定义顺序

自动记录一个类中属性和方法定义的顺序,使用元类来捕获类的定义信息。使用这个元类来创建类,通过在元类生成类对象之前,将类的属性字典变成有序字典,这样在__new__方法中就可以获取属性定义的顺序了。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Metaclass that uses an OrderedDict for class body
class OrderedMeta(type):
def __new__(cls, clsname, bases, clsdict):
# 通过clsname,bases,clsdict,创建一个类
d = dict(clsdict)
order = []
for name, value in clsdict.items():
if isinstance(value, Typed):
value._name = name
order.append(name)
# 属性的顺序记录在类属性_order中
d['_order'] = order
# 必须使用正确的dict实例,因此用d而不是clsdict
return type.__new__(cls, clsname, bases, d)
@classmethod
def __prepare__(cls, clsname, bases):
return OrderedDict()

prepare方法只能在元类中使用,必须声明为类方法,在调用__new__之前会先调用__prepare__方法,使用类定义体中的属性创建映射。

下面是用法:

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
# A set of descriptors for various types
class Typed:
_expected_type = type(None)
def __init__(self, name=None):
self._name = name
def __set__(self, instance, value):
if not isinstance(value, self._expected_type):
raise TypeError('Expected ' + str(self._expected_type))
instance.__dict__[self._name] = value
class Integer(Typed):
_expected_type = int
class Float(Typed):
_expected_type = float
class String(Typed):
_expected_type = str
class Structure(metaclass=OrderedMeta):
def as_csv(self):
return ','.join(str(getattr(self,name)) for name in self._order
# Example use
class Stock(Structure):
name = String()
shares = Integer()
price = Float()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price

定义有可选参数的元类

创建一个抽象基类:

1
2
3
4
5
6
7
8
9
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self, maxsize=None):
pass
@abstractmethod
def write(self, data):
pass

除了metaclass也可以提供其他的关键字参数:

1
2
class Spam(metaclass=MyMeta, debug=True, synchronize=True):
pass

要实现这种效果,即元类支持这些关键字参数,必须在__prepare__()__new__()__init__()方法中都强制使用关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyMeta(type):
# Optional
@classmethod
def __prepare__(cls, name, bases, *, debug=False, synchronize=False):
# Custom processing
pass
return super().__prepare__(name, bases)
# Required
def __new__(cls, name, bases, ns, *, debug=False, synchronize=False):
# Custom processing
pass
return super().__new__(cls, name, bases, ns)
# Required
def __init__(self, name, bases, ns, *, debug=False, synchronize=False):
# Custom processing
pass
super().__init__(name, bases, ns)

额外的这些关键字参数会传递给上面每一个方法,__prepare__()会在所有类定义开始之前执行,用来创建类命名空间,一般是一个字典,然后所有的类定义完成之后,会调用__new__()方法,用来实例化最终的类对象。最后调用__init__()方法,用来执行其他的初始化工作。

使用关键字配置一个元类是对类变量的一种替代方式:

1
2
3
4
class Spam(metaclass=MyMeta):
debug = True
synchronize = True
pass

使用元类是不会污染类的名称空间,因为这些属性仅仅是在类创建阶段使用,而不是类中的语句执行阶段。而且在元类中,这些参数可以在__prepare__()方法中访问,而类变量不可以。

*args**kwargs的强制参数签名

对涉及到操作函数调用签名的问题,应该使用inspect模块中的签名特性。主要是SignatureParameter

将一个签名对象和*args**kwargs绑定起来,这样既能有*的通配性,也能对传递进来的参数进行检查。

1
2
3
4
5
6
7
8
9
from inspect import Signature, Parameter
# Make a signature for a func(x, y=42, *, z=None)
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)
def func(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
print(name,value)

这样可以强制函数调用遵循特定的规则,比如必填,默认,重复等。比如下面的例子,强制所有的子类必须提供一个特定的参数签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
# Example use
class Stock(Structure):
__signature__ = make_sig('name', 'shares', 'price')
class Point(Structure):
__signature__ = make_sig('x', 'y')

通过元类来创建签名对象:

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
from inspect import Signature, Parameter
def make_sig(*names):
parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
for name in names]
return Signature(parms)
class StructureMeta(type):
def __new__(cls, clsname, bases, clsdict):
clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[]))
return super().__new__(cls, clsname, bases, clsdict)
class Structure(metaclass=StructureMeta):
_fields = []
def __init__(self, *args, **kwargs):
bound_values = self.__signature__.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
setattr(self, name, value)
# Example
class Stock(Structure):
_fields = ['name', 'shares', 'price']
class Point(Structure):
_fields = ['x', 'y']

在类上强制使用编程规约

可以通过元类来监控类的定义过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyMeta(type):
def __new__(self, clsname, bases, clsdict):
# clsname是要定义的类名
# bases是这个类的基类
# clsdict是类字典
return super().__new__(cls, clsname, bases, clsdict)
# 或者是init方法
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

这样就可以在定义类的时候检查类的内容,比如类字典,父类等。

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
from inspect import signature
import logging
class MatchSignaturesMeta(type):
def __init__(self, clsname, bases, clsdict):
super().__init__(clsname, bases, clsdict)
# 这里的self,就是类,因为类是元类的实例,因此self就是类对象
sup = super(self, self)
for name, value in clsdict.items():
if name.startswith('_') or not callable(value):
continue
# 获取当前类可调用的对象,即方法
# 这里获取父类中相同方法的定义,有可能是None
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

在大型面向对象的程序中,通常将类的定义放在元类中控制是很有用的。 元类可以监控类的定义,警告编程人员某些没有注意到的可能出现的问题。__new__() 方法在类创建之前被调用,通常用于通过某种方式(比如通过改变类字典的内容)修改类的定义。 而 __init__() 方法是在类被创建之后被调用,当你需要完整构建类对象的时候会很有用。

以编程方式定义类

可以将类的定义源代码以字符串的形式发布出去,然后用exec()来执行它。比如namedtuple的实现。其核心如下:

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
# _class_template是一段类定义的字符串,使用下面进行渲染
class_definition = _class_template.format(
typename = typename,
field_names = tuple(field_names),
num_fields = len(field_names),
arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
repr_fmt = ', '.join(_repr_template.format(name=name)
for name in field_names),
field_defs = '\n'.join(_field_template.format(index=index, name=name)
for index, name in enumerate(field_names))
)
# 这里的namespance当然也可以是{}
# 但是必须给,要不然就会使用当前的作用域
# 给定了__name__参数,其实就是默认的当前模块名,为了防止后面没法获取到模块名(_getframe(1)调用失败)
namespace = dict(__name__='namedtuple_%s' % typename)
# 通过exec将类定义加载出来,namespace就是所在的模块,即命名空间
exec(class_definition, namespace)
# result就是在这个命名空间下的类
result = namespace[typename]
# 后面需要将定义的类的module改变成调用的module
# 如果是被调用,则module名是模块名,如果是直接在本模块执行则module名是__main__
if module is None:
try:
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
if module is not None:
result.__module__ = module

sys._getframe(1)返回调用者的栈帧,可以从中访问属性f_locals来获得局部变量,f_globals来获得全局变量。f_locals 是一个复制调用函数的本地变量的字典。 尽管你可以改变 f_locals 的内容,但是这个修改对于后面的变量访问没有任何影响。所以,虽说访问一个栈帧看上去很邪恶,但是对它的任何操作不会覆盖和改变调用者本地变量的值。

exec(object[, globals[, locals]])

这个函数支持动态的执行Python代码。object必须是string或者是一个代码对象。如果是string,string会被解析成一系列的Python语句去执行。如果是一个代码对象(code object),那么就会简单的执行。在所有情况,要执行的code都会被期望是一个有效的文件输入。要注意本函数不会返回任何值,不管函数或语句有任何的返回值语句,比如return或yield语句。该函数返回值是None。

在所有的情况中,如果可选的参数被忽略,那么代码将会在当前作用域中执行。如果只有globals参数给定,它必须是一个字典,将会被同时用作global和local变量。如果globals和locals同时给定,那么它们将会对应的用作global变量和local变量。如果给定的话,locals可以是任何的mapping对象。记住在模块层级,globals和locals是同样的字典。如果exec得到了两个不同的对象作为globals和locals,代码将会被当作好像它被嵌入到一个类定义中一样执行。

如果globals字典没有包含key __builtins__,那么一个内置模块builtins的字典引用将会置为这个key的值。这样你可以通过插入自己的__builtins__字典到globals中来控制要执行的代码中哪些builtins是可用的。

内置函数globals()locals()返回当前的global和local字典,可以传递到exec函数作为第二个和第三个参数。

locals的默认行为:不应该修改默认的locals字典。传递一个显式的locals字典,如果你需要在exec函数返回后看到locals字典的变化。

使用types.new_class()来初始化新的类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
# 类名,基类元组,关键字参数(元类),用成员变量填充类字典的回调函数
# 用来接受类命名空间的映射对象的函数, 实际上是__prepare__() 方法返回的任意对象,也就是这个函数接收__prepare__()方法返回的映射对象
Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))
# 类所在的module为当前模块
Stock.__module__ = __name__
# 使用元类参数
Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta},
lambda ns: ns.update(cls_dict))

使用元类技术实现namedtuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import operator
import types
import sys
def named_tuple(classname, fieldnames):
# Populate a dictionary of field property accessors
cls_dict = { name: property(operator.itemgetter(n))
for n, name in enumerate(fieldnames) }
# Make a __new__ function and add to the class dict
def __new__(cls, *args):
if len(args) != len(fieldnames):
raise TypeError('Expected {} arguments'.format(len(fieldnames)))
return tuple.__new__(cls, args)
cls_dict['__new__'] = __new__
# Make the class
cls = types.new_class(classname, (tuple,), {},
lambda ns: ns.update(cls_dict))
# Set the module to that of the caller
cls.__module__ = sys._getframe(1).f_globals['__name__']
return cls

也可以通过直接实例化一个元类来创建一个类,Stock = type('Stock', (), cls_dict)

在定义的时候初始化类的成员

元类会在定义的时候被触发,这时候可以做一些额外操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import operator
class StructTupleMeta(type):
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
# 在类定义的时候设置属性
for n, name in enumerate(cls._fields):
setattr(cls, name, property(operator.itemgetter(n)))
class StructTuple(tuple, metaclass=StructTupleMeta):
_fields = []
def __new__(cls, *args):
if len(args) != len(cls._fields):
raise ValueError('{} arguments required'.format(len(cls._fields)))
return super().__new__(cls,args)
class Stock(StructTuple):
_fields = ['name', 'shares', 'price']
class Point(StructTuple):
_fields = ['x', 'y']

这样就形成了一个具名元组,函数 operator.itemgetter() 创建一个访问器函数, 然后 property() 函数将其转换成一个属性。