深夜问道 Python 题,实在没想明白。求指点!

2017-02-23 01:23:15 +08:00
 Tianny

下面是廖雪峰 python 教程中的编写一个简易 ORM 框架的例子,附上源代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' Simple ORM using metaclass '

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)

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

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        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
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 保存属性和列的映射关系
        attrs['__table__'] = name # 假设表名和类名一致
        return type.__new__(cls, name, bases, attrs)

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

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

# testing code:

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.save()

其中编写元类时,下面这句不太理解:

for k in mappings.keys():
	attrs.pop(k)

为什么要在找到符合条件的 key-value 对并将其放入新的字典 mapping 后,还要将这些 key-value 对从 attrs 字典中去除? 我试验了一下,如果不这么做,最后的 ARGS 打印结果是

ARGS: [<__main__.IntegerField object at 0x10cf63630>, <__main__.StringField object at 0x10cf636d8>, <__main__.StringField object at 0x10cf63668>, <__main__.StringField object at 0x10cf636a0>]

而不是创建实例 u 是传入的参数的值,即

ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]

按我的理解,就算不执行 pop 操作,在创建实例 u 后,当执行 u.save()到

args.append(getattr(self, k, None))

这一步时,实例 u 的属性应该会自动覆盖类 User 的属性,按这个道理, getattr(self,k,None)返回的应该是传入的参数的值,即 my-pwd', 'test@orm.org', 'Michael', 12345 这些。 所以,实在没想通,到底为什么要执行元类中的 pop 操作。

3456 次点击
所在节点    Python
17 条回复
Tianny
2017-02-23 01:28:49 +08:00
为什么要在找到符合条件的 key-value 对并将其放入新的字典 mapping 后,还要将这些 key-value 对从 attrs 字典中去除?也就是在执行下面的操作时
```
for k in mappings.keys():
attrs.pop(k)
```
crossmaya
2017-02-23 03:54:56 +08:00
请看一下 dir 函数。
binux
2017-02-23 05:04:30 +08:00
如果不把这些属性从属性列表里面拿走,你访问 user.id 的时候不是就访问到 IntegerField('id') 了吗
LittleKey
2017-02-23 10:11:33 +08:00
因为__getattr__是在__getattribute__没找到时调用的吧,而 getattr 里会调用__getattribute__去找。
所以如果你不删掉的话就像 @binux 说的一样了
enenaaa
2017-02-23 10:51:55 +08:00
Model 继承于 dict 。 dict 里键值对不等于类定义的属性。
getattr 函数优先从类定义里面找, 找不到后才调用__getattr__。
在__getattr__打个 log 就看出来了。
Tianny
2017-02-23 12:02:20 +08:00
@enenaaa 请问为什么 getattr 函数优先从类定义里面找呢?
Tianny
2017-02-23 12:03:18 +08:00
@LittleKey 请问,为什么 getattr 函数会先调用__getattribute__去找呢?
enenaaa
2017-02-23 12:56:52 +08:00
@Tianny
用 getattr 函数或 a.b 形式访问类和对象的属性时, 先从类定义(__dict__中)获取,找不到的话则调用 __getattr__。

对于 dict , 则是另外一种机制,以键取值用 a['b']的形式。
这是两种不同的机制。 Model 类将对象 a.b 形式的操作转换为 a['b'], 想到这一点, 就不难理解了吧。
enenaaa
2017-02-23 13:03:21 +08:00
Model 类里 self.b 和 self['b'] 是两个不同的变量。 他们保存在不同的表里。
zhuangzhuang1988
2017-02-23 13:09:27 +08:00
python ORM/metaclass 推荐看这个,
http://www.dabeaz.com//py3meta/index.html
一个 ppt 解决你大部分问题
Tianny
2017-02-24 00:52:35 +08:00
@enenaaa 再次请教下,说一下我的思路,希望您能帮我看下,不对的地方请指出,万分感谢!要创建 User 类,先根据元类 ModelMetaclass 来创建它。当执行元类后,如果不执行 pop 操作,此时 User 类的 attrs 即属性集合是{id: IntegerField('id') ,__mapping__:{id: IntegerField('id') }},这里我只是拿出 id 举个例子。然后,当创建 User 类的对象 u 时,即执行 u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')这一步,需要初始化对象就是执行__init__,因为 User 类的父类是 Model ,同时 Model 类的__init__方法是调用 dict 的__init__方法,所以初始化实例 u 时,调用的是 dict 的__init__方法,所以根据传入的参数属性就变成了字典形式{id:12345}。最后执行 u.save(),当执行到 args.append(getattr(self, k, None))时,此时就像你前面说的“用 getattr 函数或 a.b 形式访问类和对象的属性时, 先从类定义(__dict__中)获取,找不到的话则调用 __getattr__”,因为前面没有执行 pop 操作, User 类中有属性 id ,那么 getattr(self,id,None)会首先到 User 类中查找 id 对应的属性值,为 IntegerField('id')。如果前面元类中执行 pop 操作的话, getattr(id)在 User 类中找不到,就会调用__getattr__,此时执行 return self[key],那么就返回初始化实例 u 后 key id 对应的值,即 12345.
Tianny
2017-02-24 01:03:35 +08:00
@enenaaa 还有个问题。
我在这句“ u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')”后面加上“ print(dir(u))”。
当在元类中不进行 pop 操作,打印结果是带有 id 即属性中是有 id 的。
当在元类中进行 pop 操作,打印的结果不带 id 即属性中没有 id 。
那么问题来了, dir(u)这个操作,作用是返回实例 u 的属性的吧?按我的理解,不管元类有没有 pop ,在初始化实例 u 后,实例 u 不是肯定有 id 这个属性吗?
求解释,非常感谢!
Tianny
2017-02-24 01:17:53 +08:00
@binux 请问为什么 如果不进行 pop 操作,就会访问到 IntegerField('id') 。"getattr(self, k, None)"这个代码的 getattr 是怎么调用的?不理解,希望您能详解给我讲解下。非常感谢!
binux
2017-02-24 02:23:15 +08:00
enenaaa
2017-02-24 09:37:49 +08:00
@Tianny pop 之后, 原先定义的 id 没有了。 Model 类又定义了__setattr__函数, 里面把键值对存到了 dict 类的表里, 而不是 User.__dict__里, dir 函数就列不出来了。
就像 dir(dict) 不能列出 dict 对象存储的键值对一样。
dict 是 C 写的内置对象, 里面的键值对用哈希表存储。
User.__dict__是类对象用来存储属性的一个 dict 。 而 Model 类本身继承于 dict , 所以这里用到了两个 dict 。
Tianny
2017-02-24 16:45:36 +08:00
@binux 3q 懂了!😄
Tianny
2017-02-24 16:52:07 +08:00
@enenaaa 非常感谢!懂了!😄

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/342476

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX