Dec
6
友情提示:本文不一定适合阅读,如果执意要读,请备好晕车药。
== 题记 ==
"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."
-- Tim Peters
== 起因 ==
这句话听起来就很诱人,曾经试图去理解它,但是因为没有实际的需求,就因为烧脑子而放弃了。不妨摘录一段Python document里关于metaclass的概述,简直就是绕口令:
昨天心血来潮想写一个带class initializer的class,发现绕不过metaclass了,于是又翻出来看。
== 概述 ==
其实是要理解metaclass的本质,无非是要时刻牢记两点:1. Python中一切皆对象; 2. class也是一个对象,它的class就是metaclass。
举例来说:
其中第一个print很好理解:a是一个A的实例,有自己的id(其实就是内存地址)、a的class是A。
第二个print就有点烧脑子了:A是一个class,也有自己的id(因为A也是一个对象,虽然print出来的时候没有明确说),A的class是type。
而第三个就晕乎了:type是一个type,也有自己的id(因为type也是一个对象),type的class是type,也就是它自己。
再回想上面提到的两点:A是一个对象,它的class是metaclass。也就是说 type 是一个metaclass,而A类是type类的一个对象。
唉,本来想好好解释的,没想到还是说成绕口令了。算了,反正我懂了,继续。
== type ==
没有仔细了解type是什么的同学可能会以为type是一个函数:type(X)用于返回X的类对象。
然而并不完全是这样的:在python里,X(args)可能是调用一个函数,也可能是在实例化一个X的对象——而很不幸地,type(X)实际上是介于二者之间的一个调用:虽然type是一个class,但是它的__call__方法是存在的,于是python把它当成一个函数来调用,实际调用到了源码中的type_call;type_call调用了type.__new__试图初始化一个type类的实例,然而type.__new__(位于源码中的type_new函数)发现卧槽居然只有一个参数,于是就返回了这个参数的type(源码是这么写的:"return (PyObject *) Py_TYPE(x);",并没有生成新的对象)。也就是说,本来是个函数调用,里面却是要初始化一个对象,然而最后返回的却不是初始化的对象!尼玛那个特殊情况为毛不放到函数调用里面啊,开发者脑抽了吗!
感到脑抽的同学可以暂时忽略上面那段话,跟本文没太大关系。继续。
实际上type是在builtin模块中定义,指向源码中PyType_Type对象的一个引用:
这个PyType_Type又是个什么鬼?好吧,继续贴源码
注意2点:
0. PyType_Type,也就是python里的type,是在源码中生成的一个对象;这个对象的类型是PyTypeObject,所以它恰好又是一个类,至于你信不信,反正我信了。后面我把它叫做类对象,注意:不是类的对象,而是类本身是一个对象。
1. PyVarObject_HEAD_INIT递归引用了自己(PyType_Type)作为它的type(在源码中,指定某个对象的type为X,就是指定了它在python环境中的class为X),所以前面第三个print中可以看到,type(type) == type(哈哈哈,写绕口令真好玩)
2. 在PyType_Type的定义指定了 tp_init = type_init 和 tp_new = type_new 这两个属性值。这是两个函数,也位于源码中的Object/typeobject.c。
关于第2点,在Python document中关于__new__方法的说明里有详细的介绍,这里简单总结一下:在new一个对象的时候,只会调用这个class的__new__方法,它需要生成一个对象、调用这个对象的__init__方法对它进行初始化,然后返回这个对象。
好吧,我发现不得不把简单总结展开,否则确实说不清楚。
== 实例化 ==
这是一个很有意思的设计:把实例化的流程暴露给码农,意味着码农可以在对象的生成前、生成后返回前两个环节对这个对象进行修改(【甚至】在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!不过在document里建议,如果要覆盖__new__方法,那么【应当】返回这个class的父类的__new__方法返回的对象)。这里还有一个非常tricky的地方:虽然没有明确指定,但是__new__方法被硬编码为一个staticmethod(有兴趣的话可以去翻type_new函数),它的第一个参数是需要被实例化的class,其余参数则是需要传给__init__的参数。
说起来非常枯燥,还是举一个例子吧,就用document里给出的Singleton:
代码并不复杂,但是可能有点玄乎,需要理解一下那个cls参数,前面说了,它是需要被实例化的class,也就是说,最后一行实际执行的是:
而DbConnection的__new__方法直接继承于Singleton, 所以实际调用的是
主要注意的地方,在上面这段代码的第六行,Singleton是继承于object(这里特指python中的那个object对象),因此调用了object.__new__(DbConnection)来生成一个对象,生成过程位于C源码中的object_new函数(Objects/typeobject.c),它会将新生成对象的type指定为DbConnection,然后直接返回。
Singleton.__new__在拿到了生成的DbConnection实例以后,将它保存在了DbConnection类的__it__属性中,然后对该实例进行初始化,最后返回。
可以看到,任何继承于Singleton类的子类,只要不覆盖其__new__方法,每个类永远只会被实例化一次。
好了,第2点暂告一段落,接下来回归正题,尼玛我都快忘了要讲的是metaclass啊。
== metaclass ==
还记的上面可以暂时忽略的那段话吗?type(X)是试图实例化type对象,但是因为只有一个参数,所以源码中只是返回了X的类。而type的标准初始化参数应当有三个:class_name, bases, attributes。最前面那个"class A(object): pass",python解释器实际的流程是:
1. 解析这段代码,得知它需要创建一个【类对象】,这个类的名字叫做'A', 它的父类列表(用tuple表示)是 (object,),它的属性用一个dict来表示就是 {} 。
2. 查找用于生成这个类的metaclass。(终于讲到重点了有木有!)
查找过程比较蛋疼,位于Python/ceval.c : build_class函数,按顺序优先采用以下几个:
2.1 定义中使用 __metaclass__ 属性指定的(本例:没有)
2.2 如果有父类,使用第一个父类的 __class__ 属性,也就是父类的metaclass(本例:object的class,也就是type)
2.2.1 如果第一个父类没有 __class__ 属性,那就用父类的type(这是针对父类没有父类的情况)
2.3 使用当前Globals()中的 __metaclass__ 指定的(本例:没有,不过2.2里已经找到了)
2.4 使用PyClass_Type
注:2.2.1和2.4中提到了没有父类,或者父类没有父类的情形,这就是python中的old-style class,在python2.2之前所有的对象都是这样的,而2.2之后可以继承于object类,就变成了new-style class。这种设计保持了向后兼容。
3. 使用metaclass来创建这个A类。由于A类的class就是metaclass,所以这个过程其实就是实例化metaclass的过程。本例中找到的metaclass是type,所以最终python执行的相当于这一句:
再回想一下前面提到的实例化过程,实际上这一句分成两步: 1. 调用type.__new__(type, 'A', (object,), {})生成type的一个实例(也就是A类对象);2. 调用type.__init__(A, 'A', (object,), {}) 对A类对象进行初始化。注意:这里调用的是type.__init__,而不是A.__init__:因为A是type的一个实例。
流程终于解释完啦,不过我觉得还是举个栗子会比较好。就用我看到的那个有点二二的栗子吧:定义一个class,把它的所有属性都改成全大写的。我感觉这个栗子唯一的作用就是用来当栗子了。还好还有这个作用,否则连出生的机会都没有。
== 栗子 ==
直接上代码好了:
请不要说“说好的metaclass呢!怎么变成了一个函数!我摔!”,回顾一下最最前面提到的一点:everything is an object in python。upper_meta作为一个函数,它也是一个对象啊。而metaclass也不过就是个对象,并没有本质上的差别——只要它被call的时候能接受name, bases, attrs这三个参数并返回一个类对象就行了。duck-typing的语言用起来就是有这样的一种不可言状的酸爽感。
理解了这一点,这段代码就能理解了,upper_meta返回了一个type类的实例——也就是Foo类,并且可以看到print出来的属性里头只有HELLO而没有hello。
考虑到可能有人不满意,想看使用class来作为metaclass的情形,我就勉为其难换个姿势再举一下这个栗子(真累)。
写的太长了,换了一个短一点的oneliner,但是效果不变(其实我就是想炫一下,不服来咬我呀)。
这段代码虽然形式上跟前面的upper_meta函数不一样,但是本质是一样的:调用了upper_meta('Foo', (object,), {'hello': 'world'}),生成了一个新的名为Foo的类对象。
理论上,故事讲到这里应该结束了,然而我想说,压轴戏还没上呢。
== 压轴戏 ==
我要把这栗子举得更高更远,也更符合实际开发的需求:继承。
这段代码太简单了,但是埋在下面的逻辑却太复杂了。
它的输出并不是{'HI': 'there'}, 而是{'hi': 'there'}。你print Bar.HELLO, Bar.__metaclass__都能得到预期的输出,但是偏偏没有HI,只有hi。
为什么?这真是个烧脑细胞的事情。我已经把所有的逻辑都展现出来了,甚至还做了特别的标记。然而即便如此,想要把这个逻辑理顺,也是一件非常有挑战性的事情,幸好我已经想明白了:苦海无涯,回头是岸。啊呸,应该是——学海无涯苦作舟,不想明白不回头。
我想说“甚至还做了特别标记”这句话的意思是,我还给【甚至】这两个字做了特别标记:在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!
问题的关键就在这里:前面两个栗子中给出的upper_meta,返回的并不是upper_meta的实例,而是type的实例,而是type的实例,而是type的实例。重说三。
什么意思?再看看代码,最后return的是type(name, bases, attrs),也就是说,Foo类对象并不是upper_meta的实例,而是type的实例(也就是说:虽然指定并被使用的metaclass是upper_meta,但是最终创建出来的Foo类的metaclass是type)。不信你print type(Foo)试试,结果就是type,而不是upper_meta。
为什么这会导致继承于Foo类的Bar类不能由upper_meta来搭建?Bar.__metaclass__不还是upper_meta吗?
这个问题就没有那么困难了,有兴趣的同学可以自己试着分析一下,没兴趣的大概也不会有耐心看到这里吧。
Bar.__metaclass__并不是Bar的原生属性,而是继承于Foo的——所以在print Bar.__dict__的时候看不到__metaclass__。也就是说,在试图创建Bar时,attrs里并没有__metaclass__属性,所以并不会直接采用upper_meta。再回顾一下选择metaclass的顺序就可以发现,实际上在2.2里会选择Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。
解决方法很简单:关键就是前面被特别标记了的【应当】返回这个class的父类的__new__方法返回的对象。具体到代码应当是这样:
新增的__init__方法并不是必须的,有兴趣的同学可以跟上面的栗子对比一下,由于前面返回的是type类的实例,调用到的是type.__init__;而这样正确的写法就会调用到upper_meta.__init__。(p.s. super也是烧脑细胞的东西,但用于解决钻石继承的问很有意思,有兴趣的同学可以看看Cooperative methods and "super")
果然很烧脑细胞吧。
关于metaclass的选择,还有另外一个坑:在metaclass 2.3提到了,找不到metaclass的情况下,会使用Globals()中定义的__metaclass__属性指定的元类来创建类,那么为什么下面的代码却没有生效呢?
== class initializer ==
回到我最初的需求:我需要创建带class initializer的类。为什么会有这样的需求?最常见的metaclass的应用场景是对数据库的封装。举例来说,我希望创建一个Table类,所有表都是继承于这个类,同时我还想给每一个表都设置一个缓存dict(使用主键作为key缓存查询结果)。一个很自然的想法是这样的:
可惜这是错的。从输出结果就能看出来,返回的是一个Grade对象,而不是预期的Student对象。原因很简单:子类们并不直接拥有_pk_cache ,它们访问的是Table的_pk_cache ,而该dict只被初始化了一次。
当然,我可以在每一个继承于Table的class里新增一句 _pk_cache = {},但是这样的实现太丑了,而且一不注意就会漏掉导致出错。
所以我需要一个class initializer,在class被创建的时候,给它新增一个_pk_cache 。
在搞清楚了metaclass之后,解决方法特别简单:
完。(终于完结了,我写了一整个下午啊...)
== 题记 ==
"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."
-- Tim Peters
== 起因 ==
这句话听起来就很诱人,曾经试图去理解它,但是因为没有实际的需求,就因为烧脑子而放弃了。不妨摘录一段Python document里关于metaclass的概述,简直就是绕口令:
引用
Terminology-wise, a metaclass is simply "the class of a class". Any class whose instances are themselves classes, is a metaclass. When we talk about an instance that's not a class, the instance's metaclass is the class of its class: by definition, x's metaclass is x.__class__.__class__. But when we talk about a class C, we often refer to its metaclass when we mean C.__class__ (not C.__class__.__class__, which would be a meta-metaclass; there's not much use for those although we don't rule them out).
昨天心血来潮想写一个带class initializer的class,发现绕不过metaclass了,于是又翻出来看。
== 概述 ==
其实是要理解metaclass的本质,无非是要时刻牢记两点:1. Python中一切皆对象; 2. class也是一个对象,它的class就是metaclass。
举例来说:
>>> class A(object): pass
...
>>> a = A()
>>> print (a, id(a), type(a))
(<__main__.A object at 0xb183d0>, 11633616, <class '__main__.A'>)
>>> print (A, id(A), type(A))
(<class '__main__.A'>, 11991040, <type 'type'>)
>>> print (type, id(type), type(type))
(<type 'type'>, 1891232, <type 'type'>)
...
>>> a = A()
>>> print (a, id(a), type(a))
(<__main__.A object at 0xb183d0>, 11633616, <class '__main__.A'>)
>>> print (A, id(A), type(A))
(<class '__main__.A'>, 11991040, <type 'type'>)
>>> print (type, id(type), type(type))
(<type 'type'>, 1891232, <type 'type'>)
其中第一个print很好理解:a是一个A的实例,有自己的id(其实就是内存地址)、a的class是A。
第二个print就有点烧脑子了:A是一个class,也有自己的id(因为A也是一个对象,虽然print出来的时候没有明确说),A的class是type。
而第三个就晕乎了:type是一个type,也有自己的id(因为type也是一个对象),type的class是type,也就是它自己。
再回想上面提到的两点:A是一个对象,它的class是metaclass。也就是说 type 是一个metaclass,而A类是type类的一个对象。
唉,本来想好好解释的,没想到还是说成绕口令了。算了,反正我懂了,继续。
== type ==
没有仔细了解type是什么的同学可能会以为type是一个函数:type(X)用于返回X的类对象。
然而并不完全是这样的:在python里,X(args)可能是调用一个函数,也可能是在实例化一个X的对象——而很不幸地,type(X)实际上是介于二者之间的一个调用:虽然type是一个class,但是它的__call__方法是存在的,于是python把它当成一个函数来调用,实际调用到了源码中的type_call;type_call调用了type.__new__试图初始化一个type类的实例,然而type.__new__(位于源码中的type_new函数)发现卧槽居然只有一个参数,于是就返回了这个参数的type(源码是这么写的:"return (PyObject *) Py_TYPE(x);",并没有生成新的对象)。也就是说,本来是个函数调用,里面却是要初始化一个对象,然而最后返回的却不是初始化的对象!尼玛那个特殊情况为毛不放到函数调用里面啊,开发者脑抽了吗!
感到脑抽的同学可以暂时忽略上面那段话,跟本文没太大关系。继续。
实际上type是在builtin模块中定义,指向源码中PyType_Type对象的一个引用:
//位于Python/bltinmodule.c
PyObject * _PyBuiltin_Init(void)
{
...
SETBUILTIN("type", &PyType_Type);
...
}
PyObject * _PyBuiltin_Init(void)
{
...
SETBUILTIN("type", &PyType_Type);
...
}
这个PyType_Type又是个什么鬼?好吧,继续贴源码
//位于Objects/typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
...
type_init, /* tp_init */
0, /* tp_alloc */
type_new, /* tp_new */
...
};
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
...
type_init, /* tp_init */
0, /* tp_alloc */
type_new, /* tp_new */
...
};
注意2点:
0. PyType_Type,也就是python里的type,是在源码中生成的一个对象;这个对象的类型是PyTypeObject,所以它恰好又是一个类,至于你信不信,反正我信了。后面我把它叫做类对象,注意:不是类的对象,而是类本身是一个对象。
1. PyVarObject_HEAD_INIT递归引用了自己(PyType_Type)作为它的type(在源码中,指定某个对象的type为X,就是指定了它在python环境中的class为X),所以前面第三个print中可以看到,type(type) == type(哈哈哈,写绕口令真好玩)
2. 在PyType_Type的定义指定了 tp_init = type_init 和 tp_new = type_new 这两个属性值。这是两个函数,也位于源码中的Object/typeobject.c。
关于第2点,在Python document中关于__new__方法的说明里有详细的介绍,这里简单总结一下:在new一个对象的时候,只会调用这个class的__new__方法,它需要生成一个对象、调用这个对象的__init__方法对它进行初始化,然后返回这个对象。
好吧,我发现不得不把简单总结展开,否则确实说不清楚。
== 实例化 ==
这是一个很有意思的设计:把实例化的流程暴露给码农,意味着码农可以在对象的生成前、生成后返回前两个环节对这个对象进行修改(【甚至】在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!不过在document里建议,如果要覆盖__new__方法,那么【应当】返回这个class的父类的__new__方法返回的对象)。这里还有一个非常tricky的地方:虽然没有明确指定,但是__new__方法被硬编码为一个staticmethod(有兴趣的话可以去翻type_new函数),它的第一个参数是需要被实例化的class,其余参数则是需要传给__init__的参数。
说起来非常枯燥,还是举一个例子吧,就用document里给出的Singleton:
class Singleton(object):
def __new__(cls, *args, **kwargs):
it = cls.__dict__.get("__it__")
if it is not None:
return it
cls.__it__ = it = object.__new__(cls) #注意
it.__init__(*args, **kwargs)
return it
def __init__(self, *args, **kwargs):
pass
class DbConnection(Singleton):
def __init__(self, db_config):
self._connection = AnyHowToConnectBy(db_config)
conn = new DbConnection(db_config)
def __new__(cls, *args, **kwargs):
it = cls.__dict__.get("__it__")
if it is not None:
return it
cls.__it__ = it = object.__new__(cls) #注意
it.__init__(*args, **kwargs)
return it
def __init__(self, *args, **kwargs):
pass
class DbConnection(Singleton):
def __init__(self, db_config):
self._connection = AnyHowToConnectBy(db_config)
conn = new DbConnection(db_config)
代码并不复杂,但是可能有点玄乎,需要理解一下那个cls参数,前面说了,它是需要被实例化的class,也就是说,最后一行实际执行的是:
DbConnection.__new__(DbConnection, db_config)
而DbConnection的__new__方法直接继承于Singleton, 所以实际调用的是
Singleton.__new__(DbConnection, db_config)
主要注意的地方,在上面这段代码的第六行,Singleton是继承于object(这里特指python中的那个object对象),因此调用了object.__new__(DbConnection)来生成一个对象,生成过程位于C源码中的object_new函数(Objects/typeobject.c),它会将新生成对象的type指定为DbConnection,然后直接返回。
Singleton.__new__在拿到了生成的DbConnection实例以后,将它保存在了DbConnection类的__it__属性中,然后对该实例进行初始化,最后返回。
可以看到,任何继承于Singleton类的子类,只要不覆盖其__new__方法,每个类永远只会被实例化一次。
好了,第2点暂告一段落,接下来回归正题,尼玛我都快忘了要讲的是metaclass啊。
== metaclass ==
还记的上面可以暂时忽略的那段话吗?type(X)是试图实例化type对象,但是因为只有一个参数,所以源码中只是返回了X的类。而type的标准初始化参数应当有三个:class_name, bases, attributes。最前面那个"class A(object): pass",python解释器实际的流程是:
1. 解析这段代码,得知它需要创建一个【类对象】,这个类的名字叫做'A', 它的父类列表(用tuple表示)是 (object,),它的属性用一个dict来表示就是 {} 。
2. 查找用于生成这个类的metaclass。(终于讲到重点了有木有!)
查找过程比较蛋疼,位于Python/ceval.c : build_class函数,按顺序优先采用以下几个:
2.1 定义中使用 __metaclass__ 属性指定的(本例:没有)
2.2 如果有父类,使用第一个父类的 __class__ 属性,也就是父类的metaclass(本例:object的class,也就是type)
2.2.1 如果第一个父类没有 __class__ 属性,那就用父类的type(这是针对父类没有父类的情况)
2.3 使用当前Globals()中的 __metaclass__ 指定的(本例:没有,不过2.2里已经找到了)
2.4 使用PyClass_Type
注:2.2.1和2.4中提到了没有父类,或者父类没有父类的情形,这就是python中的old-style class,在python2.2之前所有的对象都是这样的,而2.2之后可以继承于object类,就变成了new-style class。这种设计保持了向后兼容。
3. 使用metaclass来创建这个A类。由于A类的class就是metaclass,所以这个过程其实就是实例化metaclass的过程。本例中找到的metaclass是type,所以最终python执行的相当于这一句:
type('A', (object,), {})
再回想一下前面提到的实例化过程,实际上这一句分成两步: 1. 调用type.__new__(type, 'A', (object,), {})生成type的一个实例(也就是A类对象);2. 调用type.__init__(A, 'A', (object,), {}) 对A类对象进行初始化。注意:这里调用的是type.__init__,而不是A.__init__:因为A是type的一个实例。
流程终于解释完啦,不过我觉得还是举个栗子会比较好。就用我看到的那个有点二二的栗子吧:定义一个class,把它的所有属性都改成全大写的。我感觉这个栗子唯一的作用就是用来当栗子了。还好还有这个作用,否则连出生的机会都没有。
== 栗子 ==
直接上代码好了:
def upper_meta(name, bases, attrs):
new_attrs = {}
for name, value in attrs.items():
if not name.startswith('__'):
new_attrs[name.upper()] = value
else:
new_attrs[name] = value
return type(name, bases, new_attrs)
class Foo(object):
__metaclass__ = upper_meta
hello = 'world'
print Foo.__dict__
new_attrs = {}
for name, value in attrs.items():
if not name.startswith('__'):
new_attrs[name.upper()] = value
else:
new_attrs[name] = value
return type(name, bases, new_attrs)
class Foo(object):
__metaclass__ = upper_meta
hello = 'world'
print Foo.__dict__
请不要说“说好的metaclass呢!怎么变成了一个函数!我摔!”,回顾一下最最前面提到的一点:everything is an object in python。upper_meta作为一个函数,它也是一个对象啊。而metaclass也不过就是个对象,并没有本质上的差别——只要它被call的时候能接受name, bases, attrs这三个参数并返回一个类对象就行了。duck-typing的语言用起来就是有这样的一种不可言状的酸爽感。
理解了这一点,这段代码就能理解了,upper_meta返回了一个type类的实例——也就是Foo类,并且可以看到print出来的属性里头只有HELLO而没有hello。
考虑到可能有人不满意,想看使用class来作为metaclass的情形,我就勉为其难换个姿势再举一下这个栗子(真累)。
class upper_meta(type):
def __new__(cls, name, bases, attrs):
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return type(name, bases, attrs)
def __new__(cls, name, bases, attrs):
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return type(name, bases, attrs)
写的太长了,换了一个短一点的oneliner,但是效果不变(其实我就是想炫一下,不服来咬我呀)。
这段代码虽然形式上跟前面的upper_meta函数不一样,但是本质是一样的:调用了upper_meta('Foo', (object,), {'hello': 'world'}),生成了一个新的名为Foo的类对象。
理论上,故事讲到这里应该结束了,然而我想说,压轴戏还没上呢。
== 压轴戏 ==
我要把这栗子举得更高更远,也更符合实际开发的需求:继承。
class Bar(Foo):
hi = 'there'
print Bar.__dict__
hi = 'there'
print Bar.__dict__
这段代码太简单了,但是埋在下面的逻辑却太复杂了。
它的输出并不是{'HI': 'there'}, 而是{'hi': 'there'}。你print Bar.HELLO, Bar.__metaclass__都能得到预期的输出,但是偏偏没有HI,只有hi。
为什么?这真是个烧脑细胞的事情。我已经把所有的逻辑都展现出来了,甚至还做了特别的标记。然而即便如此,想要把这个逻辑理顺,也是一件非常有挑战性的事情,幸好我已经想明白了:苦海无涯,回头是岸。啊呸,应该是——学海无涯苦作舟,不想明白不回头。
我想说“甚至还做了特别标记”这句话的意思是,我还给【甚至】这两个字做了特别标记:在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!
问题的关键就在这里:前面两个栗子中给出的upper_meta,返回的并不是upper_meta的实例,而是type的实例,而是type的实例,而是type的实例。重说三。
什么意思?再看看代码,最后return的是type(name, bases, attrs),也就是说,Foo类对象并不是upper_meta的实例,而是type的实例(也就是说:虽然指定并被使用的metaclass是upper_meta,但是最终创建出来的Foo类的metaclass是type)。不信你print type(Foo)试试,结果就是type,而不是upper_meta。
为什么这会导致继承于Foo类的Bar类不能由upper_meta来搭建?Bar.__metaclass__不还是upper_meta吗?
这个问题就没有那么困难了,有兴趣的同学可以自己试着分析一下,没兴趣的大概也不会有耐心看到这里吧。
Bar.__metaclass__并不是Bar的原生属性,而是继承于Foo的——所以在print Bar.__dict__的时候看不到__metaclass__。也就是说,在试图创建Bar时,attrs里并没有__metaclass__属性,所以并不会直接采用upper_meta。再回顾一下选择metaclass的顺序就可以发现,实际上在2.2里会选择Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。
解决方法很简单:关键就是前面被特别标记了的【应当】返回这个class的父类的__new__方法返回的对象。具体到代码应当是这样:
class upper_meta(type):
def __new__(cls, name, bases, attrs):
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return super(upper_meta, cls).__new__(cls, name, bases, attrs)
def __init__(cls, name, bases, attrs):
print >>sys.stderr, 'in upper_meta.__init__' #FOR TEST ONLY
def __new__(cls, name, bases, attrs):
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return super(upper_meta, cls).__new__(cls, name, bases, attrs)
def __init__(cls, name, bases, attrs):
print >>sys.stderr, 'in upper_meta.__init__' #FOR TEST ONLY
新增的__init__方法并不是必须的,有兴趣的同学可以跟上面的栗子对比一下,由于前面返回的是type类的实例,调用到的是type.__init__;而这样正确的写法就会调用到upper_meta.__init__。(p.s. super也是烧脑细胞的东西,但用于解决钻石继承的问很有意思,有兴趣的同学可以看看Cooperative methods and "super")
果然很烧脑细胞吧。
关于metaclass的选择,还有另外一个坑:在metaclass 2.3提到了,找不到metaclass的情况下,会使用Globals()中定义的__metaclass__属性指定的元类来创建类,那么为什么下面的代码却没有生效呢?
def __metaclass__(name, bases, attrs):
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return type(name, bases, attrs)
class Foo(object):
hello = 'world'
print Foo.__dict__
attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
return type(name, bases, attrs)
class Foo(object):
hello = 'world'
print Foo.__dict__
== class initializer ==
回到我最初的需求:我需要创建带class initializer的类。为什么会有这样的需求?最常见的metaclass的应用场景是对数据库的封装。举例来说,我希望创建一个Table类,所有表都是继承于这个类,同时我还想给每一个表都设置一个缓存dict(使用主键作为key缓存查询结果)。一个很自然的想法是这样的:
class Table(object):
_pk_cache = {}
@classmethod
def cache(cls, obj):
cls._pk_cache[obj.pkey()] = obj;
@classmethod
def findByPk(cls, pkey):
return cls._pk_cache[pkey]
def __init__(self, pkey, args):
self._pkey = pkey
self._args = args
type(self).cache(self)
def pkey(self):
return self._pkey
def __repr__(self):
return type(self).__name__ + ':' + repr(self._args)
class Student(Table):
pass
class Grade(Table):
pass
s1 = Student(1, 's1')
g1 = Grade(1, 'g1')
print Student.findByPk(1)
_pk_cache = {}
@classmethod
def cache(cls, obj):
cls._pk_cache[obj.pkey()] = obj;
@classmethod
def findByPk(cls, pkey):
return cls._pk_cache[pkey]
def __init__(self, pkey, args):
self._pkey = pkey
self._args = args
type(self).cache(self)
def pkey(self):
return self._pkey
def __repr__(self):
return type(self).__name__ + ':' + repr(self._args)
class Student(Table):
pass
class Grade(Table):
pass
s1 = Student(1, 's1')
g1 = Grade(1, 'g1')
print Student.findByPk(1)
可惜这是错的。从输出结果就能看出来,返回的是一个Grade对象,而不是预期的Student对象。原因很简单:子类们并不直接拥有_pk_cache ,它们访问的是Table的_pk_cache ,而该dict只被初始化了一次。
当然,我可以在每一个继承于Table的class里新增一句 _pk_cache = {},但是这样的实现太丑了,而且一不注意就会漏掉导致出错。
所以我需要一个class initializer,在class被创建的时候,给它新增一个_pk_cache 。
在搞清楚了metaclass之后,解决方法特别简单:
class TableInitializer(type):
def __new__(cls, name, bases, attrs):
attrs['_pk_cache'] = {}
return super(TableInitializer, cls).__new__(cls, name, bases, attrs)
class Table(object):
__metaclass__ = TableInitializer
... #以下不变
def __new__(cls, name, bases, attrs):
attrs['_pk_cache'] = {}
return super(TableInitializer, cls).__new__(cls, name, bases, attrs)
class Table(object):
__metaclass__ = TableInitializer
... #以下不变
完。(终于完结了,我写了一整个下午啊...)
Dec
31
手头项目中有一个模块,一般情况下需要用python将数十万条数据加载到一个dict中处理,其中每条数据是一个小的dict,整体速度稍微有点慢(毕竟python不适合处理大量琐碎的小对象),由于在性能要求范围内,所以也没怎么在意。
但是在最近的性能测试中用160w+数据来压的时候,发现性能恶化得厉害。虽然算法是线性的,但是实际运行时间却明显不对劲。增加一些log后,发现在处理过程中,每隔几万条数据就会出现一个很明显的lag,而且lag的时间越拉越长。
由于不像是算法本身的问题,初步猜测可能是python中dict的rehash带来的时间开销。但是根据一般哈希表的实现方法,lag出现得太平均,又不是很符合逻辑。
大胆假设,小心求证,翻了一下python源码,Objects/dictobject.c 中 "static int dictresize(PyDictObject *mp, Py_ssize_t minused)" 函数被多处调用,其中PyDict_SetItem的末尾的调用是:"dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used)",也就是说,在需要rehash的情况下,按4倍(少于50000个item)或2倍的规模扩大。
用下面这段代码测试1600w数据,将输出数据拷贝到Excel并生成图表,可以很明显地看出lag的出现规律与上述扩张规则非常相符。
{图一}
将上述代码稍作修改,每次插入的value是个dict,测试100w数据,生成图表,每隔10w左右产生一个lag,且lag时间越拉越长,与遇到的问题现象一致。
{图二}
因此大体可以判断问题出在大量零碎小对象上,很自然地,就联想到会不会是gc在捣蛋。查了一下,虽然Python对象内部是引用计数的管理方式,但是为了避免循环引用导致的内存泄漏,解释器还是内置了一个gc,当现有对象数量超过某个阈值以后扫描一下,看看是否可以回收一些空间。由于我们的代码中并不存在循环引用的对象,这种gc其实是没有意义的,于是把gc关掉再测:
{图三}
一条直线。
但是在最近的性能测试中用160w+数据来压的时候,发现性能恶化得厉害。虽然算法是线性的,但是实际运行时间却明显不对劲。增加一些log后,发现在处理过程中,每隔几万条数据就会出现一个很明显的lag,而且lag的时间越拉越长。
由于不像是算法本身的问题,初步猜测可能是python中dict的rehash带来的时间开销。但是根据一般哈希表的实现方法,lag出现得太平均,又不是很符合逻辑。
大胆假设,小心求证,翻了一下python源码,Objects/dictobject.c 中 "static int dictresize(PyDictObject *mp, Py_ssize_t minused)" 函数被多处调用,其中PyDict_SetItem的末尾的调用是:"dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used)",也就是说,在需要rehash的情况下,按4倍(少于50000个item)或2倍的规模扩大。
用下面这段代码测试1600w数据,将输出数据拷贝到Excel并生成图表,可以很明显地看出lag的出现规律与上述扩张规则非常相符。
begin = time.time()
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = i
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = i
{图一}
将上述代码稍作修改,每次插入的value是个dict,测试100w数据,生成图表,每隔10w左右产生一个lag,且lag时间越拉越长,与遇到的问题现象一致。
from copy import deepcopy
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = deepcopy(data)
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = deepcopy(data)
{图二}
因此大体可以判断问题出在大量零碎小对象上,很自然地,就联想到会不会是gc在捣蛋。查了一下,虽然Python对象内部是引用计数的管理方式,但是为了避免循环引用导致的内存泄漏,解释器还是内置了一个gc,当现有对象数量超过某个阈值以后扫描一下,看看是否可以回收一些空间。由于我们的代码中并不存在循环引用的对象,这种gc其实是没有意义的,于是把gc关掉再测:
from copy import deepcopy
import gc
gc.disable()
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = deepcopy(data)
import gc
gc.disable()
data = {'abcdefg': 1234, 'hijklnm': 4.0, 'opqrst': 'uvwxyz'}
begin = time.time()
i = 0
d = {}
while True:
i += 1
if i % 50000 == 0:
print '%d\t%.4f' % (i, time.time() - begin)
d[i] = deepcopy(data)
{图三}
一条直线。
Nov
30
最近项目上有需要,大概就是有一个list里的东西需要处理,例如一堆文件什么的,于是有一个file_processor——按顺序处理一个文件列表。简单封装了一下multiprocessing这个库,发现用起来很方便,很轻松地就实现了多进程并行处理(进程间无交互):
multiprocess.py 则是这样的:
import multiprocess
slices = multiprocess.split_list(filelist, 8) #分成8份
processes = map(lambda slice: multiprocess.spawn(file_processor, slice), slices)
sys.exit(multiprocess.start_and_join(processes))
slices = multiprocess.split_list(filelist, 8) #分成8份
processes = map(lambda slice: multiprocess.spawn(file_processor, slice), slices)
sys.exit(multiprocess.start_and_join(processes))
multiprocess.py 则是这样的:
#!/usr/bin/python
#coding: utf-8
import sys
from multiprocessing import Process
def split_list(data, n_slice, hash_func=lambda i, d: i): #default: sequential
slices = []
for i in range(n_slice):
slices.append([])
for i, d in enumerate(data):
slices[hash_func(i, d) % n_slice].append(d)
return slices
def spawn(target, *args, **kwargs):
return Process(target=target, args=args, kwargs=kwargs)
def start_and_join(processes, killall_if_fail=True):
for p in processes:
p.start()
exitcode = 0
for p in processes:
p.join()
if p.exitcode != 0:
exitcode = p.exitcode
break
if exitcode != 0:
for p in processes:
if killall_if_fail and p.is_alive():
p.terminate()
return exitcode
#coding: utf-8
import sys
from multiprocessing import Process
def split_list(data, n_slice, hash_func=lambda i, d: i): #default: sequential
slices = []
for i in range(n_slice):
slices.append([])
for i, d in enumerate(data):
slices[hash_func(i, d) % n_slice].append(d)
return slices
def spawn(target, *args, **kwargs):
return Process(target=target, args=args, kwargs=kwargs)
def start_and_join(processes, killall_if_fail=True):
for p in processes:
p.start()
exitcode = 0
for p in processes:
p.join()
if p.exitcode != 0:
exitcode = p.exitcode
break
if exitcode != 0:
for p in processes:
if killall_if_fail and p.is_alive():
p.terminate()
return exitcode
Jun
15
挺早之前就有设想过这样一个Python模块,基于共享内存(Linux下的mmap)的哈希表实现,使得多个Python进程之间可以方便地共享一些数据。
某天心血来潮,花了几个小时居然写出来了,然后花了两天时间陆陆续续修补完善。
由于偷懒,没有做信号量/锁之类的东西,所以只适合一些简单的场景。
性能还凑合,但是没经过正经的测试和实际使用,仅供参考。
pypi页:https://pypi.python.org/pypi/pyshmht
Github:https://github.com/felix021/pyshmht
某天心血来潮,花了几个小时居然写出来了,然后花了两天时间陆陆续续修补完善。
由于偷懒,没有做信号量/锁之类的东西,所以只适合一些简单的场景。
性能还凑合,但是没经过正经的测试和实际使用,仅供参考。
pypi页:https://pypi.python.org/pypi/pyshmht
Github:https://github.com/felix021/pyshmht
May
30
好久没写Blog了,今天来发一段带有量子效应的冷代码。
我们知道在杨氏双缝实验里面,观测与否会导致迥异的实验结果——即使是在电子通过双缝以后再观测也一样。
这段代码也有类似的效应:相信你注意到了,第6/7行被我注释掉了。你可以理解吗?是否注释这两行,会导致第5行print的结果不同哟。
我们知道在杨氏双缝实验里面,观测与否会导致迥异的实验结果——即使是在电子通过双缝以后再观测也一样。
这段代码也有类似的效应:相信你注意到了,第6/7行被我注释掉了。你可以理解吗?是否注释这两行,会导致第5行print的结果不同哟。
a = 1
def foo():
locals().update(globals())
print locals()
#print a
#a += 1
foo()
def foo():
locals().update(globals())
print locals()
#print a
#a += 1
foo()
Aug
22
本篇来自这个问题:python中创建父类对象的问题
也许可以认为这是Python设计的缺陷导致的,因为Python 3.0只需要用 `super().some_attr` 就行了。至于为什么需要两个参数,可以大概分析一下,如果有错,欢迎指正。
先介绍下背景知识:super是从Python 2.2开始随着 `new-style class` 一起引入的,也只能应用于所谓的 `new-style class` (即直接或间接继承于 `object` 的class),可以在一定程度上解决所谓`钻石继承`的问题。2.2起所有python内置class都是new-style class。
那么super是什么呢?实际上它是一个builtin的type,你调用 `super(Base, self)` 会返回一个 `__class__ = super` 的对象,这个对象可以作为一个代理,通过BFS的顺序(实际上列表已经保存在`Base.__mro__`里了)去访问第一个**直接定义**了目标属性的基类里的属性。`super(type, obj_or_subtype).attr` 基本上可以认为是 `find_in_mro_provide_by(obj_or_subtype, "attr")` ,有点晦涩。
**注意**,super()返回的不是“父类对象”,而是一个super类型的对象;实例化的时候,第一个参数是一个类型(type),第二个参数可以是type的instance,也可以是type的subclass。
介绍完基础知识,可以先说说为什么要有第二个参数。理由很简单 —— 我们知道 self 代表当前对象,每个对象的方法在定义的时候都需要显示地把self作为第一个参数,你本来应该写成这样
some_class.some_method(obj, *args, **kwargs)
但是因为Python语法允许 `obj.some_metod(*args, **kwargs)` (本质上是个语法糖) ,所以你可以写得简单点(不必显式地给出方法的第一个参数)。而super对象则不同,它没有语法上的直接支持,所以在内部invoke some_method的时候必须指定某个对象,而这个对象的得你自己塞给它,也就是说把这个super对象绑定(bound)到第二个参数上。所以实际上并不是非要用self作为super的第二个参数,甚至super并不是必须在class内部才能调用:
至于为什么要有第一个参数,如果你看了 `help(super)` 就会发现它居然提供了个单参数的版本:
super(type) -> unbound super object
这个理解起来比较困难点。先说这个`unbound`,没绑定,就是说这个super对象没有实际绑定到某个对象上,它并不是可以直接用的。要怎么用呢?那就得注意到`help(super)`里面有一个 `__get__` 方法,也就是说,super类还是一个Descriptor类!哎,这个Descriptor类展开又是好大一段,简单地说就是它可以把自己和某个object的属性绑定,使得访问这个属性的时候,实际上是在调用它的`__get__/__set__`方法:
回到unbound super, 为了使用它,就要通过 __get__ 方法把它再绑定到一个对象上。由于Descriptor的属性,特别适合这么用:
这是它的一个可能用法。我不确定是否还有其他更合适的用途,但是在这里实际上也并不是很好,尤其是遇到staticmethod的时候还会出错,再加上绕了这么大一个弯,实在不是很推荐使用。
据说unbound super的使用非常少,不知道现实意义有多少,也不知道当初为什么设计成这个样子,总之由于Python3.0已经改了(虽然仍然保留了unbound super),所以基本上还是可以认为这是设计的历史遗留问题。
参考文献(unbound super主要参考了这个系列):Things to Know About Python Super
[1] http://www.artima.com/weblogs/viewpost.jsp?thread=236275
[2] http://www.artima.com/weblogs/viewpost.jsp?thread=236278
[3] http://www.artima.com/weblogs/viewpost.jsp?thread=237121
也许可以认为这是Python设计的缺陷导致的,因为Python 3.0只需要用 `super().some_attr` 就行了。至于为什么需要两个参数,可以大概分析一下,如果有错,欢迎指正。
先介绍下背景知识:super是从Python 2.2开始随着 `new-style class` 一起引入的,也只能应用于所谓的 `new-style class` (即直接或间接继承于 `object` 的class),可以在一定程度上解决所谓`钻石继承`的问题。2.2起所有python内置class都是new-style class。
那么super是什么呢?实际上它是一个builtin的type,你调用 `super(Base, self)` 会返回一个 `__class__ = super` 的对象,这个对象可以作为一个代理,通过BFS的顺序(实际上列表已经保存在`Base.__mro__`里了)去访问第一个**直接定义**了目标属性的基类里的属性。`super(type, obj_or_subtype).attr` 基本上可以认为是 `find_in_mro_provide_by(obj_or_subtype, "attr")` ,有点晦涩。
**注意**,super()返回的不是“父类对象”,而是一个super类型的对象;实例化的时候,第一个参数是一个类型(type),第二个参数可以是type的instance,也可以是type的subclass。
介绍完基础知识,可以先说说为什么要有第二个参数。理由很简单 —— 我们知道 self 代表当前对象,每个对象的方法在定义的时候都需要显示地把self作为第一个参数,你本来应该写成这样
some_class.some_method(obj, *args, **kwargs)
但是因为Python语法允许 `obj.some_metod(*args, **kwargs)` (本质上是个语法糖) ,所以你可以写得简单点(不必显式地给出方法的第一个参数)。而super对象则不同,它没有语法上的直接支持,所以在内部invoke some_method的时候必须指定某个对象,而这个对象的得你自己塞给它,也就是说把这个super对象绑定(bound)到第二个参数上。所以实际上并不是非要用self作为super的第二个参数,甚至super并不是必须在class内部才能调用:
class A(object):
def foo(self):
print self.name
class B(A):
def __init__(self, name):
self.name = name
b1 = B('b1')
super(B, b1).foo() #produces 'b1'
def foo(self):
print self.name
class B(A):
def __init__(self, name):
self.name = name
b1 = B('b1')
super(B, b1).foo() #produces 'b1'
至于为什么要有第一个参数,如果你看了 `help(super)` 就会发现它居然提供了个单参数的版本:
super(type) -> unbound super object
这个理解起来比较困难点。先说这个`unbound`,没绑定,就是说这个super对象没有实际绑定到某个对象上,它并不是可以直接用的。要怎么用呢?那就得注意到`help(super)`里面有一个 `__get__` 方法,也就是说,super类还是一个Descriptor类!哎,这个Descriptor类展开又是好大一段,简单地说就是它可以把自己和某个object的属性绑定,使得访问这个属性的时候,实际上是在调用它的`__get__/__set__`方法:
class Descr(object):
def __get__(self, obj, tp): #最后一句传进来的 obj=x, tp=X
return 'get!'
class X(object):
t = Descr()
x = X()
print x.t #this produces 'get!'
def __get__(self, obj, tp): #最后一句传进来的 obj=x, tp=X
return 'get!'
class X(object):
t = Descr()
x = X()
print x.t #this produces 'get!'
回到unbound super, 为了使用它,就要通过 __get__ 方法把它再绑定到一个对象上。由于Descriptor的属性,特别适合这么用:
class A(object):
def foo(self):
print 'foo'
class B(A):
def foo(self):
self._super.foo()
B._super = super(B) #这还不能直接写在B的定义里,多蛋疼啊。
b = B()
b.foo() #produces 'foo'
def foo(self):
print 'foo'
class B(A):
def foo(self):
self._super.foo()
B._super = super(B) #这还不能直接写在B的定义里,多蛋疼啊。
b = B()
b.foo() #produces 'foo'
这是它的一个可能用法。我不确定是否还有其他更合适的用途,但是在这里实际上也并不是很好,尤其是遇到staticmethod的时候还会出错,再加上绕了这么大一个弯,实在不是很推荐使用。
据说unbound super的使用非常少,不知道现实意义有多少,也不知道当初为什么设计成这个样子,总之由于Python3.0已经改了(虽然仍然保留了unbound super),所以基本上还是可以认为这是设计的历史遗留问题。
参考文献(unbound super主要参考了这个系列):Things to Know About Python Super
[1] http://www.artima.com/weblogs/viewpost.jsp?thread=236275
[2] http://www.artima.com/weblogs/viewpost.jsp?thread=236278
[3] http://www.artima.com/weblogs/viewpost.jsp?thread=237121
Aug
16
为了扩展Python,我们可以[用C/C++编写模块](http://docs.python.org/2/extending/ ),但是这要求对Python的底层有足够的了解,包括Python对象模型、常用模块、引用计数等,门槛较高,且不方便利用现有的C库。而 [ctypes](http://docs.python.org/2/library/ctypes.html ) 则另辟蹊径,通过封装`dlopen/dlsym`之类的函数,并提供对C中数据结构的包装/解包,让Python能够加载动态库、导出其中的函数直接加以利用。
快速上手
-------
最简单的,我们可以从 libc 开始:
由于ctypes库能够直接处理 None, integers, longs, byte strings 和 unicode strings 类型的转换,因此在这里不需要任何额外的操作就能完成任务。(注意,最后一行的25是printf的返回值,标识输出了25个字符)
自己动手
-------
[这里](http://wolfprojects.altervista.org/articles/dll-in-c-for-python/)有一个最简单的例子——实现一个 `int sum(int a, int b)`:
编译之得到 foo.so:
$ gcc -fPIC -shared -o foo.so foo.c
然后在Python里头:
真是超级简单。
丰衣足食
-------
下面来个稍微复杂点的例子:反转一个字符串。尽管ctypes可以直接处理string的转换,但是你不能直接修改string里的内容,所以这里需要变通一下。可能的解决方案有两个:
1. 在foo.c里面malloc一点空间然后返回。这样可以不修改原string,但是却要考虑释放该空间的问题,不太好处理。
2. 让Python分配好空间,将原字符串拷贝进去,再让foo里面的函数来修改它。ctypes为这种方案提供了`create_string_buffer`这个函数,正适合。
那就让我们用方案2来实现它:
然后在Python里头:
这样就OK啦。
补充说明
-------
以上的操作都是在Linux下完成的,在Windows下基本上一致,除了windows不能直接load libc.so.6,而是应该找 msvcrt :
>>> libc = ctypes.cdll.msvcrt
或者直接导入文件
>>> libc = ctypes.CDLL("msvcr90.dll")
小结
-------
前面演示了ctypes库的简单使用,已经能够完成许多任务了;但是它可以完成更多的任务,包括操作更复杂的例如结构体、数组等C数据结构,具体的这里不细说了,可以参考详细的[官方文档](http://docs.python.org/2/library/ctypes.html)。
好了,就到这里。
快速上手
-------
最简单的,我们可以从 libc 开始:
felix021@ubserver:~/code$ python
Python 2.7.3 (default, Jul 5 2013, 08:39:51)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> libc = ctypes.CDLL("libc.so.6")
>>> libc.printf("hello, %s! %d + %d = %d\n", "world", 1, 2, 3)
hello, world! 1 + 2 = 3
25
Python 2.7.3 (default, Jul 5 2013, 08:39:51)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> libc = ctypes.CDLL("libc.so.6")
>>> libc.printf("hello, %s! %d + %d = %d\n", "world", 1, 2, 3)
hello, world! 1 + 2 = 3
25
由于ctypes库能够直接处理 None, integers, longs, byte strings 和 unicode strings 类型的转换,因此在这里不需要任何额外的操作就能完成任务。(注意,最后一行的25是printf的返回值,标识输出了25个字符)
自己动手
-------
[这里](http://wolfprojects.altervista.org/articles/dll-in-c-for-python/)有一个最简单的例子——实现一个 `int sum(int a, int b)`:
//file foo.c
#ifdef __WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
DLLEXPORT int sum(int a, int b)
{
return a + b;
}
#ifdef __WIN32
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT
#endif
DLLEXPORT int sum(int a, int b)
{
return a + b;
}
编译之得到 foo.so:
$ gcc -fPIC -shared -o foo.so foo.c
然后在Python里头:
>>> foo = ctypes.CDLL("./foo.so")
>>> foo.sum(5, 3)
8
>>> foo.sum(5, 3)
8
真是超级简单。
丰衣足食
-------
下面来个稍微复杂点的例子:反转一个字符串。尽管ctypes可以直接处理string的转换,但是你不能直接修改string里的内容,所以这里需要变通一下。可能的解决方案有两个:
1. 在foo.c里面malloc一点空间然后返回。这样可以不修改原string,但是却要考虑释放该空间的问题,不太好处理。
2. 让Python分配好空间,将原字符串拷贝进去,再让foo里面的函数来修改它。ctypes为这种方案提供了`create_string_buffer`这个函数,正适合。
那就让我们用方案2来实现它:
//file foo.c
DLLEXPORT int reverse(char *str)
{
int i, j, t;
for (i = 0, j = strlen(str) - 1; i < j; i++, j--)
t = str[i], str[i] = str[j], str[j] = t;
return 0;
}
DLLEXPORT int reverse(char *str)
{
int i, j, t;
for (i = 0, j = strlen(str) - 1; i < j; i++, j--)
t = str[i], str[i] = str[j], str[j] = t;
return 0;
}
然后在Python里头:
>>> s = ctypes.create_string_buffer("hello world!")
>>> foo.reverse(s)
0
>>> print s.value
!dlrow olleh
>>> foo.reverse(s)
0
>>> print s.value
!dlrow olleh
这样就OK啦。
补充说明
-------
以上的操作都是在Linux下完成的,在Windows下基本上一致,除了windows不能直接load libc.so.6,而是应该找 msvcrt :
>>> libc = ctypes.cdll.msvcrt
或者直接导入文件
>>> libc = ctypes.CDLL("msvcr90.dll")
小结
-------
前面演示了ctypes库的简单使用,已经能够完成许多任务了;但是它可以完成更多的任务,包括操作更复杂的例如结构体、数组等C数据结构,具体的这里不细说了,可以参考详细的[官方文档](http://docs.python.org/2/library/ctypes.html)。
好了,就到这里。
Mar
7
上一篇 说到,对于这样的一段代码:
Python解释器会为 a 和 b 各 创建一个 PyIntObject (通过修改PyInt_FromLong打印int的id可以看出来),但是在实际的执行中,a和b却指向了同一个PyIntObject。也就是说,在执行之前,a和b已经被映射到了同一个PyIntObject。
前面说了,Python的解释执行是由以下调用链组成的:
PyRun_FileExFlags()
mod_ty *mod = PyParser_ASTFromFile() //把py源码转换成AST(Abstract Syntax Tree)
run_mod(mod, ...) //执行AST
co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode
PySymtable_Build() //创建符号表
co = compiler_mod() //编译ast为bytecode
PyEval_EvalCode(co, ...) //执行bytecode
PyEval_EvalCodeEx()
由于没有编译原理的基础,只能从全局上看出这些代码都做了什么,但是却很难从细节上去追查。通过修改源码我尽可能了解了 PyParser 将Python源码转换成AST的运行机制(虽然还是没有看懂tokens->cst的转换),但是run_mod的细节实在是看不懂了。于是我在StackOverflow上面提了一个问题,@Bakuriu大牛给了个hint,说是PyCompiler在处理lambda的时候,使用 compiler_add_o() 来将lambda对应的函数的__doc__设置为 PyNone:
这里头PyNone是常量,而且又出现了 c->u->u_consts ,大有看头。
有了线索以后,突然一切都变得清晰了,简单加了些代码追,可以发现对于上面给出的代码, compiler_mod 是这样处理mod_ty *mod(也就是那棵AST)的:
简单解释下Assign操作的代码:
1. 获得赋值目标的数量(比如a=b=1,就是2个)
2. "VISIT"要赋的值
3. 挨个ADDOP(c, DUP_TOP)是告诉编译器,增加一个OPCODE=DUP_TOP
DUP_TOP 是 Duplicates the reference on top of the stack 的简写,意思是取得上次计算的值(比如对于b,就是int(1)的reference,而对于a,就是b=1的返回值,也就是b的reference)加入stack_top,这样正好把多个赋值操作串起来。
不过我关注的主要是第二条,对应的代码就是:
这个compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)的作用是将一个变量o及其type组成的tuple(o, o->ob_type)塞入到dict中。但是并不是简单暴力地直接插入,它的源码大约是这样的:
这个代码乍看挺诡异的,因为还与后续编译成字节码的部分有所耦合,这里大致解释一下:
(1) 对于LOAD_CONSTS, 在compiler_visit_expr里面已将dict指定为c->u->u_consts,也就是专门用来存放常量的dict
(2) 所有常量都是用 (o, type(o)) 作为 key 存进去的,并返回顺序递增的编号v,表示o是第v个存进去的常量
(3) dict是个hash表,所以往dict里面塞东西时要计算key的hash
(4) tuple的hash值是将每个元素的hash值组合起来哈希(详见tuplehash函数),类似于sdbmhash或者jshash
(5) int对象的哈希是针对int的值,int对象比较时仅比较它们的值
尽管 a 和 b 对应的key (a, int)和(b, int)虽然不是同一个对象,但是它们的哈希值是一样的!并且!PyDict_GetItem()查找到某个slot去比较key的时候,递归地去比较key的每一个元素,而两个int的比较,“正好”是比较它们的值是否相等!
所以,在遍历AST生成bytecode之前,两个相同的const只会在c->u->u_consts中出现1次。
在compiler_mod函数的末尾有一个assemble(c, addNone),它是将前面生成好的opcode等数据转换成最终的bytecode,其中一些代码逻辑是这样的
也就是说,这里把保存所有常量的 c->u->u_consts 按照插入元素的顺序将所有塞进来的PyObject逐个插入到 consts 里,并最后赋值给PyCodeObject的PyListObject *co_consts。而在最最后的eval环节,LOAD_CONST这个opcode会将它的oparg(就是前面 compiler_addop_i 塞进去的值,也就是compiler_addop_o返回的值)作为索引,从co_consts里取出来,PUSH到栈顶(参见Python/ceval.c +1123行),供下一个指令读取。
于是int常量整个python的解释执行所经历的步骤都完整地串起来了。
泪流满面,居然看懂了。
a = 257
b = 0x101
print a is b
b = 0x101
print a is b
Python解释器会为 a 和 b 各 创建一个 PyIntObject (通过修改PyInt_FromLong打印int的id可以看出来),但是在实际的执行中,a和b却指向了同一个PyIntObject。也就是说,在执行之前,a和b已经被映射到了同一个PyIntObject。
前面说了,Python的解释执行是由以下调用链组成的:
引用
PyRun_FileExFlags()
mod_ty *mod = PyParser_ASTFromFile() //把py源码转换成AST(Abstract Syntax Tree)
run_mod(mod, ...) //执行AST
co = PyAST_Compile(mod, ...) //将AST转换成CFG(Control Flow Graph) bytecode
PySymtable_Build() //创建符号表
co = compiler_mod() //编译ast为bytecode
PyEval_EvalCode(co, ...) //执行bytecode
PyEval_EvalCodeEx()
由于没有编译原理的基础,只能从全局上看出这些代码都做了什么,但是却很难从细节上去追查。通过修改源码我尽可能了解了 PyParser 将Python源码转换成AST的运行机制(虽然还是没有看懂tokens->cst的转换),但是run_mod的细节实在是看不懂了。于是我在StackOverflow上面提了一个问题,@Bakuriu大牛给了个hint,说是PyCompiler在处理lambda的时候,使用 compiler_add_o() 来将lambda对应的函数的__doc__设置为 PyNone:
/* Make None the first constant, so the lambda can't have a
docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
return 0;
docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
return 0;
这里头PyNone是常量,而且又出现了 c->u->u_consts ,大有看头。
有了线索以后,突然一切都变得清晰了,简单加了些代码追,可以发现对于上面给出的代码, compiler_mod 是这样处理mod_ty *mod(也就是那棵AST)的:
引用
compiler_mod(compiler *c, mod_ty *mod)
case Module_kind: //mod->kind = 1
compiler_body(c, mod->v.Module.body <as> stmts)
for (i = 0; i < asdl_seq_LEN(stmts); i++) //循环3次,因为有3个stmt
VISIT(c, stmt, asdl_seq_GET(stmts, i)) //宏展开到compiler_visit_stmt
compiler_visit_stmt(c, asdl_seq_GET(stmts,i) <as> s)//访问每个stmt
case Assign_kind: //第一个stmt的kind = 5,表示一个赋值操作
//赋值操作允许 a = b = 1 所以看起来有点罗嗦,
//它会被解析成如下AST:
//Assign([AssName('a', 'OP_ASSIGN'),
// AssName('b', 'OP_ASSIGN')], Const(1))
n = asdl_seq_LEN(s->v.Assign.targets);
VISIT(c, expr, s->v.Assign.value);
for (i = 0; i < n; i++) {
if (i < n - 1)
ADDOP(c, DUP_TOP);
VISIT(c, expr, asdl_seq_GET(s->v.Assign.targets, i));
}
case Module_kind: //mod->kind = 1
compiler_body(c, mod->v.Module.body <as> stmts)
for (i = 0; i < asdl_seq_LEN(stmts); i++) //循环3次,因为有3个stmt
VISIT(c, stmt, asdl_seq_GET(stmts, i)) //宏展开到compiler_visit_stmt
compiler_visit_stmt(c, asdl_seq_GET(stmts,i) <as> s)//访问每个stmt
case Assign_kind: //第一个stmt的kind = 5,表示一个赋值操作
//赋值操作允许 a = b = 1 所以看起来有点罗嗦,
//它会被解析成如下AST:
//Assign([AssName('a', 'OP_ASSIGN'),
// AssName('b', 'OP_ASSIGN')], Const(1))
n = asdl_seq_LEN(s->v.Assign.targets);
VISIT(c, expr, s->v.Assign.value);
for (i = 0; i < n; i++) {
if (i < n - 1)
ADDOP(c, DUP_TOP);
VISIT(c, expr, asdl_seq_GET(s->v.Assign.targets, i));
}
简单解释下Assign操作的代码:
1. 获得赋值目标的数量(比如a=b=1,就是2个)
2. "VISIT"要赋的值
3. 挨个ADDOP(c, DUP_TOP)是告诉编译器,增加一个OPCODE=DUP_TOP
DUP_TOP 是 Duplicates the reference on top of the stack 的简写,意思是取得上次计算的值(比如对于b,就是int(1)的reference,而对于a,就是b=1的返回值,也就是b的reference)加入stack_top,这样正好把多个赋值操作串起来。
不过我关注的主要是第二条,对应的代码就是:
引用
VISIT(c, expr, s->v.Assign.value); //宏展开到compiler_visit_expr
compiler_visit_expr(c, s->v.Assign.value <as> e)
case Num_kind: //e->kind = 16
ADDOP_O(c, LOAD_CONST, e->v.Num.n, consts) //宏展开到compiler_addop_o
//这里e->v.Num.n是在CST->AST的过程中生成的PyIntObject
//下面的c->u->u_consts是一个PyDictObject,用来保存常量对象
compiler_addop_o(c, LOAD_CONST <as> opcode,
c->u->u_consts <as> dict, e->v.Num.n <as> o)
arg = compiler_addop_o(c, dict, o)//塞入dict
compiler_addop_i(c, opcode, arg) //将插入顺序作为opcode的oparg
compiler_visit_expr(c, s->v.Assign.value <as> e)
case Num_kind: //e->kind = 16
ADDOP_O(c, LOAD_CONST, e->v.Num.n, consts) //宏展开到compiler_addop_o
//这里e->v.Num.n是在CST->AST的过程中生成的PyIntObject
//下面的c->u->u_consts是一个PyDictObject,用来保存常量对象
compiler_addop_o(c, LOAD_CONST <as> opcode,
c->u->u_consts <as> dict, e->v.Num.n <as> o)
arg = compiler_addop_o(c, dict, o)//塞入dict
compiler_addop_i(c, opcode, arg) //将插入顺序作为opcode的oparg
这个compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)的作用是将一个变量o及其type组成的tuple(o, o->ob_type)塞入到dict中。但是并不是简单暴力地直接插入,它的源码大约是这样的:
static int
compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)
{
PyObject *t, *v;
Py_ssize_t arg;
if (PyFloat_Check(o)) {
//省略部分与int无关的代码
}
else {
t = PyTuple_Pack(2, o, o->ob_type); //t = tuple(o, type(o))
}
if (t == NULL)
return -1;
v = PyDict_GetItem(dict, t); //看看t是否已经在dict中出现过
if (!v) { //如果没有
arg = PyDict_Size(dict); //获取dict的当前大小(PyIntObject)
v = PyInt_FromLong(arg);
if (!v) {
Py_DECREF(t);
return -1;
}
if (PyDict_SetItem(dict, t, v) < 0) { //dict[(o, type(o))] = v
Py_DECREF(t);
Py_DECREF(v);
return -1;
}
Py_DECREF(v);
}
else
arg = PyInt_AsLong(v); //如果出现过,取得之前设置的v
Py_DECREF(t);
return arg;
}
compiler_add_o(struct compiler *c, PyObject *dict, PyObject *o)
{
PyObject *t, *v;
Py_ssize_t arg;
if (PyFloat_Check(o)) {
//省略部分与int无关的代码
}
else {
t = PyTuple_Pack(2, o, o->ob_type); //t = tuple(o, type(o))
}
if (t == NULL)
return -1;
v = PyDict_GetItem(dict, t); //看看t是否已经在dict中出现过
if (!v) { //如果没有
arg = PyDict_Size(dict); //获取dict的当前大小(PyIntObject)
v = PyInt_FromLong(arg);
if (!v) {
Py_DECREF(t);
return -1;
}
if (PyDict_SetItem(dict, t, v) < 0) { //dict[(o, type(o))] = v
Py_DECREF(t);
Py_DECREF(v);
return -1;
}
Py_DECREF(v);
}
else
arg = PyInt_AsLong(v); //如果出现过,取得之前设置的v
Py_DECREF(t);
return arg;
}
这个代码乍看挺诡异的,因为还与后续编译成字节码的部分有所耦合,这里大致解释一下:
(1) 对于LOAD_CONSTS, 在compiler_visit_expr里面已将dict指定为c->u->u_consts,也就是专门用来存放常量的dict
(2) 所有常量都是用 (o, type(o)) 作为 key 存进去的,并返回顺序递增的编号v,表示o是第v个存进去的常量
(3) dict是个hash表,所以往dict里面塞东西时要计算key的hash
(4) tuple的hash值是将每个元素的hash值组合起来哈希(详见tuplehash函数),类似于sdbmhash或者jshash
(5) int对象的哈希是针对int的值,int对象比较时仅比较它们的值
尽管 a 和 b 对应的key (a, int)和(b, int)虽然不是同一个对象,但是它们的哈希值是一样的!并且!PyDict_GetItem()查找到某个slot去比较key的时候,递归地去比较key的每一个元素,而两个int的比较,“正好”是比较它们的值是否相等!
所以,在遍历AST生成bytecode之前,两个相同的const只会在c->u->u_consts中出现1次。
在compiler_mod函数的末尾有一个assemble(c, addNone),它是将前面生成好的opcode等数据转换成最终的bytecode,其中一些代码逻辑是这样的
assemble(c, addNone)
struct assembler a;
完成一些初始化
makecode(c, &a);
PyObject *tmp = dict_keys_inorder(c->u->u_consts, 0); //convert to Tuple
consts = PySequence_List(tmp); //convert to List
...
PyCodeObject *co = PyCode_New(..., consts, ...);
co->co_consts = consts
struct assembler a;
完成一些初始化
makecode(c, &a);
PyObject *tmp = dict_keys_inorder(c->u->u_consts, 0); //convert to Tuple
consts = PySequence_List(tmp); //convert to List
...
PyCodeObject *co = PyCode_New(..., consts, ...);
co->co_consts = consts
也就是说,这里把保存所有常量的 c->u->u_consts 按照插入元素的顺序将所有塞进来的PyObject逐个插入到 consts 里,并最后赋值给PyCodeObject的PyListObject *co_consts。而在最最后的eval环节,LOAD_CONST这个opcode会将它的oparg(就是前面 compiler_addop_i 塞进去的值,也就是compiler_addop_o返回的值)作为索引,从co_consts里取出来,PUSH到栈顶(参见Python/ceval.c +1123行),供下一个指令读取。
于是int常量整个python的解释执行所经历的步骤都完整地串起来了。
泪流满面,居然看懂了。