首先,类也是一种对象1,其具有以下特点:
代码示例如下:
class ObjectClass(object):
pass
# 可以赋值
valObj = ObjectClass
obj1 = ObjectClass()
obj2 = valObj()
print(obj1, obj2)
# <__main__.ObjectClass object at 0x1023a45f8>
# <__main__.ObjectClass object at 0x1023a4518>
# 可以作为参数传递
def testfunc(obj):
return obj()
obj3 = testfunc(ObjectClass)
print(obj3)
# <__main__.ObjectClass object at 0x102371f98>
# 可以动态的增加属性
print(hasattr(ObjectClass, 'new_attr'))
ObjectClass.new_attr = 'Megvii'
print(hasattr(ObjectClass, 'new_attr'))
# False
# True
# 可以拷贝
import copy
ObjcetC_C = copy.copy(ObjectClass)
print(ObjcetC_C)
print(ObjcetC_C is ObjectClass)
#
# True
然后,类这种对象,一般是由type
这个函数创建的,其具体语法为:2
type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
type
相关内容在前面的新类和旧类
部分也有聊到3。
那么,像type
这种能够创建类的东西,我们一般称之为元类。
元类的创建过程大致如下:4
In [1]: class Mymeta(type):
...: def __init__(self, name, bases, dic):
...: super().__init__(name, bases, dic)
...: print('===>Mymeta.__init__')
...: print(self.__name__)
...: print(dic)
...: print(self.yaml_tag)
...:
...: def __new__(cls, *args, **kwargs):
...: print('===>Mymeta.__new__')
...: print(cls.__name__)
...: return type.__new__(cls, *args, **kwargs)
...:
...: def __call__(cls, *args, **kwargs):
...: print('===>Mymeta.__call__')
...: obj = cls.__new__(cls)
...: cls.__init__(cls, *args, **kwargs)
...: return obj
...:
In [2]: class Foo(metaclass=Mymeta):
...: yaml_tag = '!Foo'
...:
...: def __init__(self, name):
...: print('Foo.__init__')
...: self.name = name
...:
...: def __new__(cls, *args, **kwargs):
...: print('Foo.__new__')
...: return object.__new__(cls)
...:
===>Mymeta.__new__
Mymeta
===>Mymeta.__init__
Foo
{
'__module__': '__main__',
'__qualname__': 'Foo',
'yaml_tag': '!Foo',
'__init__': <function Foo.__init__ at 0x7f85d669a8b0>,
'__new__': <function Foo.__new__ at 0x7f85d669aaf0>
}
!Foo
In [3]: foo = Foo('foo')
===>Mymeta.__call__
Foo.__new__
Foo.__init__
In [4]: class Bar(Foo):
...: yaml_tag = '!Bar'
...:
...: def __init__(self, name):
...: print('Bar.__init__')
...: self.name = name
...:
...: def __new__(cls, *args, **kwargs):
...: print('Bar.__new__')
...: return object.__new__(cls)
...:
===>Mymeta.__new__
Mymeta
===>Mymeta.__init__
Bar
{
'__module__': '__main__',
'__qualname__': 'Bar',
'yaml_tag': '!Bar',
'__init__': <function Bar.__init__ at 0x7f85d5c8e280>,
'__new__': <function Bar.__new__ at 0x7f85d5c8eb80>
}
!Bar
In [5]: bar = Bar('bar')
===>Mymeta.__call__
Bar.__new__
Bar.__init__
我们来试着理解一下在
python
内部是怎么执行这几个步骤的:2
对于父类
Foo
,Python
会在类的定义中寻找__metaclass__
属性,如果找到了,Python
就会用它来创建类Foo
,如果没有找到,就会用内建的type
来创建这个类。很显然,它找到了。对于子类
Bar
,python
会先在子类中寻找__metaclass__
属性,如果找到了,Python
就会用它来创建类Bar
,如果没有找到,就再从父类中寻找,直到type
。显然,它在父类中找到了。
我们可以看到使用元类的一个好处了,即他可以让子类隐式的继承一些东西。
注意,子类并没有继承父类的__metaclass__
属性,所以其需要到父类当中去寻找;而父类有__metaclass__
属性,不需要再向上寻找;所以两者在这个行为上是不一样的。
既然type
函数可以新建类,我们也可以利用这个把元类写成函数:5
# python2中
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
return type(class_name, class_parents, new_attr)
class Foo():
__metaclass__ = upper_attr
name = "zhangsan"
age = 18
# python3中
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
return type(class_name, class_parents, new_attr)
# 在创建Foo类对象之前,python会先执行metaclass指向的方法
# 也就是upper_attr("Foo", (Bar,), {"name": "zhangsan", "age": 18})
class Foo(metaclass=upper_attr):
name = "zhangsan"
age = 18
但是我们一般是不会这么做的:6
- 使用class作为metaclass能够使得我们代码的动机更加明确。比如当我们读到上面所定义的UpperAttrMetaclass(type)代码时,我们清楚地知道接下来这段代码想要干什么(自定义class object初始化的过程)。
- 我们能够使用OOP的思想进行处理。class作为metaclass可以继承其他的metaclass,重载母类的方法,甚至可以使用别的metaclass。
- 如果我们使用class作为metaclass,某一使用该metaclass的class的子类将仍是是其metaclass的实例。但这一功能无法通过使用函数作为metaclass实现。
- 使用metaclass可以使得代码结构更加优美。实际应用中我们很少使用metaclass来实现上面那样简单的功能。使用metaclass往往是为了实现非常复杂的操作。如果使用class作为metaclass,我们就可以把相应的方法封装到这一个metaclass中,使得代码更加易懂。
- 使用class作为metaclass可以在class中容易的定义__new__,init,__call__方法。虽然我们在将所有的逻辑都放入__new__中,但有的时候根据需要使用其他几个方法会使得逻辑更加清晰。
- 额,人家名字就叫metaclass。这不是带着个class吗?
这一段翻译自StackOverflow
,这是原文7,还有另一篇翻译8,对元类的解释十分详细。
元类的用处有单例模式
、ORM
等,单例模式的代码举例如下:1
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
class Foo(metaclass=Singleton):
def __init__(self):
self.author = 'Megvii'
foo1=Foo()
foo2=Foo()
print(foo1 is foo2)
foo1.author = 'MEGVII'
print(foo2.author)
# True
# MEGVII
ORM
的例子详见1、9、5
还有一个应用是通过MixIn
的设计将各个属性类中定义的成员全部都绑定到组合类中:10
import inspect
import types
class RunImp(object):
def run(self):
print 'just run'
class FlyImp(object):
def fly(self):
print 'just fly'
class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
# 调用父类的方法进行初始化
member_list = (RunImp, FlyImp)
# 将列表内类的成员绑定到组合类
for imp_member in member_list:
if not imp_member:
# 如果该类没用成员就跳过
continue
for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
# 挑选出类属性的名字和地址,绑定到组合类
setattr(cls, method_name, fun.im_func)
class Bird(object):
__metaclass__ = MetaMixin
print Bird.__dict__
# {
# 'fly': < function fly at 0x025220F0 > ,
# '__module__': '__main__',
# '__metaclass__': < class '__main__.MetaMixin' > ,
# '__dict__': < attribute '__dict__' of 'Bird' objects > ,
# 'run': < function run at 0x025220B0 > ,
# '__weakref__': < attribute '__weakref__' of 'Bird' objects > ,
# '__doc__': None
# }
print Bird.__base__
#
可以看到,通过
metaclass
,Bird
拥有了run
、fly
两个method
。但是类的继承体系没有受到影响。
还有一个房屋的组合类的例子,引用如下,详见11
class Wall(object):
STATIC_WALL_ATTR = "static wall"
# 全局变量,非静态数据
def init_wall(self):
self.wall = "attr wall"
# 有self,静态数据
def wall_info(self):
print "this is wall of room"
@staticmethod
def static_wall_func():
print 'static wall info'
# 静态方法
class Door(object):
def init_door(self):
self.door = "attr door"
# 有self,静态数据
def door_info(self):
print "this is door of room"
print self.door, self.wall, self.STATIC_WALL_ATTR
import inspect, sys, types
class metaroom(type):
meta_members = ('Wall', "Door")
# 注入的类
exclude_funcs = ('__new__', '__init__')
# 除外的方法
attr_types = (types.IntType, basestring, types.ListType, types.TupleType, types.DictType)
# 属性的类型
def __init__(cls, name, bases, dic):
super(metaroom, cls).__init__(name, bases, dic)
# type.__init__(cls, name, bases, dic)
# 调用父类初始化
for cls_name in metaroom.meta_members:
# 对于每一个要注入的类
cur_mod = sys.modules[__name__]
# cur_mod = sys.modules[metaroom.__module__]
# 拿到当前的模块
cls_def = getattr(cur_mod, cls_name)
# 从当前的模块里面取出来这个要注入的类
for func_name, func in inspect.getmembers(cls_def, inspect.ismethod):
# 挑选该类的方法
# 注意,这里是实例方法,因为静态方法没有绑定的地址
if func_name not in metaroom.exclude_funcs:
# 如果不是除外的方法
assert not hasattr(cls, func_name), func_name
# 如果组合类当中没有这个方法
setattr(cls, func_name, func.im_func)
# 注入这个方法
# func.im_func是方法里实际的函数对象的引用
# 相当于将原类中的成员函数解绑,然后注入到组合类
# 这样非静态成员函数中的self参数实际上表示的是新的组合类对象;
for attr_name, value in inspect.getmembers(cls_def):
# 遍历这个要注入类的属性
if isinstance(value, metaroom.attr_types) and attr_name not in ('__module__', '__doc__'):
# 如果这个属性的类型在允许的列表当中,而且属性名不是着两个
assert not hasattr(cls, attr_name), attr_name
# 如果组合类当中没有这个属性
setattr(cls, attr_name, value)
# 注入这个属性
class Room(object):
__metaclass__ = MetaRoom
def __init__(self):
self.room = "attr room"
# print self.__metaclass__.meta_members
self.add_cls_member()
def add_cls_member(self):
""" 分别调用各个组合类中的init_cls_name的成员函数 """
for cls_name in self.__metaclass__.meta_members:
# 对于每个要注入的类
init_func_name = "init_%s" % cls_name.lower()
# 获得其中init_cls_name方法名称
init_func_imp = getattr(self, init_func_name, None)
# 取出这个方法
if init_func_imp:
init_func_imp()
# 如果不为空便执行
# 这样就将各个注入类的静态成员数据,即实例属性也注入到组合类
注意,上面的代码当中,无法注入staticmethod
和classmethod
。
方法虽然不是函数,但可以理解为在函数外面加了一层外壳。使用im_func
可以拿到方法里实际的函数对象的引用,但是staticmethod
类似函数,不是一般的方法。所以可以更改如下:11
class MetaRoom(type):
... ...
def __init__(cls, name, bases, dic):
... ...
for cls_name in MetaRoom.meta_members:
... ...
for func_name, func in inspect.getmembers(cls_def, inspect.ismethod):
# 添加成员函数
if func_name not in MetaRoom.exclude_funcs:
if func.im_self:
# 添加原类中定义的classmethod
setattr(cls, func_name, classmethod(func.im_func))
else:
setattr(cls, func_name, func.im_func)
... ...
for func_name, func in inspect.getmembers(cls_def, inspect.isfunction):
# 添加静态成员函数
assert not hasattr(cls, func_name), func_name
setattr(cls, func_name, staticmethod(func))
关于im_func
详见12、13、14
如果某一日新增需求,我们需要继承这个组合类并重载其中的方法,很可能子类显式重载的方法会被元类覆盖掉。因为在建造子类的时候,系统会按照父类的元类来建造。也就是说,当子类带着重载的方法定义作为参数进入元类后,元类进行类的注入,将方法重置为注入类的方法,也就是重载的方法被覆盖掉了。
为了避免这种覆盖的情况发生,我们建议在注入类的时候先检查一下组合类当中是否存在要注入的类。例如两个注入类当中如果有同名的方法,那么第一个就会被第二个给覆盖掉。加入断言assert
进行检查即可,例如assert not hasattr(cls, method_name), method_name
。实际上在上文的第二个例子,即房屋构建的例子当中,我们已经加入了检查机制。
加入子类后的第一个例子在检查时会报错,即存在重复注入的现象。因为父类在建造时调用__metaclass__
,已经将各个类的方法添加到属性了,而子类已经继承了这些属性。但是子类的建造的时候还是会调用父类当中的元类,也就是要再注入一遍,所以会报错。简而言之,这个是一个很隐蔽的陷阱:如果基类定义了__metaclass__
,那么子类在创建的时候会再次调用metaclass,然而理论上来说可能是没有必要的,甚至会有副作用。
为了解决这个情况,我们可以给子类一个新的元类。注意,子类的__metaclass__
必须继承自基类的__metaclass__
。也就是说,我们给子类的元类,必须是父类元类的子类。即继承父类元类后,重写其中的方法,去掉注入的过程,这样就可以作为子类的元类了。
以上的例子等全都是重写的__init__
方法,也有重写__new__
方法的,甚至效果还更好,详见10、11
关于元编程的解释详见15
还有一些其他问题比如im_func
当中涉及到了绑定方法和未绑定方法:16
我们知道像
A.b
这样的方法实际上跟一个普通定义的函数没有本质区别,这个函数有一个参数self
,所以实际上完全可以用A.b(a)
的方式来调用,也就是手工将self
参数指定为a
。这也就是unbound method
的用法。而相应的,bound method
是一个实现了__call__
的对象,它自动将调用这个对象的过程重定向到A.b(a)
上面,相当于通过functools.partial
绑定了第一个参数的效果,所以叫做bound method
。
关于functools.partial
详见17,assert
详见18,Dis
详见19
单例模式的实现有很多,其中还需要注意多线程的情况,详见:20、21
Cpython
是当下最流行的Python
的解释器,使用引用计数(详见第三章第一节的内存管理部分22)来管理内存。
在多线程的情况下,如果有两个线程同时引用变量a
,这样就有可能使得a
的引用计数只增加了一次,导致内存被污染。而当第一个线程结束的时候,a
的引用计数会减去1
。假设第一个线程释放后,a
的引用计数刚好为0
,a
所引用的列表就会被释放。这时候另一个线程去访问a
,就找不到有效的内存了。23
因此,我们需要保护引用计数器。当多个线程同时修改这个值时,可能会导致内存泄漏,那我们可以使用锁来解决这个问题。可有时如果添加了多个锁,会导致另外的死锁问题。为了避免内存泄漏和死锁,CPython
使用了单锁机制,即全局解释器锁(GIL
),所有Python
字节码的执行都需要获取GIL
。24
这可以防止死锁,但它也使得任何计算密集型的Python
程序都采用单线程机制。因为其规定任何时候都只能有一个Python
线程执行,所以我们的多线程其实是伪并行,实际是并发的。24
但这并不意味着线程就是安全的,由于check_interval
机制25,还可能会引起一些问题。26
例如一个简单的n += 1
,在Python
当中的字节码为:
n = 0
def foo():
global n
n += 1
import dis
dis.dis(foo)
# LOAD_GLOBAL 0 (n)
# LOAD_CONST 1 (1)
# INPLACE_ADD
# STORE_GLOBAL 0 (n)
我们可以看到该行代码由4
个字节码组成,每个字节码是原子操作不可打断,但字节码和字节码之间是有空隙的。
假如执行到第二个字节码的时候,解释器打断夺走GIL
并分配给了其他线程,此时该线程对全局变量n
的操作并未完成,而其他线程获得GIL
后同样对全局变量n
进行了修改,这就出现了错误。所以我们可以引入一个互斥锁来保护变量。
所以,尽管有 GIL,你仍然需要加锁来保护共享的可变状态:26
n = 0 lock = threading.Lock() def foo(): global n with lock: n += 1
关于with lock
详见27,关于with
详见28
关于互斥锁:29
互斥锁是在多线程的情况下,确保当前线程执行完成后,再执行下一个任务,当前任务没有结束,下个任务会阻塞。
GIL是保证同一时间只有1个线程在执行,但是该线程让出GIL的时,有可能并没完成该线程的任务,该线程的任务分多少次执行完成这个会安装GIL的默认策略。
进一步的解释详见30
即便如此,Python
当中的多线程还是有用处的,例如IO
密集型操作:爬虫:31
在使用多线程抓取网页内容时,遇到IO阻塞时,正在执行的线程会暂时释放GIL锁,这时其它线程会利用这个空隙时间,执行自己的代码,因此多线程抓取比单线程抓取性能要好。
多线程爬虫的实例详见32
关于GIL
的释放:31
在IO
操作等可能会引起阻塞的system call
之前,可以暂时释放GIL
,但在执行完毕后,必须重新获取GIL
。
Python 3.x
使用计时器(执行时间达到阈值后,当前线程释放GIL
)或Python 2.x
,tickets
计数达到100
从2.x
的tickets
到3.x
的计时器,这是一个有效的改进:33
因为旧
GIL
基于ticker
来决定是否释放GIL
(ticker
默认为100
),并且释放完后,释放的线程依旧会参与GIL
争夺,这就使得某线程一释放GIL
就立刻去获得它,而其他CPU
核下的线程相当于白白被唤醒,没有抢到GIL
后,继续挂起等待,这就造成了资源的浪费。
其具体的实现是引入了一个gil_drop_request
变量,详见33
总结起来就是:
Python
语言和GIL
没有什么关系。仅仅是由于历史原因在Cpython
虚拟机(解释器),难以移除GIL
。gn更换解释器就可以避开GIL
机制,但一般不推荐这么做。C
模块,或者索性用其他语言实现GIL
存在,由于GIL
只保护Python
解释器的状态,所以对于非原子操作,在Python
进行多线程编程时也需要使用互斥锁(如thread
中的lock
)保证线程安全。I/O
密集型程序在多线程上效果良好,例如多线程爬取比单线程性能有提升,因为遇到IO
阻塞会自动释放GIL
锁。start()
和run()
好了,从上面四个小例子,我们可以总结出:34
- start() 方法是启动一个子线程,线程名就是我们定义的name
- run() 方法并不启动一个新线程,就是在主线程中调用了一个普通函数而已。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。35
具体解释详见35
避免僵尸进程:36
- fork两次用孙子进程去完成子进程的任务
- 用wait()函数使父进程阻塞
- 使用信号量,在signal handler中调用 waitpid,这样父进程不用阻塞
当该进程的代码在父进程的代码执行完毕后就没有存在的意义了,则应该将该进程设置为守护进程(例如在生产者与消费者模型中,生产者是专门负责产生数据的任务,而消费者是负责处理数据的任务,当生产者对象join之后,意味生产者不再生产数据,也意味着执行父进程的下一行代码,而消费者处理的数据来自生产者,所以应该将充当消费者的子进程设置为守护进程)37
具体解释及代码详见37
并行: 同一时刻多个任务同时在运行
并发:不会在同一时刻同时运行,存在交替执行的情况。
实现并行的库有: multiprocessing
实现并发的库有: threading
程序需要执行较多的读写、请求和回复任务的需要大量的IO操作,IO密集型操作使用并发更好。
CPU运算量大的程序,使用并行会更好38
详见
Python 之并发编程之进程上(基本概念、并行并发、cpu调度、阻塞 ) 39
Python 之并发编程之进程中(守护进程(daemon)、锁(Lock)、Semaphore(信号量))40
Python 之并发编程之进程下(事件(Event())、队列(Queue)、生产者与消费者模型、JoinableQueue)41
python并发编程手册42
Python 之并发编程之线程上 43
Python 之并发编程之线程中44
Python 之并发编程之线程下45
Python 之并发编程之协程46
Python 之并发编程之manager与进程池pool47
python 之并发编程更新版进程池与进程池比较与回调函数48
Python3 元类(metaclass) ↩︎ ↩︎ ↩︎
python中的元类Metaclass ↩︎ ↩︎
第3章_Python进阶(三) ↩︎
一文搞懂什么是Python的metaclass ↩︎
python3知识点杂记(六) ↩︎ ↩︎
一文带你完全理解Python中的metaclass ↩︎
What are metaclasses in Python? ↩︎
深入理解Python中的元类(metaclass) ↩︎
python3 metaclass–创建类的过程分析 ↩︎
关于metaclass,我原以为我是懂的 ↩︎ ↩︎
Python MetaClass深入分析 ↩︎ ↩︎ ↩︎
Python自省(反射)指南 ↩︎
详解python自定义方法属性 ↩︎
替换一个实例方法,没你想的那么简单~ ↩︎
什么是元编程? ↩︎
python中的绑定方法和未绑定方法是什么? - 灵剑的回答 - 知乎 ↩︎
如何使用python 中的functools.partial用法! ↩︎
python assert的作用 ↩︎
python 使用Dis模块进行代码性能剖析 ↩︎
Python实现Singleton模式的几种方式
↩︎
Python中的单例模式的几种实现方式的及优化 ↩︎
第3章_Python进阶(一) ↩︎
浅谈Python GIL ↩︎
Python之GIL ↩︎ ↩︎
Python进阶:深入GIL(上篇) ↩︎
聊聊Python中的GIL ↩︎ ↩︎
Python多线程with语句加锁Lock ↩︎
python的with关键字 ↩︎
python 互斥锁与GIL的区别 ↩︎
python笔记 Gil全局解释锁和线程互斥锁的关系 ↩︎
Python中的GIL(全局解释器锁)详解及解决GIL的几种方案 ↩︎ ↩︎
python并发编程手册 ↩︎
Python进阶:深入GIL(下篇) ↩︎ ↩︎
Python 多线程 start()和run()方法的区别(三) ↩︎
孤儿进程与僵尸进程[总结] ↩︎ ↩︎
python第三单元选择题题库_Python练习题(三) ↩︎
python之路—并发编程之进程&僵尸进程/孤儿进程/守护进程 ↩︎ ↩︎
并行(parallel)和并发(concurrency)? ↩︎
Python 之并发编程之进程上(基本概念、并行并发、cpu调度、阻塞 ) ↩︎
Python 之并发编程之进程中(守护进程(daemon)、锁(Lock)、Semaphore(信号量)) ↩︎
Python 之并发编程之进程下(事件(Event())、队列(Queue)、生产者与消费者模型、JoinableQueue) ↩︎
python并发编程手册 ↩︎
Python 之并发编程之线程上 ↩︎
Python 之并发编程之线程中 ↩︎
Python 之并发编程之线程下 ↩︎
Python 之并发编程之协程 ↩︎
Python 之并发编程之manager与进程池pool ↩︎
python 之并发编程更新版进程池与进程池比较与回调函数 ↩︎