基础进阶
# 元类
# 传统方式创建类
class Foo(object):
def __new__(cls, *args, **kwargs):
data = object.__new__(cls)
return data
def __init__(self, name):
self.name = name
foo = Foo("alex")
创建类的时候分几步创建对象:
- 执行类的 new 方法(魔法方法),创建一个空对象
- 执行类的 init 方法(魔法方法),进行初始化
那么对象是基于类创建出来的,那么类也是由一个更高层的内容创建的,即 type
创建。
# 传统方式创建类,更加直观
# 这里创建了一个类 Foo,继承了 object,成员是类变量 v1、类方法 func
class Foo(object):
v1 = 123
def func(self):
return 666
# 非传统方式创建类,即 type 创建
Foo = type("Foo", (object,), {"v1": 123, "func": lambda self: 666})
# 元类
类默认是使用 type
创建,但是我们可以不走默认值,也就是元类,元类也就是指定类由谁来创建。
class MyType(type):
def __new__(cls, *args, **kwargs):
new_cls = super.__new__(cls, *args, **kwargs)
print(new_cls)
return new_cls
def __init__(self):
super().__init__(self)
def __call__(self, *args, **kwargs):
"""
call 的作用:
1. 调用自己类的 __new__ 方法去创建对象,即 self.__new__()
2. 调用自己类的 __init__ 方法去做初始化,即 self.__init__()
"""
empty_obj = self.__new__(*args, **kwargs)
self.__init__(self)
return empty_obj
class Foo(object, metaclass=MyType):
pass
在这里,我们首先创建了一个元类 MyType,然后创建了一个 Foo,指定 Foo 的元类为 MyType。
那么在创建 Foo 类的时候,会首先执行 MyType 的 new 和 init,也就是说 Foo 类其实就是 MyType 实例 MyType()
。
所以说,Foo 的对象 Foo()
其实就是 MyType 实例的实例,一个对象的实例会调用一个魔法方法 call()。
所以理解这个逻辑,就知道为啥一个类在创建的时候会先执行 new() 然后执行 init(),因为 call() 执行的顺序就是先执行 new() 再执行 init()。
# 垃圾回收
python 中的垃圾回收是以引用计数器为主,标记清除和分代回收为辅。
# 引用计数器
python 中,程序创建的任何对象都会放到数据结构的环状双向链表 refchain。
每个对象都会存放:
- 上一个对象、下一个对象、当前对象类型、被引用的数量
- 当前对象 value(普通数值) / items(列表等)、当前对象元素个数(仅列表等)
所以当 python 程序运行时,会根据数据类型找到对应的结构体,然后根据结构体中的字段创建相关的数据,然后将对象添加到双向链表中。
每个对象中引用计数器的值默认为 1,如果有其他变量引用此对象则会发生变化。
但是引用计数器也有一个 bug,他解决不了循环引用的问题。
# 标记清除
为了解决循环引用的问题,python 中又增加一个标记清除的方法。
当有这种循环引用对象出现时,除了会放到引用计数器所代表的双向链表之外,还会专门开辟一个新的链表,再放到标记清除代表的链表中。
那么 python 会到循环链表中查看是否有这样的循环链表,查看可能循环的每个元素,如果有则让双方的引用计数器 -1,如果为 0(说明没人用了) 则直接垃圾回收。
# 分代回收
那么标记清除检查可能循环引用的链表,每次扫描时间都比较久,代价比较大(每个元素都要扫描),所以 python 为了解决这个问题引入了分代回收。
分代回收中规定了一个新的技术,将可能循环引用的链表维护成了三个新链表,分别为:
- 0 代:0 代中的对象假如个数达到 700 个则扫描一次。
- 1 代:0 代扫描 10 次则 1 代扫描一次。
- 2 代:1 代扫描 10 次则 2 代扫描一次。
所以引入了分代回收法之后,假如存在循环引用的问题,除了会加到专用的标记清除链表中之外,还会将对象添加到 0 代中。
0 代增长到 700 个后,进行一次扫描,垃圾回收,不是垃圾升级为 1 代,同时标记为 0 代已经扫描了一次。
0 代扫描了 10 次后 1 代会扫描一次,2 代同理。
# 缓存机制
在引用计数器和分代回收的基础上,python 做出了一些优化机制,就是缓存。
缓存在 python 中分为两大类:
池
为了避免常见的对象被重复创建和销毁,python 在启动解释器后创建了一个池用于维护一些常见的对象
到时候会直接到池子中获取而不是开辟一个新的内存
free_list
当一个对象的引用计数器为 0 时,按理说应该回收,有一些 python 内部不会去回收,而是将对象添加到 free_list 列表中并且重新初始化当成缓存
以后再去创建对象时,不再开辟内存,而是直接使用 free_list,当 free_list 满了之后新对象接着走回收机制
具体看某一些对象是否放到 free_list 还是直接走回收机制,那么直接看文章 (opens new window)
# 并发编程
# 基本概念
- 进程:操作系统资源分配和独立运行的最小单位。
- 线程:进程内一个任务执行的独立单元,是任务调度和系统执行的最小单位。
- 协程:用户态的轻量级线程,协程的调度完全用用户控制,主要是单线程下模拟多线程。
操作系统中每打开一个程序都会创建一个进程 ID 即 PID,作为操作系统区分进程的唯一标识符。没当进程执行任务结束,操作系统会回收进程的一切,包括 PID。
# 进程创建
python 中创建进程的方式有很多种,包括 os、multiprocessing、process、subprocess 等模块。
os 创建
import os def main(): print(f'当前进程: {os.getpid()}') # fork 创建一个子进程,注意 windows 中无此函数 pid = os.fork() # 子进程中 pid = 0,父进程中 pid > 0,原因为父进程中执行了 fork,所以得到了子进程作为 pid 返回值 if pid == 0: # 子进程 print(f'当前进程: {os.getpid()},父进程: {os.getppid()}') else: # 父进程 print(f'父进程: {os.getpid()}') return if __name__ == '__main__': main()
方法名 描述 os.fork() 创建子进程,相当于复制一份主进程信息,从而创建一个子进程。
os.fork 是依赖于 linux 系统的 fork 系统调用实现的进程创建,在 windows 下是没有该操作的。os.getpid() 获取当前进程的 PID os.getppid() 获取当前进程的父进程的 PID multiprocessing,工作中最常用
import multiprocessing import os import time def main(): # 创建进程,并且子进程中执行函数 watch process = multiprocessing.Process(target=watch) process.start() for i in range(3): print(f'主进程: {os.getpid()}') time.sleep(1.5) return def watch(): for i in range(3): print(f'{os.getpid()}') time.sleep(1) return if __name__ == '__main__': main()
windows 中 python 创建子进程是通过 import 导入父进程代码到子进程中实现的子进程创建方式,所以 import 在导入以后会自动执行被导入模块的代码,所以会报错。
因此在 windows 中需要将进程创建放到
if __name__ == '__main__':
中,而 linux / os 中使用os.fork()
的方式实现,所以不需要。
# multiprocessing
假设 p 为 multiprocessing.Process(target=任务函数/函数方法) 的返回值,子进程操作对象。
方法名
p.start()
在主进程中启动子进程p,并调用该子进程 p 中的 run() 方法
p.run()
子进程 p 启动时运行的方法,去调用 start 方法的参数 target 指定的函数/方法。如果要自定义进程类时一定要实现或重写 run 方法。
p.terminate()
在主进程中强制终止子进程 p,不会进行任何资源回收操作,如果子进程 p 还创建自己的子进程(孙子进程),则该孙子进程就成了僵尸进程,使用该方法需要特别小心这种情况。
如果子进程 p 还保存了一个锁(lock)那么也将不会被释放,进而导致出现死锁现象。
p.is_alive()
检测进程是否还存活,如果进程 p 仍然运行中,返回 True
p.join([timeout])
主进程交出 CPU 资源,并阻塞等待子进程结束(强调:是主进程处于等待的状态,而子进程p是处于运行的状态)
timeout 是可选的超时时间,需要强调的是,p.join() 只能 join 住 start 开启的子进程,而不能 join 住 run 开启的子进程
属性名
p.daemon
默认值为 False,如果设为 True,代表子进程p作为守护进程在后台运行的
当子进程 p 的父进程终止时,子进程 p 也随之终止,并且设定为 True 后,子进程 p 不能创建自己的孙子进程
daemon 属性的值必须在 p.start() 之前设置
p.name
进程的名称
p.pid
进程的唯一标识符
TODO