作者:Jeff Knupp
原文地址:https://jeffknupp.com/blog/2013/12/28/improve-your-python-metaclasses-and-dynamic-classes-with-type/
关键字metaclass与type都是很少用到(因此大多数人没有很好理解)的Python构造。在本文里,我们将探索type()不同的“类型”,以及type少有人知的使用如何与metaclass关联。
你是我的类型吗?
Type()的第一个使用是最广为人知的:确定一个对象的类型。这里,Python新手通常会打断且说道,“不过我认为Python没有类型!”相反,Python里每样东西都要一个类型(甚至类型本身!)因为一切都是对象。让我们看几个例子:
>>> type(1)
<class 'int'>
>>> type('foo')
<class 'str'>
>>> type(3.0)
<class 'float'>
>>> type(float)
<class 'type'>
type的类型
一切如预期,直到我们检查float的类型。<class ’type’>?这是什么?好吧,很奇怪,但让我们继续:
>>> class Foo(object):
... pass
...
>>> type(Foo)
<class 'type'>
啊!又一次<class ‘type’>。显然,所有类本身的类型是type(不管它们是内置的还是用户定义的)。Type本身的类型是什么呢?
>>> type(type)
<class 'type'>
好吧,它在某处停止了。Type是所有类型的类型,包括自己。事实上,type是一个metaclass,或者“构建类的一个事物”。类,比如list(),就像在my_list = list()里,构建了该类的实例。同样,metaclass构建类型,就像下面的Foo(译注:Foo类是一个用户定义类型):
class Foo(object):
pass
运转你自己的metaclass
就像普通的类,metaclass可以是用户定义的。要使用它,你将一个类的__metaclass__属性设置为你构建的metaclass。Metaclass可以是任意callable,只要它返回一个类型。通常,你将一个类的__metaclass__赋给一个函数,在某处,该函数使用我们尚未讨论的type的一个变体:用于创建类的三样参数。
Type的黑暗面
如提到的,事实证明,在使用3个实参调用时,type有一个完全独立的用途。Type(name, bases, dict)以编程方式创建了一个新类型。如果我有以下代码:
class Foo(object):
pass
使用下面的代码我们可以实现完全相同的效果:
Foo = type('Foo', (), {})
现在Foo引用名为Foo的一个类,其基类是object(使用type创建的类,如果没有指定一个基类,会自动做成新形式的类)。
这都很好,但如果我们向向Foo添加成员函数会怎么样呢?很容易通过设置Foo的属性实现这,就像这样:
def always_false(self):
return False
Foo.always_false = always_false
我们可以使用下面的代码一口气完成:
Foo = type('Foo', (), {'always_false': always_false})
当然,bases参数是Foo的一组基类。我们把它留空,但创建一个从Foo派生的新类完全是合法的,再次使用type来创建它:
FooBar = type('FooBar', (Foo), {})
这什么时候更有用?
一旦向别人解释,type与metaclass是接下来一个问题是“OK,那么我什么时候用它呢?”的其中一个话题。答案是,不经常。不过,有时使用type动态创建类是合适的解决方案。让我们看一下一个例子。
Sandman是我编写的库,用来自动生成一个REST API以及用于现有数据库、基于web的管理接口的一个接口(无需要求任何样板代码,boilerplate code)。大部分繁重的工作由SQLAlchemy完成,一个ORM框架。
使用SQLAlchemy注册一个数据库表仅有一个方式:创建一个描述该表的一个Model类(并非不像Django的模型)。为了让SQLAlchemy认识一张表,必须以某种方式创建用于该表的一个类。因为sandman预先不知道数据库的结构,它不能依赖预制的模型类来注册表。相反,它需要內省(introspect)数据库,同时创建这些类。听起来很熟悉?任何时候你动态地创建新类时,type是正确的/仅有的选择。
下面是sandman的相关代码:
if not current_app.endpoint_classes:
db.metadata.reflect(bind=db.engine)
for name in db.metadata.tables():
cls = type(str(name), (sandman_model, db.Model),
{'__tablename__': name})
register(cls)
正如你看到的,如果使用者没有为一张表手动创建一个模型类,它被自动创建,带有一个设置为该表名字的__tablename__属性(SQLAlchemy用来将表匹配到类)。
总结
在本文,我们讨论了type,metaclass的两个使用,以及何时要求type的另一个使用。虽然metaclass是一个有点令人混淆的概念,希望你现在有一个好的基础,可以开展进一步的学习。