文章目录

  • 使用元类
  • type函数
  • 什么是元类
  • 怎样使用元类
  • 编写ORM框架
  • 小结


使用元类

type函数

动态语言和静态语言最大的不同,就是在动态语言中,函数和类的定义,不是编译时定义的,而是运行时动态创建的

比方说我们要定义一个 Hello 类,首先编写一个 hello.py 模块,里面的代码如下:

class Hello(object):
    def hello(self, name='world'):
        print('Hello, %s.' % name)

当Python解释器导入 hello 模块时,就会依次执行该模块的所有语句(与我们在交互环境下逐个语句输入来定义类一样),从而动态创建出一个类对象(注意这里说的是类对象而不是实例对象),测试如下:

>>> from hello import Hello # 这个语句创建了一个名为Hello的类对象
>>> h = Hello()             # 创建一个Hello类的实例h
>>> h.hello()
Hello, world.
>>> print(type(Hello))      # Hello对象的类型为type
<class 'type'>
>>> print(type(h))          # 而Hello类实例的类型为hello.Hello
<class 'hello.Hello'>
>>> type(str)               # str是一个类型
<class 'type'>
>>> type(int)               # int也是一个类型
<class 'type'>

type() 函数可以用来查看一个变量的类型Hello 是一个类,它的类型就是 type,而 h 是一个实例,它的类型就是它所属的类。

前面说到,在Python中,类的定义是运行时动态创建的。而动态创建类使用的其实是 type()函数type() 函数既可以返回一个变量的类型,又可以创建出新的类型。依然举 Hello 类为例子,但我们这次使用 type() 函数来创建 Hello 类而不使用显式的 class Hello

>>> def fn(self, name='world'): # 先定义函数
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

使用 type() 函数创建一个类对象,需要依次传入以下3个参数:

  • 类名
  • 继承的父类集合:Python支持多重继承,所以这里用一个 tuple 来囊括继承的所有父类。注意只有一个父类时,要采用 tuple 的单元素写法,不要漏掉逗号。
  • 类的方法名与函数的绑定:在上面的例子中,我们把函数 fn 绑定到方法名 hello 上。也即类 Hello 的方法 hello 就是函数 fn,注意这和这章开头所说的动态绑定方法是不同的

通过 type() 函数创建的类和直接写类是完全一样的。事实上,Python解释器遇到类定义时,在扫描类定义的语法之后,就是调用 type() 函数来创建类的

正常情况下,我们都用 class 类名(父类1, 父类2, ...) 的方式来定义类,但是,type() 函数也允许我们动态创建类。

动态语言能够支持运行期间动态创建类,这和静态语言有非常大的不同。关于这两者的区别,感兴趣的话可以再查找其他资料。


什么是元类

除了使用 type() 函数动态创建类以外,要控制类的创建行为,还可以使用元类(metaclass)。

怎么理解什么是元类呢?简单地解释一下:

  • 当我们定义了类以后,以类为模版就可以创建出实例了。
  • 但如果我们要创建类呢?那就必须先定义元类,有了元类之后,以元类为模版就可以创建出类了。
  • 连起来就是:以元类为模版创建类,以类为模版创建该类的实例

也就是说,可以把类看成是元类创建出来的“实例”

python中Hello和world谁大 python中hello>hello_数据库


在上一个小节中,我们了解到可以使用 type() 函数创建类,但 type 的本质是什么呢?

>>> help(type)
Help on class type in module builtins:

class type(object)
 |  type(object_or_name, bases, dict)
 |  type(object) -> the object's type
 |  type(name, bases, dict) -> a new type
 |
 |  Methods defined here:
 |
 |  __call__(self, /, *args, **kwargs)
 |      Call self as a function.
 ...

其实呀, type 本身就是一个类,调用 type() 创建类得到的其实就是 type 类的实例。所以所有类对象的类型都是 type。不难分析出,type 是一个元类,并且类都是默认以元类 type 为模版创建的


怎样使用元类

如果我们想要创建一个元类,并且想以这个元类为模版创建类,那么定义元类的时候,就应当让这个元类继承自 type

按照习惯,元类的类名应总是以Metaclass结尾,以便清楚地表示这是一个元类。下面举一个例子,定义元类 ListMetaclass

# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)

元类的 __new__() 方法用于创建一个类对象,它接收四个参数,依次是:

  • 准备创建的类对象;
  • 准备创建的类的名字;
  • 准备创建的类继承的父类集合;
  • 准备创建的类的方法集合。

我们在 ListMetaclass__new__() 方法中加入了一句 attrs['add'] = lambda self, value: self.append(value),然后调用元类 type__new__() 方法创建类对象。这多出来的一句,实际上我们是给要创建的类提供了一个 add 方法,这个 add 方法接收实例本身和一个变量,并把这个变量拼接到实例的尾部。其实就是一个 append 方法。

定义好元类 ListMetaclass 之后,我们以它为模版创建类,注意传入关键字参数 metaclass

