基础进阶

  1. 元类
    1. 传统方式创建类
    2. 元类
  2. 垃圾回收
    1. 引用计数器
    2. 标记清除
    3. 分代回收
    4. 缓存机制
  3. 并发编程
    1. 基本概念
    2. 进程创建
    3. multiprocessing

元类

传统方式创建类

class Foo(object):

    def __new__(cls, *args, **kwargs):
        data = object.__new__(cls)
        return data

    def __init__(self, name):
        self.name = name


foo = Foo("alex")

创建类的时候分几步创建对象:

  1. 执行类的 new 方法(魔法方法),创建一个空对象
  2. 执行类的 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 的 newinit,也就是说 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 还是直接走回收机制,那么直接看文章

并发编程

基本概念

  • 进程:操作系统资源分配和独立运行的最小单位。
  • 线程:进程内一个任务执行的独立单元,是任务调度和系统执行的最小单位。
  • 协程:用户态的轻量级线程,协程的调度完全用用户控制,主要是单线程下模拟多线程。

操作系统中每打开一个程序都会创建一个进程 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


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。