Python的Metaclass魔法

乘着风

通过type创建Class

众所周知,在Python编程中,通过class定义类,再通过类实例化生成实例对象,所有实例对象都继承自object对象。但其实class本身也是一个对象,我们称之为类对象,所有class对象继承自type。
我们通过以下简单代码在Python交互式CLI中进行测试:

# 定义类A
>>> class A(object):
>>>    pass
    
>>> type(A)
<class 'type'>

>>> type(A())
<class '__main__.A'>

>>> isinstance(A, type)
True

>>> isinstance(A(), object)
True

我们可以通过类(class)对实例对象(instance object)进行定制和设计,控制实例对象的创建过程,那么我们是否能通过type控制类对象(class object)的创建过程,从而对类对象的创建过程进行定制和设计呢?答案是可定的。

type本身也是一个类(class),它不但能判断一个对象的类型,还可以创建类对象。

  • type(obj):判断一个对象的类型
  • type(name, bases, attrs):创建一个新类对象。

三个参数描述如下:

  1. name:类的名称,str类型
  2. bases:此类集成的父类集合,tuple类型
  3. attrs:类的属性列表,dict类型

通过type创建类示例:

# 定义一个方法
def greeting(self, name='world'):
    print("Hello, %s." % name)

# 通过type创建类
Hello = type("Hello", (object,), {"greeting": greeting})
h = Hello()
h.greeting() # >> Hello, world.

print(type(Hello)) #>> <class 'type'>
print(type(h)) # >> <class '__main__.Hello'>

Metaclass的使用

不但可以通过type动态的创建类,还可以通过继承type创建一个元类Metaclass,把此Metaclass类作为其他类的metaclass。class是实例对象(instance)的模板,而Metaclass则是class的模板,三者间的关系描述如下图。

+----------+             +----------+             +----------+
|          |             |          |             |          |
|          | instance of |          | instance of |          |
| instance +------------>+  class   +------------>+ metaclass|
|          |             |          |             |          |
|          |             |          |             |          |
+----------+             +----------+             +----------+

通过这种方式,在创建类对象前,Python先会执行Metaclass的相关方法来定制类对象,从而达到对类进行动态定制的目的,Django中的ORM框架,就是通过这种方式来实现的。这种定制,包括给类增加属性、方法,对类进行二次处理等。

下面演示通过Metaclass给自定义list类增加insert_before方法的过程。

class ListMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        attrs["insert_before"] = lambda self, item: self.insert(0, item)
        return type.__new__(mcs, name, bases, attrs)
        
class NewList(list, metaclass=ListMetaclass):
    pass
    
new_list = NewList(["a", "b"])
new_list.insert_before("insert_before")
print(new_list)  # >> ['insert_before', 'a', 'b']
  1. 首先定义元类ListMetaclass。
  2. class的__new__方法是用来创建实例对象的,类似的Metaclass的__new__方法是用来创建类对象的。在创建类对象前,我们通过在attrs中增加insert_before属性,这样创建的类对象中就拥有此属性。
  3. 定义NewList类,并指定metaclass。
  4. NewList在创建类实例时,会调用ListMetaclass来创建此类实例。
注:Metaclass.__new__的参数说明见上述type的参数说明。

通过Metaclass实现ORM

下面来看一个更实际可用的例子,以Django Tutorial中的展示的ORM的使用方式为例,通过Metaclass来开发一个简易的ORM框架。

Metaclass的寻找顺序:
类创建时,先查找类本身是否设置了Metaclass,如果设置了,则直接调用Metaclass创建类;如果没有,则根据基础顺序,一路找到基类,如果找到,则调用父类的Metaclass创建类;如果都没找到,则调用type创建类。

Django Tutorial使用ORM主要包括三个步骤:

  1. 定义模型类
  2. 创建模型对象
  3. 保存模型数据到数据库

示例代码如下:

# 定义模型类
class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

# 创建模型对象
q = Question(question_text="What's new?", pub_date=timezone.now())

# 保存模型数据到数据库
q.save()

ORM框架的基本设计思路包括如下几点:

  1. 定义Model类作为所有模型的基类,所有模型对象继承此类。
  2. 定义Field类作为模型所有字段的基类,模型的属性(Field字段)以类变量的形式定义在Model中。
  3. 定义ModelMetaclass类作为Model的Metaclass,解析Model中的Field字段,进行预处理,保存为Model的元数据,供ORM映射时使用。
  4. 在Model中实现save方法,利用Model的元数据,自动拼装Insert SQL语言。

先搭个框架,再填充各部分功能。

from datetime import datetime

class ModelMetaClass(type):
    def __new__(mcs, name, bases, attrs):
        return type.__new__(mcs, name, bases, attrs)
        
