python-进阶-元类在ORM上的应用详解

ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。

要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。

让我们来尝试编写一个ORM框架。

编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:
期望代码

class User(Model):      #  User类,继承Model
    # 定义类的属性到列的映射: 右边的 StringField(‘username’),这里StringField是类,类(‘username’)是输入__init__绑定的参数,所以得到的是一个实例,正好对应value
    id = IntegerField('id')     # ⚠️id理解为一个变量不好,id理解成一个key最恰当,=右边的则是value,这里对应的就是数据库字典的id数据
    name = StringField('username')    # name是类属性,具体可参考类属性与实例属性定义,类调用属性就是User.name 
    email = StringField('email')       # id name email password 均是User的属性,User的实例可以直接调用
    password = StringField('password')  # User().name = StringField(‘username’),直接调用,也就是对应一个user的表,表里的name叫SF()

# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')     # ⚠️对照这个效果可以想通一二了
# 保存到数据库:
u.save()

其中,父类Model和属性类型StringField、IntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。

现在,我们就按上面的接口来实现该ORM。

首先来定义Field类,它负责保存数据库表的字段名和字段类型

class Field(object):    # 对应数据库中保存的字段名和字段类型

    def __init__(self, name, column_type):     # 添加__init__(self,name)方法后,类的实例必须是StringField(‘某name’),正好呼应上方的期望代码
        self.name = name    # 字段名
        self.column_type = column_type    # 字段类型

    def __str__(self):   # 输入print(xxx)会自动调用__str__,它为了使打印结果更好看而已,Python 定义了__str__()和__repr__()两种方法,__str__()用于显示给用户,而__repr__()用于显示给开发人员。

        return '<%s:%s>' % (self.__class__.__name__, self.name)    # 不使用print(xxx)而是直接输入实例xxx,则该行不会打印显示出来,看起来就很丑

在Field的基础上,进一步定义各种类型的Field,比如StringField,IntegerField等等:

class StringField(Field):     # ✌️通过比对测试,StringField.name = name, StringField.column_type = varchar(100).

    def __init__(self, name):     # 实例属性,`实例.name`调用
        super(StringField, self).__init__(name, 'varchar(100)')   # 为什么这么设计?super最后跟的参数是(name,’varchar(100)’)一个变量name,一个值。为了就是不让人去修改 column_type这个参数而已
         # ⚠️super的用法,继承父类的方法而自己无需定义, super(StringFileld,self).__init__这段是套路,都一样,super后面跟的子类名+self,只有最后的(name,’varchar(100)’)表示继承父类的哪些属性
------------------------测试------------------------------
In [67]: test = StringField('love')

In [68]: type(test)
Out[68]: __main__.StringField

In [70]: test.column_type
Out[70]: 'varchar(100)'

In [71]: test.name
Out[71]: 'love'
------------------------------------------------------------
class IntegerField(Field):       # ⚠️子类继承这些方法

    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')   # __init__()这样的写法,可参考我的super文章





下一步,就是编写最复杂的ModelMetaclass了:(重点 难)

class ModelMetaclass(type):
    # 元类必须实现__new__方法,当一个类指定通过某元类来创建,那么就会调用该元类的__new__方法
    # 该方法接收4个参数
    # cls为当前准备创建的类的对象 
    # name为类的名字,创建User类,则name便是User
    # bases类继承的父类集合,创建User类,则base便是Model
    # 🐷attrs廖神说是类的方法的集合,是类方法!类方法是一种函数,怎么集合?所以集合的key应是方法名,而value应该是 Field类 !!!可看最下方的打印结果 !

    def __new__(cls, name, bases, attrs):    # 新建new,所以后面4个参数都是类相关的
        if name=='Model':
        # 因为Model类是基类,所以排除掉,如果你print(name)的话,会依次打印出Model,User,Blog,即
        # 所有的Model子类,因为这些子类通过Model间接继承元类
            return type.__new__(cls, name, bases, attrs)
        print('Found model: %s' % name)   # 打印提示

        mappings = dict()   #  用于存储所有的字段,以及字段值,注意是dict(),而不是[ ],所以后面的语法才能实现,参考http://www.jianshu.com/p/2ecaddc66100
        for k, v in attrs.items():     # 注意这里attrs的key是字段名,value是字段实例,不是字段的具体值
            if isinstance(v, Field):       # 筛选 v,判断v是否是Field
            # attrs同时还会拿到一些其它系统提供的类属性,我们只处理自定义的类属性,所以判断一下
            # isinstance 方法用于判断v是否是一个Field,只处理自定义的类属性
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v   # 经测试,语法正确,attrs字典移植到mappings里
        for k in mappings.keys():    # mappings.keys会打印出所有的key
            attrs.pop(k)    # pop删除k,删除mappings含有的所有的字段名,v是字段实例
        attrs['__mappings__'] = mappings # 保存属性和列的映射关系,猜测是mappings已经确定了,我把它赋值给attrs[xxx],attrs是字典格式,dict[x]是取字典value,取出后,被赋值。类似于 Student.name = jack,那么Student这个类就有了个类属性,子类可继承。
            # '__mappings__'是字符串,属于我们想要的属性,属性很多,想要的就这几个,其他全pop,attrs是类属性,若我们保留,实例属性覆盖属性,容易造成错误
        attrs['__table__'] = name # 假设表名和类名一致
            # 以上都是要返回的东西了,刚刚记录下的东西,如果不返回给这个类,又谈得上什么动态创建呢?
            # 到此,动态创建便比较清晰了,各个子类根据自己的字段名不同,动态创建了自己
            # 上面通过attrs返回的东西,在子类里都能通过实例拿到,如self
        return type.__new__(cls, name, bases, attrs)

