Discuss / Python / 用类的继承关系试着梳理一下ORM的实现,以及为什么要用attr.pop(k)的原因

用类的继承关系试着梳理一下ORM的实现,以及为什么要用attr.pop(k)的原因

Topic source

alienation

#1 Created at ... [Delete] [Delete and Lock User]

首先ORM作为结果是为了将SQL的表结构映射为在python更直观的类-对象结构

有如下的对应关系:

表<——>某一类

表行<——>该类的一个实例

表列<——>该类的某一个属性

表某一行的某一列元素<——>该类某一实例对应属性的一个特殊值

(表间关系这里没讲我也不知道咋映射的)

所以最前端的类是属性使用的数据类型类xxxField

这些类都是由object-Field这个树继承下来的

class StringField(Field):
#这些数据类型类自己只有一个定义列名的方法其他方法从Field继承
    def __init__(self, name):
#这里的super()用于继承Field的初始化方法,只是特别定义了一个数据类型,表现为第二个参数
        super(StringField, self).__init__(name, 'varchar(100)')
class Field(object):
#这里的Field作为所有xxxField的基类,定义了一般的初始化方法和定制了打印这些类的形式
    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)

然后所有这些数据类型类xxxField都以嵌套方式用在被映射为类的表(如User)中

实现表列<——>类的属性的映射

class User(Model):
    #定义类的属性到列的映射,这里的输入的参数即field初始化的name位置参数
    #等号左边是定义的列名,等号右边是以该列名为name参数的一个数据类型实例
    #通过该实例化,此id实例具有field的__str__定制方法,print(u.id) 预期输出为:<IntegerField:id>
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

# 创建一个实例:
#这里的实例化格式是从dict类继承的,由于user类没有限制键的格式,所以输入没有被分配数据类型的键也是可以的,但是没有意义
#因为实例化的对象之后在保存时需要经过几道程序检验是否落在定义好的数据类型范围,没有的话就不保存
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
#这里的save()方法是从Model类继承的
u.save()

接着是User类的继承树

User—Model—(metaclass=Modelmetaclass)—dict

User的父类是Model和dict,dict作为最底端的类给所有由表映射而来的类提供了一般字典方法

Model作为基类为所有由表映射而来的类(如User)提供了getattr、setattr、save三个一般方法

class Model(dict, metaclass=ModelMetaclass):

    def __init__(self, **kw):
        super(Model, self).__init__(**kw)
#给了所有由表衍生的类一个获取方法和一个修改方法
    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
#给了所有由表衍生的类一个保存方法,mapping是由元类metamodelclass继承来的一个方法,该方法能够返回一个字典
#这个字典的键值对是该衍生类(如user)定义时的等号左为键右为值
#然后根据键值对生成一个符合sql语法的语句,执行后就把该实例保存在sql里了
#下面几个参数的注释:v.name:v是user类中等号右边的东西.name就是获取该对象初始化时的参数(如'id')
#?是sql的占位符,用来匹配后面的args
#getattr(self.k.None)self是该表,getattr就是获取该表以k为属性的具体值,即实例化时的id=xxx中的xxx,k是该类定义时等号左边的东西
#self.table、self.mappings都是在元类中生成的方法
    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))

  然后就是元类modelmetaclass了,根据mro方法,这里的modelmetaclass其实不是后续model和user的父类

他只是劫持了这几个类的创建过程,往里加了一个过滤属性的机制并且把合法的属性转换成了字典映射(不合法的不进入映射),删去了用来确定合法属性的属性

新增了mapping和table两个映射方法,然后以这两个为真正的方法返回到创建类的过程,所以user真正的方法其实是mapping和table

看mapping的具体实现

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
#这里如果发现创建的本身就是基类Model,就不加修改跳过
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
#否则就会进入过滤机制,最终是要把由表衍生来的类中的定义语句转换成映射形式,经过定义的属性就进入映射,那些没有在类内部定义过数据类型的属性就不进入映射
#如u=user(score=100)这里的score就不进入映射,这样也就不会在save中被迭代
#这里的attr就是衍生类内部的定义语句等号两边的东西,左边为键,右边为值
#如果值落在定义域中,就会进入映射,如果没有就略过
        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类型派生下来的user对象而言
