python:使用元类手撸orm框架

作者:vk
链接:https://0vk.top/zh-hans/article/details/55/
来源:爱尚购

何为ORM

ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

这里模拟一段用来用户注册用的代码,sign_up函数的功能即为实现用户注册

views.py

from my_models import User

def sign_up():
    u = User(id=1,name='vk', age=22)
    u.save()
    print('成功注册用户')

sign_up()
image.png

先不用管内部是怎么实现的,可以看到表User中新建了一条id为1,name=vk,age=22,的数据。上面代码中<mark class="pen-red" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">u = User(id=1,name='vk', age=22) </mark>只需要设置参数即可实现插入数。

这就成功实现了对象关系映射即orm。不需要手写一条sql语句即可非常简便的操作数据库

字段设计

数据库中有整型,字符串型,这个字段的值是否唯一等各种属性,那么orm中也应该设计出对应的字段。为了方便管理,<mark class="marker-yellow" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">我们新建一个py文件my_models</mark> 。在这个文件里定义我们需要设计的数据字段

my_models .py

import models

class User(models.Model):
    id = models.IntegerField('id')
    name = models.CharField('name')
    age = models.IntegerField('age')

现在,这个类就和数据库完美对应起来了,<mark class="pen-green" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">类名=表名,类属性=字段名</mark>。还可以设置这个字段是属于整型还是字符串型。

<mark class="marker-yellow" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">新建一个叫fields.py的py文件</mark>,用来定义各种类型的字段。目前目录是这样的

C:.
│  fields.py
│  my_models.py
│  views.py

fields.py

class Field:
    def __init__(self, name: str, column_type):
        self.name = name
        self.column_type = column_type

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

    def check(self, value):
        if isinstance(value, str):
            return str(value)
        else:
            raise TypeError('a varchar type is required not ":%s"'%value)

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

    def check(self, value):
        if isinstance(value, int):
            return int(value)
        else:
            raise TypeError('an int type is required not %s'%value)

__all__ = ['CharField', 'IntegerField', 'Field']

逻辑解析

每个字段肯定有它的名字,类型。所以在父类Field中定义两个初始值name ,column_type 。各个具体的字段继承这个Field,为了检查输入值是否合法,给子类中加入check函数。这样就不会存在定义了一个IntegerField结果传入了一个字符串类型的值这种问题了。

最后一行all是为了优化使用者的体验,比如:from xxx import * ,如果在xxx里自定义 了all那么其他使用的人使用时只会导入all中定义的东西。

mro和super()

子类init中的super(CharField, self).init(name, 'varchar'),会根据mro机制寻找到上一个类的init参数。

所谓MRO即Method Resolution Order(方法解析顺序),即在调用方法时,会对当前类以及所有的基类进行一个搜索,以确定该方法之所在,而这个搜索的顺序就是MRO。Python 类是支持(多)继承的,一个类的方法和属性可能定义在当前类,也可能定义在基类。

针对这种情况,当调用类方法或类属性时,就需要对当前类以及它的基类进行搜索,以确定方法或属性的位置,而搜索的顺序就称为方法解析顺序。最近的版本中用的是c3算法,感兴趣的可以自己了解一下。

我们将CharField这个类的mro打印出来可以看到,CharField的mro中上一个就是Field。所以这句话就是找到父类的init并且传入了两个参数name,和'varchar'。这个name是变量我们在my_models.py设计字段时传入的参数。

image.png

其实在类内部的话可以直接将super(CharField, self).init()写为super().init(),python会帮我们自动找到。

super()不是函数,也不是一个python内置方法,它是一个类

image.png

不光是在类里面继承时用super().__init()这种,你可以在任何地方使用它,因为使用它的过程无非就是实例化一个类罢了。

字段设计已经成功实现,现在最主要的来了,如何实现sql语句与相应方法的对应。我们拿插入举例

元类实现models

<mark class="marker-yellow" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">新建一个叫models.py的py文件</mark>,里面用来存放实现具体的sql语句,并且在db.sqlite3新建一个叫user的表,因为我们演示的是插入,其他功能暂未实现

import sqlite3

conn = sqlite3.connect('db.sqlite3')

## 创建一个表 - User
conn.execute('''CREATE TABLE company
       (ID INT PRIMARY KEY     NOT NULL,
       NAME           TEXT    NOT NULL,
       AGE            INT     NOT NULL);''')

conn.close()

现在的的目录是这样的

C:.
│  db.sqlite3
│  fields.py
│  models.py
│  my_models.py
│  views.py

<mark class="pen-green" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">完整代码</mark>

先上代码然后再一步一步解析其中的逻辑

models.py

from fields import __all__ as fields_all
from fields import *

import sqlite3

conn = sqlite3.connect('db.sqlite3')

