【python进阶 笔记】元类Metaclasses、实现ORM
目录
2.元类实现ORM (Object Relational Mapping)
1. 元类
元类就是用来创建类的“东西”,元类创建类,类创建实例对象。
1.1. 类也是对象
在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这一点仍然成立:
以下在ipython3中,通过类生成了一个对象并输出:
>>>class ObjectCreator(object):
… pass
…
>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>
但是,Python中的类还远不止如此。类同样也是一种对象。只要使用关键字class,Python解释器在执行的时候就会自动创建一个对象。
执行以下代码段:
class ObjectCreator(object):
pass
python将在内存中自动创建一个对象,名字就是ObjectCreator。
这个对象(类对象ObjectCreator)拥有创建对象(实例对象)的能力。但是,它的本质仍然是一个对象,可以对它做针对对象的操作:
- 赋值给一个变量
- 拷贝它
- 为它增加属性
- 作为函数参数进行传递
注:
- globals() 函数会以字典类型返回当前位置的全部全局变量,包括所有全局对象的引用。即返回的字典会包含了python定义好的变量,也包含了自己定义的变量,可以直接使用。
- 想查看一个类或模块里面有什么,可以使用 类.__dict__ 或 模块.__dict__ 。
- globals()的返回有一个内建模块 __builtins__ ,里面包含了常用的print 等对象(__builtins__.__dict__可见 )。
- 用一个变量名或函数名时,先直接到globals()返回的字典里面找,找不到再到字典的内建模块 __builtins__ 去找。
1.2. 动态的创建类
因为类也是对象,可以在运行时动态的创建它们,就像其他任何对象一样。可以在函数中创建类,使用class关键字。
def choose_class(name):
if name == 'foo':
class Foo(object):
pass
return Foo # 返回的是类,不是类的实例
else:
class Bar(object):
pass
return Bar
MyClass = choose_class('foo')
print(MyClass) # 函数返回的是类,不是类的实例
print(MyClass()) # 你可以通过这个类创建类实例,也就是对象
# 输出:
# <class '__main__.choose_class.<locals>.Foo'>
# <__main__.choose_class.<locals>.Foo object at 0x7fa8b5ac2f98>
根据传递的参数不一样,返回的类的引用不一样,然后导致创建的实例对象不一样。但这还不够动态,因为仍然需要自己编写整个类的代码。可以使用type动态的创建类。(注:type函数功能之一能够返回一个对象的类型,如print(type(1)) 会打印<type 'int'>)
1.3. 使用type创建类
type还有一种完全不同的功能,即动态的创建类。函数type实际上是一个元类。
type可以接受一个类的描述作为参数,然后返回一个类对象。
type(类名, 由元组组成的父类名称(针对继承的情况,可以为空),包含属性的字典(名称和值))
以下分别用两种方法创建了类Test1和类Test2:
In [13]: class Test:
....: num = 100
....: num2 = 200
....:
In [14]: Test2 = type("Test2", (), {"num":100, "num2":200})
使用help来测试这2个类:
In [16]: help(Test) # 用help查看Test类
Help on class Test in module __main__:
class Test(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| num = 100
|
| num2 = 200
In [18]: help(Test2)
Help on class Test2 in module __main__:
class Test2(builtins.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| num = 100
|
| num2 = 200
可见,二者是一样的。
1.4. 使用type创建复杂的类
1.4.1. type创建带有继承的类
Test11继承于Test,Test22继承于Test2.
In [1]: class Test:
num = 100
num2 = 200
...:
In [2]: Test2 = type("Test2", (), {"num":100, "num2":200})
In [3]: class Test11(Test):
...: pass
...:
In [4]: Test22 = type("Test22", (Test2,), {})
再使用help查看,两个类是一样的。
1.4.2. 添加实例方法
In [15]: class Foo(object):
....: bar = True
....:
In [16]: def echo_bar(self): # 定义了一个普通的函数
....: print(self.bar)
....:
In [17]: FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) # 让FooChild类中的echo_bar属性,指向了上面定义的函数
In [18]: hasattr(Foo, 'echo_bar') # 判断Foo类中 是否有echo_bar这个属性
Out[18]: False
In [19]: hasattr(FooChild, 'echo_bar') # 判断FooChild类中 是否有echo_bar这个属性
Out[19]: True
In [20]: my_foo = FooChild()
In [21]: my_foo.echo_bar()
True
1.4.3. 添加静态方法
在上面代码的基础上
In [36]: @staticmethod
...: def test_static():
...: print("static method ....")
...:
In [37]: Foochild = type('Foochild', (Foo,), {"echo_bar": echo_bar, "test_static": test_static})
In [38]: fooclid = Foochild()
In [39]: fooclid.test_static
Out[39]: <function __main__.test_static>
In [40]: fooclid.test_static()
static method ....
In [41]: fooclid.echo_bar()
True
1.4.4. 添加类方法
在上面代码的基础上
In [42]: @classmethod
...: def test_class(cls):
...: print(cls.bar)
...:
In [43]: Foochild = type('Foochild', (Foo,), {"echo_bar":echo_bar, "test_static": test_static, "test_class": test_class})
In [44]: fooclid = Foochild()
In [45]: fooclid.test_class()
True
1.4.5. 较为完整的使用type创建类
class A(object):
num = 100
def print_b(self):
print(self.num)
@staticmethod
def print_static():
print("----haha-----")
@classmethod
def print_class(cls):
print(cls.num)
# B继承于A,
B = type("B", (A,), {"print_b": print_b, "print_static": print_static, "print_class": print_class})
b = B()
b.print_b()
b.print_static()
b.print_class()
# 结果
# 100
# ----haha-----
# 100
1.5.元类定义
- 元类就是用来创建类的“东西”。创建类是为了创建类的实例对象。
- 但是Python中的类也是对象,元类就是用来创建这些类(对象)的,元类就是类的类。
- 即元类创建类,类创建实例对象。
MyClass = MetaClass() # 使用元类创建出一个对象,这个对象称为“类”
my_object = MyClass() # 使用“类”来创建出实例对象
前面用到的函数type实际上是一个元类。type就是Python在背后用来创建所有类的元类。
- type就是创建类对象的类。可以通过检查__class__属性来验证这一点。
(注:实例调用__class__属性时会指向该实例对应的类(即那个类创建了该实例),还可以再去调用其它类属性)
- Python中所有的东西,都是对象。包括整数、字符串、函数以及类。它们全部都是对象,而且它们都是从一个类创建而来,这个类就是type。
In [24]: class T(object):
....: pass
....:
In [25]: t = T()
In [26]: t.__class__
Out[26]: __main__.T
# 以上可见t由当前模块的T创建
In [27]: t.__class__.__class__
Out[27]: type
# 以上可见T由type创建 (注:type还是type创建的)
1.6. __metaclass__属性
定义一个类时,可以为其添加__metaclass__属性。定义了__metaclass__就定义了这个类的元类。
class Foo(object): # python2
__metaclass__ = something…
...省略...
class Foo(metaclass=something): # python3
__metaclass__ = something…
这样,Python就会用元类来创建类Foo。执行到语句class Foo(object):时,类Foo还没有在内存中创建。Python会在类的定义中寻找__metaclass__属性,如果找到了,Python就会用它来创建类Foo,如果没有找到,就会用内建的type来创建这个类。
如,以下代码 :
class Foo(Bar): pass
Python做了如下的操作:
- Foo中有__metaclass__这个属性吗?如果是,Python会通过__metaclass__创建一个名字为Foo的类(对象)
- 如果Python没有找到__metaclass__,它会继续在Bar(父类)中寻找__metaclass__属性,并尝试做和前面同样的操作。
- 如果Python在任何父类中都找不到__metaclass__,它就会在模块层次中去寻找__metaclass__,并尝试做同样的操作。
- 如果还是找不到__metaclass__,Python就会用内置的type来创建这个类对象。
问:在__metaclass__中放置些什么代码呢?
答:可以创建一个类的东西。那么什么可以用来创建一个类呢?type,或者任何使用到type或者子类化type的东东都可以。
1.7. 元类应用
元类的主要目的就是为了当创建类时能够自动地改变类。
创建类时,metaclass用来标记元类是谁,不写metaclass会默认调用type来创建类。
元类所做的事情概括起来就是:
- 拦截类的创建
- 修改类
- 返回修改之后的类
demo1:
假设,你决定在你的模块里所有的类的属性都应该是大写形式。方法之一就是通过在模块级别设定__metaclass__。采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式就万事大吉了。
幸运的是,__metaclass__实际上可以被任意调用,它并不需要是一个正式的类。
例子如下,以下也相当于自定义了一个元类。
#-*- coding:utf-8 -*-
def upper_attr(class_name, class_parents, class_attr):
#遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name,value in class_attr.items():
if not name.startswith("__"): # 不是下划线开头时
new_attr[name.upper()] = value
#调用type来创建一个类
return type(class_name, class_parents, new_attr)
class Foo(object, metaclass=upper_attr):
bar = 'bip'
print(hasattr(Foo, 'bar'))
print(hasattr(Foo, 'BAR'))
f = Foo()
print(f.BAR)
以上,类名Foo传给了class_name,父类object传给了class_parents,把类内部的内容以字典的方式传给了class_attr。把传过来的字典里的key改为大写再传给type并返回。
上面传的upper_attr是一个函数,而不是一个类,并不十分符合元类这个名称。下面Demo2用一个真正的class来当做元类.
Demo2:
#coding=utf-8
class UpperAttrMetaClass(type): # 继承了type类的子类,相当于是个元类
# __new__ 是在__init__之前被调用的特殊方法
# __new__是用来创建对象并返回之的方法
# 而__init__只是用来将传入的参数初始化给对象
# 你很少用到__new__,除非你希望能够控制对象的创建
# 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
# 如果你希望的话,你也可以在__init__中做些事情
# 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用
def __new__(cls, class_name, class_parents, class_attr):
# 遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name, value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value
# 方法1:通过'type'来做类对象的创建
return type(class_name, class_parents, new_attr)
# 方法2:复用type.__new__方法
# 这就是基本的OOP编程,没什么魔法
# return type.__new__(cls, class_name, class_parents, new_attr)
# python3的用法
class Foo(object, metaclass=UpperAttrMetaClass):
bar = 'bip'
# python2的用法
# class Foo(object):
# __metaclass__ = UpperAttrMetaClass
# bar = 'bip'
print(hasattr(Foo, 'bar'))
# 输出: False
print(hasattr(Foo, 'BAR'))
# 输出:True
f = Foo()
print(f.BAR)
# 输出:'bip'
思考:实现定义类时,无论是否写上object,都会继承object,该如何实现?
1.8. 更新:元类中的__call__方法
注:关于元类中的new、init、call
在元类中:
创建的对象是类,如果希望能够自定义它,一般改写__new__;
如果有需要的话,也可以在__init__中做些事情;
一些高级的用法会涉及到改写__call__特殊方法。
1.9. 元类的使用场景
- 若定义好了函数,需要给函数添加功能时,可以使用装饰器;
- 若定义好了类,需要对类的功能进行修改,可以使用元类;
Python界的领袖 Tim Peters说过,“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。”
2.元类实现ORM (Object Relational Mapping)
2.1. ORM是什么
ORM( Object Relational Mapping ),即对象-关系-映射, 是 python编程语言后端web框架 Django的核心思想,用其操作数据库时可以不需要再用SQL语句。
一个句话理解就是:创建一个实例对象,用创建它的类名当做数据表名,用创建它的类属性对应数据表的字段,当对这个实例对象操作时,能够对应MySQL语句。
说明:
- 所谓的ORM就是让开发者在操作数据库的时候,能够像操作对象时通过
xxxx.属性=yyyy
一样简单,这是开发ORM的初衷- 只不过ORM的实现较为复杂,Django中已经实现了 很复杂的操作。本节知识 主要通过完成一个 insert相类似的ORM,理解其中的道理就就可以了
class User(父类省略):
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
...省略...
u = User(uid=12345, name='Michael', email='[email protected]', password='my-pwd')
u.save()
# 对应如下sql语句
# insert into User (username,email,password,uid)
# values ('Michael','[email protected]','my-pwd',12345)
以上Demo的思想,定义一个类,类名对应于表名,类属性相关的信息对应着每一个字段。
通过创建实例对象,然后创建实例对象的save方法,来对表进行操作。通过操作实例对象,自动转换成操纵SQL语句,而不用写SQL语句。
2.2. 通过元类简单实现ORM中的insert功能
Demo:
- 定义了一个元类ModelMetaclass(继承自type)。
- 定义了一个User类,有四个类属性、__init__方法和save方法。User类使用了 metaclass指定了ModelMetaclass,所以不会用默认的type去创建类,而是使用指明 ModelMetaclass 。
- 会调用ModelMetaclass的__new__方法(类名User传给name、空元组传给bases、类属性作为字典传给attrs),最后返回 type.__new__ 。
- u=User(uid=12345, name='Michael', email='[email protected]', password='my-pwd') 执行时,User的__init__方法会被调用;
- 最后调用save()方法:self指向实例对象...
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
mappings = dict()
# 判断是否需要保存
for key, values in attrs.items(): # 注:字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组
# 判断是否是指定的StringField或者IntegerField的实例对象
if isinstance(values, tuple): # 判断values是否是元组
print('Found mapping: %s ==> %s' % (key, values))
mappings[key] = values # 循环结束时传进来的类属性保存到了字典mappings
# 删除这些已经在字典中存储的属性
for key in mappings.keys():
attrs.pop(key)
# 将之前的uid/name/email/password以及对应的对象引用、类名字
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致
return type.__new__(cls, name, bases, attrs)
class User(metaclass=ModelMetaclass):
uid = ('uid', "int unsigned") # !!注意:在Django中等号右边是对象,而非元祖
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
# 当指定元类之后,以上的类属性将不在类中,而是在__mappings__属性指定的字典中存储
# 以上User类中有(即最终User类的类属性对应为下面的样子)
# __mappings__ = {
# "uid": ('uid', "int unsigned")
# "name": ('username', "varchar(30)")
# "email": ('email', "varchar(30)")
# "password": ('password', "varchar(30)")
# }
# __table__ = "User"
def __init__(self, **keywargs):
for name, value in keywargs.items():
setattr(self, name, value) # setattr 往self指向的对象添加一个属性。
def save(self):
fields = []
args = []
for key, values in self.__mappings__.items(): # 注:实例对象没有__mappings__,但是类对象里面有
fields.append(values[0]) # 取元组values第一个值添加(如:第一次循环取元组('uid', "int unsigned")的uid)
args.append(getattr(self, key, None)) # getattr取对象的key的值,添加到列表args
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join([str(i) for i in args]))
print()
print('SQL: %s' % sql)
u = User(uid=12345, name='Michael', email='[email protected]', password='my-pwd')
# print(u.__dict__)
u.save()
输出:
Found mapping: email ==> ('email', 'varchar(30)')
Found mapping: password ==> ('password', 'varchar(30)')
Found mapping: uid ==> ('uid', 'int unsigned')
Found mapping: name ==> ('username', 'varchar(30)')
SQL: insert into User (email,username,uid,password) values ([email protected],Michael,12345,my-pwd)
注意看,生成的SQL语句是有问题的:...values ([email protected],Michael,12345,my-pwd),括号内有些值应该加上引号。
改进版本:
将语句
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join([str(i) for i in args]))
改成
args_temp = list()
for temp in args:
# 判断入如果是数字类型
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append("""'%s'""" % temp)
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(args_temp))
# 例如,经过以上["12345", """'xxxx'""", """'[email protected]'""", """'567'"""]就会变成 12345,'xxxx','[email protected]','567'
最后一行输出变为:
SQL: insert into User (email,username,password,uid) values ('[email protected]','Michael','my-pwd',12345)
扩展:
- 字典(Dictionary) 的 items() 函数以列表返回可遍历的(键, 值) 元组数组。
- setattr(object, name, value)函数对应函数 getattr(),用于设置属性值,该属性不一定是存在的。往self指向的对象添加一个属性。
- object -- 对象。
- name -- 字符串,对象属性。
- value -- 属性值。
- getattr() 函数用于返回一个对象属性值。getattr(object, name[, default])
- object -- 对象。
- name -- 字符串,对象属性。
- default -- 默认返回值,如果不提供该参数,在没有对应属性时,将触发 AttributeError
- join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。返回通过指定字符连接序列中元素后生成的新字符串。str.join(sequence)
- str--指定的字符
- sequence -- 要连接的元素序列。
最后一句,ORM做到了操作对象,就相当于操作数据表。
-----end-----