#user.name,本来是要返回实例化时user(name=xxx)的xxx的,但是由于定义类型时保留了
#name = stringfield(‘name’)
#结果变成返回name属性值stringfield(‘name’)了
        for k in mappings.keys():
            attrs.pop(k)
#最后写上通用方法用来在save时迭代,然后形成sql语句
        attrs['__mappings__'] = mappings # 保存属性和列的映射关系
        attrs['__table__'] = name # 假设表名和类名一致
        return type.__new__(cls, name, bases, attrs)

alienation

#2 Created at ... [Delete] [Delete and Lock User]

另外评论区好像以为save()里面的getattr是model里定义的那个,其实那个getattr是python的内置函数,看参数格式就看出来区别了,

后者是getattr(obj,attr),返回该属性的值

前者是getattr(key),返回的是该键的值

相比起来前者少个obj位置参数

alienation

#3 Created at ... [Delete] [Delete and Lock User]

啊不对,是我搞错了,getattr相当于覆写了getattr方法

根据是否注释pop(k)和def getattr这两个操作列个矩阵

1:两个都注释,args返回为ARGS: [<__main__.IntegerField object at 0x000001CB37837460>, <__main__.StringField object at 0x000001CB37837400>,

相当于把id = integerfield('id)中的integerfield(‘id’)作为getattr的返回值放入args了,这是没删去定义域的缘故,此时调用的getattr是内置函数getattr

2,两个都不注释, args 返回为ARGS: [10115, 'Michael', 'test@orm.org', 'my-pwd']

这时候getattr返回的是以id为键的对应值10115,符合预期,此时调用的getattr是覆写的getattr,因为没有属性等等,所以内置函数getattr跑不通,由自定义的getattr顶上

3,只注释def getattr ,args返回为[None, None, None, None],这时调用的是内置函数getattr(定义的getattr注释掉了),因为没有对应属性(被pop删了),所以返回为默认值None

4,只注释pop(k), args返回为ARGS: [<__main__.IntegerField object at 0x000001CB37837460>, <__main__.StringField object at 0x000001CB37837400>,这时调用的是内置函数getattr

说明在两个getattr都能跑通的时候优先调用的是内置的getattr,这时候直接用u['id']可以拿到值,只是定义的getattr没有运行

那这样就有个比较神奇的地方,迭代里的getattr是有三个参数(self,k,None),但是自定义的getattr只有两个(self,key)

结果运行自定义getattr(self,k,None)的时候居然没问题

三千

#4 Created at ... [Delete] [Delete and Lock User]

关于getattr使用和pop的原因,楼主可能解释有失偏颇,__getattr__不是复写了getattr的方法,显然两者的参数也不一样。

1. getattr方法:

获取实例属性,如果没有实例属性则获取实例对应类的属性【这里是实验得到的,没有查官网,和楼主说的一样~】。如果类和实例都不具备该属性,python自动调用特殊函数__getattr__。

2. __getattr__:

官网描述为Called when the default attribute access fails with an AttributeError (either __getattribute__() raises an AttributeError because name is not an instance attribute or an attribute in the class tree for self; or __get__() of a name property raises AttributeError). This method should either return the (computed) attribute value or raise an AttributeError exception.
也就是说在获取不到实例属性和类属性后,getattr会raise AttributeError。随后python会将属性名传入__getattr__,若用户定义类时重写了__getattr__,则按照用户定义的方式返回结果。
根据官网要求,__getattr__要么返回结果,要么继续raise AttributeError。

3. 此处的用法:

首先metaclass在__new__函数中更改的均为类属性,而非实例属性。其次在定义User时,也是定义的类属性。所以User对应的实例暂时没有实例属性。

如果在__new__中没有删除User定义时的attrs,则获取User实例的属性时,会返回对应类属性。【因为Field只定义了__str__,在运行时返回的具体Field信息,像这样:<__main__.IntegerField object at 0x000001E941E13B00>]

如果删除了User定义时的attrs,即使用attr.pop(k),按照getattr调用的规定,最终会调用自定义的__getattr__方法.

4.在__getattr__中self[key]的使用:

因为Model继承自dict,在初始化时将实例属性保存为字典,因此实例(self)有字典的方法,如__getitem__,所以可以通过self[key]调用.


  • 1

Reply