# ## 创建一个表 - User
# conn.execute('''CREATE TABLE company
#        (ID INT PRIMARY KEY     NOT NULL,
#        NAME           TEXT    NOT NULL,
#        AGE            INT     NOT NULL);''')
#

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):

        mapping = {}
        for k, v in attrs.items():
            if isinstance(v, Field):
                mapping[k] = v
        for k in mapping.keys():
            attrs.pop(k)
        attrs['__mapping__'] = mapping
        attrs['__table__'] = name

        return type.__new__(cls, name, bases, attrs)

class Model(metaclass=ModelMetaclass):

    def __init__(self, **kw):
        print(self.__mapping__)
        self.args = kw
        super(Model, self).__init__()

    def __getattr__(self, key):

        raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def save(self):

        fields = []
        args = []
        for k, v in self.__mapping__.items():
            fields.append(k)
            value = v.check(self.args[k])
            args.append(value)
        for i in range(len(args)):
            if isinstance(args[i],str):
                args[i]="'"+args[i]+"'"
            else:
                args[i]=str(args[i])
        conn.execute(f"insert into {self.__table__} ({ ','.join(fields)}) values ({','.join(args)})")
        conn.commit()
        conn.close()

__all__ = fields_all + ['Model']

元类

首先来看python中如何定义一个类:

class A:
        pass

这样我们就可以静态的定义一个最简单的类A了,除此之外,还可以动态的定义一个类A,使用type来生产一个新的A

A=type("A",(),{})

我们最常用的type方法是源码 注释中的第二种,返回object's type。

image.png
a='1'
print(type(a))
<class 'str'>

如果有一天,我们不喜欢这个type生产出来的类了,想要自定义一种生产方式,用函数v生产。

想象一下面向对象中想要有一个类的功能而且还要自定义一部分该怎么做?

没错,继承!只需要将这个类V继承type,就可以既有它的功能又可以拓展了

class V(type):
        pass
A=V("A",(),{})

上面这个是动态生成类A的写法,静态生成是这么写的:

class V(type):
        pass
class A(metaclass=V):
    pass

<mark class="marker-yellow" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">所以,有啥用呢?</mark>

我们知道,new这个魔法方法是在init之前执行的,在object被创造出来之前就执行了,这么说,其实是可以通过new来充当A生命周期的钩子,类似于vue中的beforeCreate等钩子函数

class V(type):
    def __new__(cls, name, bases, attrs):
        print(cls,name,bases,attrs)
        return type.__new__(cls, name, bases, attrs)

class A(metaclass=V):
    def do():
        pass
A()
#结果
<class '__main__.V'> A () {'__module__': '__main__', '__qualname__': 'A', 'do': <function A.do at 0x0000017E6E5F7D00>}

可以看到,在A被创建之前,我们是可以通过元类获取到它的一些信息的,比如类名,类属性等。这样就可以进行相应的操作了,比如:我想让我的v这个元类生成出来的类中的函数名字不包含数字,只需要判断attrs函数名字就行,包含数字主动raise 一个错误。

class V(type):
    def __new__(cls, name, bases, attrs):
        for i in attrs.keys():
            print(i)

            if i.isalnum():
                raise TypeError('类型错误')
        return type.__new__(cls, name, bases, attrs)

class A(metaclass=V):
    def do1():
        pass
A()
image.png

需要用元类的场景

一个比较常见的场景就是,当继承无法简单解决问题的时候需要用元类,而继承无法解决问题的很多时候就是你写一个框架给别人用的时候,而你都到写自己框架并且继承无法解决的时候了,那么这时候你肯定是个大佬。

所以Tim Peters说

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why). - Tim Peters

元类是更深的魔法比99的用户应该担心过。如果你想知道是否你需要他们,你不(真正需要的人肯定地知道,他们需要他们,不需要解释为什么)。--彼特斯

这个Tim Peters就是写python之禅 的人。还是相当牛逼的人物

image.png

元类实现ORM

回到我们的orm框架,在my_models.py这里

image.png

继承了models,我们想在user类生成的时候知道id = models.<mark class="pen-red" style="box-sizing: border-box; padding: 0.2em; background-color: rgb(252, 248, 227);">IntegerField</mark>('id'),这里引用的字段是什么类型。显然它用继承完成不了。但是可以在元类里new中的参数获取了

image.png

可以清楚的看到字段的类型,name。还有类的名字也就是表的名字User。

接下来只需要给但凡是ModelMetaclass这个元类生成的类添加两个属性:mapping和table分别记录字段对应的类型和表名。

    attrs['__mapping__'] = mapping
        attrs['__table__'] = name

这个元类生成的类Model中,只需要常规的用获取属性值的方法self.mapping就可以获取到。字段对应的关系

属性解决了,传入的值(1,'vk,22)就好办了

    u = User(id=1,name='vk', age=22)

在Model初始化的时候就传入参数即可

image.png

取出想要的值简单拼接一下即可生成想要的sql语句

image.png

但是在真正使用的时候不要sql=xxx这种拼接,有可能会出现sql注入。

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

推荐阅读更多精彩内容