class MyList(list, metaclass=ListMetaclass):
    pass

传入关键字参数 metaclass 后,Python解释器会在创建 MyList 类时,通过元类 ListMetaclass__new__() 方法来创建。因此虽然我们在类定义时没有为 MyList 类定义任何方法,但因为它是以元类 ListMetaclass 为模版创建的,所以拥有了 add 方法。另外,因为它继承了 list 类,所以我们相当于创建了一个拥有 add 方法的新的 list 类,测试一下:

>>> L = MyList()
>>> L.add(1) # 使用add方法在列表尾部添加元素
>> L
[1]
>>> L.add(2)
>>> L
[1, 2]

普通的 list 是没有 add() 方法的:

>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

但是,直接在 MyList 类的定义中写上 add() 方法不是更简单吗?是的,正常情况下我们应该直接在类定义中编写方法,而不是通过元类

但是,也有需要通过元类动态修改类定义的情况,ORM就是一个典型的例子。


编写ORM框架

ORM 全称 Object Relational Mapping(对象-关系映射),简单来说就是把关系型数据库中表格的每一行都映射为一个对象,而每一个表就是一个类。这样写代码更简单,不用直接操作SQL语句。

要编写一个 ORM 框架供不同的使用者使用,框架中的所有类都应该能动态定义,因为每位使用者的需求不同,需要根据具体的表结构来定义出不同的类。

举个例子,假如使用者想使用这个 ORM 框架定义一个 User 类来操作数据库中的表格 User,我们期望使用者可以写出这样简洁的形式:

class User(Model):
    # 定义类的属性到表格中列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')
# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 插入到表格中:
u.insert()

也即,用户在使用这个 ORM 框架时,每个表格对应一个类,类定义只需要指定表格每列的字段类型即可,每一行数据都是该类的一个实例。而父类 Model 和数据类型 StringFieldIntegerField 等都由 ORM 框架负责提供。save() 之类的方法则全部由元类自动完成。虽然这样元类的编写会比较复杂,但 ORM 的使用者用起来却可以异常简单

想好了希望实现怎样的效果后,我们可以开始编写调用接口。

首先定义 Field 类,它是最底层的类,负责保存字段名(列名)和对应的字段类型:

class Field(object):
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type
    def __str__(self):
        return '<%s:%s>' % (self.__class__.__name__, self.name)
    __repr__ = __str__

Field 的基础上,我们可以进一步定义各种类型的 Field,比如 StringFieldIntegerField 等等:

class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')

class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')

注意这里使用了 super 函数来获取父类的方法,并进行绑定,先看一看官方的解释:

super(type[, object-or-type])
  Return the superclass of type. If the second argument is omitted the super object
  returned is unbound. If the second argument is an object, isinstance(obj, type)
  must be true. If the second argument is a type, issubclass(type2, type) must be
  true. super() only works for new-style classes.
  A typical use for calling a cooperative superclass method is:
   class C(B):
       def meth(self, arg):
           super(C, self).meth(arg)
  New in version 2.2.

所以这里实际上我们实例化 StringFieldIntegerField 时,是调用它们的父类,也即 Field 类的 __init__ 方法进行的,这两个类封装了 Field 的功能,使用者只需要传入字段名就可以了,不需要关心在数据库中类型的名字。上面的实现比较简单,不需要使用元类。

接下来先理一理整体的实现思路,我们编写 ORM 框架来实现底层的功能,用户使用该框架时,只需要根据自己的需求来为表格定义对应的类,比方说上面举的例子中定义 User 类那样。这个类的实例对应表格中的一行,定义一个新实例 u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd'),我们希望得到这个实例后可以通过 print(u['name']) 的方式读取字段值,通过 u['id']=23456 的方式来修改字段值,这就类似于Python中的 dict 的功能,所以我们实际上最底层的父类采用 dict 即可。

但是,我们除了 dict 的功能之外,肯定还需要实现一些其他功能,比如把新实例插入到数据库的表格中。这些功能我们可以在 Model 类中实现,Model 类继承 dict 类,这样我们就可以像前面说的那样进行读取和修改了。使用者为表格编写类时继承 Model 类即可,这样所有表格都能得到 Model 类中实现的操作表格的功能了。

但是,我们还注意到一点,我们希望用户定义类的时候,写法尽可能简单,只需要关注有哪些字段,然后每个字段作为一个属性,用 id = IntegerField('id') 的方式来定义,也即 属性名 = 字段类型('字段名'),字段类型的实现前面已经说过了。

这里我们需要关注另外一个很重要的点,在实例化得到表格的一行以后,我们希望使用者可以采用 实例名.属性名 = 值 的方式来修改这一行某个字段的值。但事实上,使用者定义类的时候,类属性表示的是以某个字段名为名的某字段类型的实例,属性的类型是 StringField 或者 IntegerField。而在读取或修改一个实例的属性值时,我们希望实例属性表示的是这一行数据在这个字段的值,属性的类型是 str 或者 int。这里说得比较绕,简单归纳来说就是用户定义类的方式和使用该类实例的方式不相符