#  一次复盘后的猜想:我们先看最上方的User,这个就是我们需要的效果,也正是我们 元类 __new__方法 可以创建的结果。
 那么,我们想,__new__(cls, name, bases, attrs)里的attrs属性应该对应的是元类属性,也就是说元类的attrs作为子类是可以继承的。
但是元类的属性继承虽然可以,因为在元类基础上定义的子类属性应该是动态创建的,所以属性的名称可能会不同,这个属性名怎么更换?我猜这恰恰是  attrs.pop(k) 需要做的事情。

#  廖-把类看成是metaclass创建出来的“实例”
#   类的方法(类中def的就是类方法,实例中def的是函数)就是类实例的属性。
所以,我们定义元类的方法就是给 类增加属性,这样类通过 __new__ 创建后自带属性

#  参第一块代码User,在创建User类时,中name == User, bases == Model, attrs 是即将创建的类 User的类属性
   相当于在attrs处传入了  字典{  'id': IntegerField('id'),  'name': 'StringField('username'),  'email': StringField('email),  'password': StringField('password')    };
   那么for k, v in attrs.items()中,k 为'id'等(赋值号左边),'v'为IntegerField('id')实例等(赋值号右边);
   在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性)

以及基类Model:

class Model(dict, metaclass=ModelMetaclass):
# 让Model继承dict,主要是为了具备dict所有的功能,如get方法
# metaclass指定了Model类的元类为ModelMetaClass
    def __init__(self, **kw):    
        super(Model, self).__init__(**kw)       # 继承父类的init 

    def __getattr__(self, key):    # 定义实例属性 目标:获得属性
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)
            # 实现__getattr__与__setattr__方法,可以使引用属性像引用普通字段一样  如self['id']  
    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

廖神解说:
当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找metaclass,如果没有找到,就继续在父类Model中查找metaclass,找到了,就使用Model中定义的metaclass的ModelMetaclass来创建User类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。

在ModelMetaclass中,一共做了几件事情:

  1. 排除掉对Model类的修改;

  2. 在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性);

  3. 把表名保存到__table__中,这里简化为表名默认为类名。

在Model类中,就可以定义各种操作数据库的方法,比如save()delete()find()update等等。

我们实现了save()方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT语句。

编写代码试试:

u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()

输出如下:

Found model: User
Found mapping: email ==> <StringField:email>   # 注意⚠️,从结果导向,k 为 email,v 为<StringField:email> 也就是说,我上面对k,v的判断都是错的!
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
# 从结果可以看出:self.__table是User; .join(fields)是(password,email,username,id); .join(params)是(?,?,?,?)
# 进而推导fields.append(v.name),那么v.name就是(password,email,username,id);推导args.append(getattr(self, k, None)),那么agrs就是 getattr后的key,也就是展示key的属性。
# getattr是python中自省功能,即返回展示对象的属性

可以看到,save()方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能



12月6日补充

在ModelMetaclass 里为什么会有pop()这样的操作?请注意,我们初识定义的时候,def __new__(cls, name, bases, attrs)中的 attrs 是 类的方法的集合(廖),类的方法,也就是实例的方法,那么方法有很多种,一部分方法是要归纳到 数据库 中,并建立映射mappings,所以对应的 原来的类的方法的集合 attrs 就该删除这部分已经归纳的,防止出现意外,导致数据库 数据错误。
这两段代码里的v都是在类User定义时的实例化Field对象,注意v不是字符串,虽然在第二段代码里输出了字符串,那是在Field定义时有函数__str,输出的时这个函数返回的字符串,所有的Field实例都可以这样返回字符串

然后剩下的类方法,再 return type.__new__(cls, name, bases, attrs),此时的attrs里已经没有了那些属于 Field 类的 value 了。

廖大-----在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个mappings的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性)

这里廖大又归结为类的属性,其实在使用上来说,类属性与类方法差别不大,除了类属性 类可以直接调用,面对实例时它们用法相同,如下,还真的可以判断是类的属性,所以现在初始的 attrs ,我可以认为它是类属性和类方法的合集

class User(Model):
    # 定义类的属性到列的映射:
    id = IntegerField('id')
    name = StringField('username')



网友总结:

1、这两段代码里的v都是在类User定义时的实例化Field对象,注意v不是字符串,虽然在第二段代码里输出了字符串,那是在Field定义时有函数__str__,输出的时这个函数返回的字符串,所有的Field实例都可以这样返回字符串

     for k, v in self.__mappings__.items():
        fields.append(v.name)
        params.append('?')
        args.append(getattr(self, k, None))

     for k, v in attrs.items():
        if isinstance(v, Field):
            print('Found mapping: %s ==> %s' % (k, v))
            mappings[k] = v



2、这两段代码里‘=’的意义不一样,第一段是赋值,构建class ModelMetaclass里的attrs,第二段是在构建一个dict,也就是class Model(dict, metaclass=ModelMetaclass)里用到的**kw

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')



3、这两段代码中关于super的用法表示调用父函数的__init__方法进行初始化类的实例

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

def __init__(self, **kw):
    super(Model, self).__init__(**kw)



4、这段代码,可以知道User的任何实例已经没有属性id,name,email,password了,不过有属性__mappings__,这里保存了id,name,email,password的信息,还有__table__保存了类名也就是数据库的表名,然后还有对应表每一行信息的的dict,如id=12345, name='Michael', email='test@orm.org', password='my-pwd'

    for k in mappings.keys():
        attrs.pop(k)
    attrs['__mappings__'] = mappings   # 保存属性和列的映射关系
    attrs['__table__'] = name          # 假设表名和类名一致

5、再次在评论里看到Michael Liao的回复,终于算是明白这句话了!

metaclass 负责创建类,类 负责创建实例---廖大

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容