class Model(metaclass=ModelMetaClass):
    def __init__(self, **kwargs):
        pass
 def __setattr__(self, key, value):
        pass
 def __getattr__(self, item):
        pass
 def save(self):
        pass
        
class Field:
    pass
    
class CharField(Field):
    pass
    
class DateTimeField(Field):
    pass
    
class Question(Model):
    question_text = CharField()
    pub_date = DateTimeField()
    
question = Question(question_text="My first question.", pub_data=datetime.now())
question.save()

根据设计思路,功能完善后的代码如下:

  • ModelMetaclass主要解析Model中的Field类型字段,生成ORM数据库表字段的元数据。
from datetime import datetime

class ModelMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        # 只处理Model的子类
        if name == "Model" or Model not in bases:
            return type.__new__(mcs, name, bases, attrs)
        # 处理Field类型字段,信息保存在__fields__字段中
        fields = dict()
        for key in attrs:
            if isinstance(attrs[key], Field):
                fields[key] = attrs[key]
        attrs["__fields__"] = fields
        # 表名默认为class名的小写
        attrs["__table__"] = name.lower()
        # 删除Field类型的类变量
        for key in fields:
            attrs.pop(key)
        return type.__new__(mcs, name, bases, attrs)
  • Model类作为所有ORM模型的基类,为模型提供通用功能。

    1. __init__为模型构造函数,处理传入的Field字段
    2. __setattr____getattr__让模型Field类型字段可以像普通字段一样进行设值和取值
    3. save方法用于产生把模型保存到数据库的SQL语句
class Model(metaclass=ModelMetaclass):
    def __init__(self, **kwargs):
        # Field字段数据保存在self.fields中
        self.fields = dict()
        # self.__fields__即Metaclass中的attrs["__fields__"]字段
        model_fields = self.__fields__
        for kwarg_key in kwargs:
            if kwarg_key in model_fields:
                self.fields[kwarg_key] = kwargs[kwarg_key]
            else:
                raise KeyError()
    def __setattr__(self, key, value):
        # 实现通过model.field = xxx 对Field字段赋值
        if key in self.__fields__:
            self.__dict__["fields"][key] = value
            return
        self.__dict__[key] = value
    def __getattr__(self, key):
        # 实现通过model.field 读取Field字段值
        if key in self.__fields__:
            return self.__dict__["fields"][key]
        return self.__dict__[key]
    def save(self):
        model_fields = self.__fields__
        fields_key = list()
        fields_value = list()
        fields_placeholder = list()
        for field_key in model_fields:
            fields_key.append(field_key)
            fields_value.append(self.fields.setdefault(field_key, None))
            fields_placeholder.append("?")
        sql = "INSERT INTO %s (%s) VALUES (%s)" % (self.__table__, ", ".join(fields_key), ", ".join(fields_placeholder))
        print("SQL: ", sql)
        print("ARGS: ", fields_value)
  • Field类及其子类、模型类
class Field:
    pass
    
class CharField(Field):
    pass
    
class DateTimeField(Field):
    pass
    
class Question(Model):
    question_text = CharField()
    pub_date = DateTimeField()
  • ORM使用示例、执行输出
question = Question(question_text="My first question.", pub_date=datetime.now())
question.save()
# >> SQL:  INSERT INTO question (question_text, pub_date) VALUES (?, ?)
# >> ARGS:  ['My first question.', datetime.datetime(2020, 10, 13, 17, 52, 38, 443969)]

question.question_text = "My second question."
question.save()
# >> SQL:  INSERT INTO question (question_text, pub_date) VALUES (?, ?)
# >> ARGS:  ['My second question.', datetime.datetime(2020, 10, 13, 17, 52, 38, 443969)]

从打印输出可用看到,框架已经打印出了SQL语句和参数,只要提交到数据库就可以实际运行了。

结束语

通过以上几个简单的示例,相信大家对Metaclass的使用应该有了一个更深的了解,不过要理解代码,最好还是自己动手编码、调试,纸上得来终觉浅,绝知此事要躬行
得益于Python的动态语言特性的灵活性和精巧设计,我们通过不到100行代码,就实现了一个ORM框架原型,和Java等重量级编译型语言相比,各方面都要更胜一筹。
不过这只是一个简易的示例,实际框架还有很多的工作要做。

参考资料:

阅读 637

五岁时,妈妈告诉我,人生的关键在于快乐。上学后,人们问我长大了要做什么,我写下“快乐”。他们告诉我...

60 声望
7 粉丝
0 条评论

五岁时,妈妈告诉我,人生的关键在于快乐。上学后,人们问我长大了要做什么,我写下“快乐”。他们告诉我...

60 声望
7 粉丝
文章目录
宣传栏