我们希望使用者定义类的方式尽可能简单,同时也能用简单的方式修改字段值(实例的属性值),但由于类属性和实例属性同名时,对实例属性赋值会覆盖类属性,所以我们必须进行一些修改去避免这个问题。怎么实现呢?这时候我们就要用到元类了,虽然作为框架的编写者,我们要做的工作比较多,但这样使用者用起来就很方便了,他们依然可以很简单地定义类,但运行时类定义会被元类动态修改,我们可以把类属性该为其他名字,这样类定义中的类属性信息就可以保留下来了,而且不会被实例属性的赋值所覆盖。

另外,由于使用者不一定明白元类这么复杂的概念,所以我们把元类封装在 Model 类的定义中,指定 Model 类使用 ModelMetaclass 为模版。把前面所说的更换类属性名的操作封装在 ModelMetaclass 中,使用者为表格编写类的时候只需要继承 Model 类,那么运行时就会自动以 ModelMetaclass 为模版,得到 ModelMetaclass 的所有功能。但是要注意,Model 类本身不需要更换类属性名,所以在 ModelMetaclass 中我们要排除掉 Model 类。

接下来,直接上代码。元类 ModelMetaclass

class ModelMetaclass(type):
    # 四个参数,依次为:准备创建的类对象,类的名字,继承的父类集合,属性&方法集合
    def __new__(cls, name, bases, attrs):
        # Model类不需额外操作,先排除掉
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        # 其他类(对应具体的表格)则把类属性使用dict存好,绑定到__mappings__属性上
        # 然后删除掉这些类属性
        # 这里还动态地要创建的类添加了一个表名属性__table__,直接令表名等于类名
        # 当然也可以作一些其他修改
        print('Found model: %s' % name)
        mappings = dict()
        for k, v in attrs.items(): # 取出每个类属性
            # 因为除了用户定义的类属性之外,还有一些继承自父类的属性等等
            # 所以这里要先判断一下,属于字段类型的属性才需要考虑
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v # 使用一个dict保存 类属性名-字段类型实例 的映射
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 把映射绑定到__mappings__属性上
        attrs['__table__'] = name        # 把表名绑定到__table__属性上
        return type.__new__(cls, name, bases, attrs)

父类 Model

class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        super(Model, self).__init__(**kw) # 创建一个dict
    def __getattr__(self, key): # 可以采用点符访问实例属性(字段值)
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)
    def __setattr__(self, key, value): # 可以采用点符修改实例属性(字段值)
        self[key] = value
    def insert(self):  # 将实例插入到数据库的对应表格中
        fields = []
        values = []
        # 取得这一行数据的字段名及对应字段值
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            values.append(str(getattr(self, k, None)))
        # 格式化SQL语句(MySQL语法)
        sql = 'INSERT INTO %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(values))
        print('SQL: %s' % sql) # 输出SQL语句,这里我们没有写真的插入数据库的操作,只是举例子

当用户为 User 表定义 User 类时,Python解释器首先在当前类 User 的定义中查找是否有metaclass关键字,如果没有找到,就继续在父类 Model 中查找metaclass关键字,因为父类 Model 定义了以元类 ModelMetaclass 为模版来创建,所以 User 类也会以元类 ModelMetaclass 为模版来创建。借助元类,我们可以在运行时动态地修改子类的定义,但使用者定义子类时却不需要显式地声明

使用者定义 User 类之后,会输出:

Found model: User
Found mapping: password ==> <StringField:password>
Found mapping: name ==> <StringField:username>
Found mapping: email ==> <StringField:email>
Found mapping: id ==> <IntegerField:id>

运行时,类定义被元类动态地修改了,使用者定义的四个类属性被集成到 __mappings__ 属性中,因此不会被实例属性覆盖,也就不会丢失字段名信息了。

创建实例,然后把这个实例(一行数据)插入到数据库:

>>> # 创建一个实例:
... u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
>>> # 插入到表格中:
... u.insert()
SQL: INSERT INTO User (email,username,password,id) values (test@orm.org,Michael,my-pwd,12345)

我们也可以看看 __mappings__ 属性怎么样:

>>> User.__mappings__
{'email': <StringField:email>, 'name': <StringField:username>, 'password': <StringField:password>, 'id': <IntegerField:id>}
>>> u.__mappings__
{'email': <StringField:email>, 'name': <StringField:username>, 'password': <StringField:password>, 'id': <IntegerField:id>}

正如我们定义那样,它是一个 dict,里面保存着各个字段的名字和它们的数据类型。

虽然我们没有真的实现插入数据库,但可以看到打印出的SQL语句是正确的,要实现完整的功能,只要再使用数据库模块的接口就可以了。通过这样短短的不到100行代码,我们就借助元类实现了一个精简的 ORM 框架。


小结

元类是Python中非常具有魔术性的对象,它可以改变类创建时的行为。这种强大的功能使用起来务必小心。