进阶
杂知识点
什么是 CPython GIL?
GIL,Global Interpreter Lock,即全局解释器锁
引入 GIL 是因为 CPython 的内存管理并不是线程安全的
为了保护多线程下对 python 对象的访问,每个线程在执行过程中都需要先获取 GIL,保证同一时刻只有一个线程在执行代码
GIL 使得 python 的多线程不能充分发挥多核 CPU 的性能,对 CPU 密集型程序的影响较大
另一种解释
全局解释器 锁 GIL,英文名称为 Global Interpreter Lock,它是解释器中一种线程同步的方式。
对于每一个解释器进程都具有一个 GIL ,它的直接作用是限制单个解释器进程中多线程的并行执行,使得即使在多核处理器上对于单个解释器进程来说,在同一时刻运行的线程仅限一个。 对于 Python 来讲,GIL 并不是它语言本身的特性,而是 CPython 解释器的实现特性。
Python 代码被编译后的字节码会在解释器中执行,在执行过程中,存在于 CPython 解释器中的 GIL 会致使在同一时刻只有一个线程可以执行字节码。 GIL 的存在引起的最直接的问题便是:在一个解释器进程中通过多线程的方式无法利用多核处理器来实现真正的并行。
因此,Python 的多线程是伪多线程,无法利用多核资源,同一个时刻只有一个线程在真正的运行。
Python 的内存管理
Python 有内存池机制,Pymalloc 机制,用于对内存的申请和释放管理。
先来看一下为什么有内存池:
- 当创建大量消耗小内存的对象时,c 中频繁调用 new/malloc 会导致大量的内存碎片,致使效率降低。
- 内存池的概念就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。
查看源码,可以看到 Pymalloc 对于小的对象,Pymalloc 会在内存池中申请空间,一般 是少于 236kb,如果是大的对象,则直接调用 new/malloc 来申请新的内存空间。
python 垃圾回收机制
引用计数为主,标记清除和分代回收为辅
引用计数
引用计数机制是这样的:
- 当对象被创建,被引用,作为参数传递,存储到容器中,引用计数 +1
- 当对象离开作用域,引用指向别的对象,del,从容器中移除,引用计数 -1
- 当引用计数降为 0,python 就会自动回收该对象所在的内存空间,
但是引用计数无法解决循环引用的问题,所以引入了标记清除和分代回收机制
标记清除
标记清除(Mark-And-Sweep)主要是解决循环引用问题。
标记清除算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。
它分为两个阶段:第一阶段是标记阶段,GC 会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。那么 GC 又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个 有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
分代回收
分代回收是一种以空间换时间的操作方式。
Python 将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python 将内存分为了 3 “代”,分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代),他们对应的是 3 个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python 垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。
async 和 await 的作用
async: 声明一个函数为异步函数,函数内只要有 await 就要声明为 async
await: 搭配 asyncio.sleep()
时会切换协程,当切换回来后再继续执行下面的语句
OOP 相关
类变量和实例变量的区别?
- 类变量由所有实例共享,一个对象对其进行修改,其他对象也都会被修改
- 实例变量由实例单独享有,不同实例之间不影响
- 当我们需要在一个类的不同实例之间共享变量的时候使用类变量
classmethod 和 staticmethod 区别?
- 都可以通过
Class.method()
的方式使用 - classmethod 第一个参数是 cls,可以引用类变量
- staticmethod 使用起来和普通函数一样,不需要用到类变量,只不过放在类里去组织而已
- classmethod 是为了使用类变量,staticmethod 是代码组织的需要,完全可以放到类之外
class Person:
Country = 'china'
def __init__(self, name, age):
self.name = name
self.age = age
def print_name(self):
print(self.name)
@classmethod
def print_country(cls):
print(cls.Country)
@staticmethod
def join_name(first_name, last_name):
return print(last_name + first_name)
a = Person("Bruce", "Lee")
a.print_country()
a.print_name()
a.join_name("Bruce", "Lee")
Person.print_country()
Person.print_name(a)
Person.join_name("Bruce", "Lee")
__new__
和 __init__
区别?
__new__
是一个静态方法,而__init__
是一个实例方法.__new__
方法会返回一个创建的实例,而__init__
什么都不返回.- 只有在
__new__
返回一个 cls 的实例时后面的__init__
才能被调用. - 当 创建一个新实例时调用
__new__
,初始化一个实例时用__init__
.
我们可以做几个有趣的实验。
class Person:
def __new__(cls, *args, **kwargs):
print("in __new__")
instance = super().__new__(cls)
return instance
def __init__(self, name, age):
print("in __init__")
self._name = name
self._age = age
p = Person("zhiyu", 26)
print("p:", p)
这段程序输出为:
in __new__
in __init__
p: <__main__.Person object at 0x00000261FE562E50>
可以看到先执行 new 方法创建对象,然后 init 进行初始化。假设将 new 方法中不返还该对象,会有什么结果了?
class Person:
def __new__(cls, *args, **kwargs):
print("in __new__")
instance = super().__new__(cls)
# return instance
def __init__(self, name, age):
print("in __init__")
self._name = name
self._age = age
p = Person("zhiyu", 26)
print("p:", p)
发现如果 new 没有返回实例化对象,init 就没法初始化了。
输出结果为:
in __new__
p: None
什么是元类?
元类 (meta class) 是创建类的类
元类允许我们控制类的生成,比如修改类的属性等
使用 type 来定义元类
元类最常见的一个使用场景就是 ORM 框架
functools.lru_cache()
装饰器
该函数是一个装饰器,为函数提供缓存功能。在下次以相同参数调用时直接返回上一次的结果。
例 1:生成第 n 个斐波纳契数,这种慢速递归函数适合使用 lru_cache
import time
def fibonacci(n):
"""斐波那契函数"""
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
stime = time.time()
print(fibonacci(34)) # 没有使用缓存,则需要几秒钟的时间
print("total time is %.3fs" % (time.time() - stime))
# output
# 5702887
# total time is 1.335s
如果没有使用缓存,则需要几秒钟的时间,像下面这样使用缓存后,瞬间就可以计算出结果。
import datetime
import functools
@functools.lru_cache(maxsize=300)
def fibonacci(n):
"""斐波那契函数"""
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
如果使用了 lru_cache,计算用时被大大减少,测试计算时间为 0s。这是因为我们在使用 fibonacci 递归函数时,会重复计算值。使用了 lru_cache 后,所有的重复计算只会执行一次。
注意:
- 缓存是按照参数作为键
- 所有参数必须可哈希 hash,因为缓存实际是存储在字典中,所以使用 list 做参数时就会报错
from functools import lru_cache
@lru_cache(maxsize=100)
def list_sum(nums: list):
return sum(nums)
# output
# TypeError: unhashable type: 'list'
assert
Python assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常。
断言可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况,例如我们的代码只能在 Linux 系统下运行,可以先判断当前系统是否符合条件。
语法格式如下:
assert expression
等价于:
if not expression:
raise AssertionError
assert 后面也可以紧跟参数:
assert expression [, arguments]
等价于:
if not expression:
raise AssertionError(arguments)
以下为 assert 使用实例:
>>> assert True # 条件为 true 正常执行
>>> assert False # 条件为 false 触发异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1==1 # 条件为 true 正常执行
>>> assert 1==2 # 条件为 false 触发异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> assert 1==2, '1 不等于 2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: 1 不等于 2
>>>
以下实例判断当前系统是否为 Linux,如果不满足条件则直接触发异常,不必执行接下来的代码:
import sys
assert ('linux' in sys.platform), "该代码只能在 Linux 下执行"
# 接下来要执行的代码
@abstractmethod 抽象方法
抽象方法表示基类的一个方法,没有实现,所以基类不能实例化,子类实现了该抽象方法才能被实例化。
Python 的 abc 提供了 @abstractmethod
装饰器实现抽象方法
下面以 Python3 的 abc 模块举例。
from abc import ABC, abstractmethod
class Foo(ABC):
@abstractmethod
def fun(self):
'''please Implemente in subclass'''
class SubFoo(Foo):
def fun(self):
print('fun in SubFoo')
a = SubFoo()
a.fun()
@cached_property
每个实例只计算一次的属性,然后用普通属性替换自身。删除属性将重置属性。
class cached_property(object):
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
""" # noqa
def __init__(self, func):
self.__doc__ = getattr(func, "__doc__")
self.func = func
def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
其原理就是保存到实例字典 __dict__
中, 避免多次调用重复计算
举个例子
class Test(object):
test1 = 'aaa'
def __init__(self):
self.age = 20
@cached_property
def real_age(self):
return self.age + 19
if __name__ == '__main__':
t = Test()
print t.real_age # 39
print t.__dict__ # {'real_age': 39, 'age': 20}, 不加装饰器就不存在__dict__中了
typing.TypedDict
class typing.TypedDict(dict)
向字典添加类型提示的特殊构造。在运行时,它是一个普通的 dict
。
TypedDict
声明了一个字典类型,它期望它的所有实例都有一组特定的键,其中每个键都与一个一致类型的值相关联。这种期望不会在运行时检查,但仅由类型检查器强制执行。用法:
class Point2D(TypedDict):
x: int
y: int
label: str
a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK
b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check
assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first')
允许在不支持的旧版本 Python 中使用此函数 PEP 526,TypedDict
支持另外两种等效的句法形式:
Point2D = TypedDict('Point2D', x=int, y=int, label=str)
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
当键不是一个有效的标识符 / 关键字时,也应该使用函数语法,例如因为它们是关键字或包含连字符。例子:
# raises SyntaxError
class Point2D(TypedDict):
in: int # 'in' is a keyword
x-y: int # name with hyphens
# OK, functional syntax
Point2D = TypedDict('Point2D', {'in': int, 'x-y': int})
默认情况下,所有键都必须存在于 TypedDict
中。可以通过指定整体来覆盖它。用法:
class Point2D(TypedDict, total=False):
x: int
y: int
这意味着 Point2D
TypedDict
可以省略任何键。类型检查器仅应支持文字 False
或 True
作为 total
参数的值。 True
是默认值,它使类主体中定义的所有项都成为必需项。
TypedDict
类型可以使用基于类的语法从一个或多个其他 TypedDict
类型继承。用法:
class Point3D(Point2D):
z: int
Point3D
具有三个项目: x
、 y
和 z
。它等价于这个定义:
class Point3D(TypedDict):
x: int
y: int
z: int
TypedDict
不能从非 TypedDict 类继承,特别是包括 Generic
。例如:
class X(TypedDict):
x: int
class Y(TypedDict):
y: int
class Z(object): pass # A non-TypedDict class
class XY(X, Y): pass # OK
class XZ(X, Z): pass # raises TypeError
T = TypeVar('T')
class XT(X, Generic[T]): pass # raises TypeError
TypedDict
可以通过注解字典 (有关注解最佳实践的更多信息,请参阅注解最佳实践)、__total__
、__required_keys__
和 __optional_keys__
进行自省。
多等于号表达式
>>> i=7
>>> a=b=c=d=e=f=g=h=i
形如 a=b=c=d...=X
左边不管有多少个变量之间用等于号连接,都仅仅是为了声明
最右边的那个等于号左边的所有变量(a=b=c=d…) 都等于 它右边的那个变量(=X)。
注意,如果最后赋值的是一个列表,前面的变量都会指向一个列表
i=[1,2,3]
a=b=c=i
# 相当于
a=i
b=i
c=i
# 所以他们指向同一个列表
__getattribute__
,__getattr__
与 __setattr__
__getattribute__
官方文档中描述如下:
该方法可以拦截对对象属性的所有访问企图,当属性被访问时,自动调用该方法(只适用于新式类)。因此常用于实现一些访问某属性时执行一段代码的特性。
需要注意的是,正式由于它拦截对所有属性的访问(包括对 __dict__
的访问),在使用中要十分小心地避开无限循环的陷阱。在 __getattribute__
方法中访问当前实例的属性时,唯一安全的方式是使用基类(超类) 的方法 __getattribute__
(使用 super)。例如:
通过上图中的代码示例可以看出,一旦实现了 __getattribute__
方法,所有通过对象访问的属性(包括类属性)都会被拦截,而直接通过类访问类属性则不会。
注意:当访问的属性不存在并重载(覆盖基类对某方法的默认实现)了 __getattribute__
方法时,该方法不会主动抛出 AttributeError 异常。上图中捕获的 AttributeError 异常,是由基类 __getattribute__
方法实现并抛出。
常见的错误用法示例:
在实现 __getattribute__
方法时访问对象自身的属性,程序陷入无限循环直到崩溃。
__getattr__
官方文档描述如下:
__getattr__
方法的自动执行,需要满足两个条件:
- 访问对象属性;
- 触发 AttributeError 异常。
代码示例如下:
上图中,调用不存在的 job 属性首先调用 __getattribute__
方法(如果该方法未定义,会调用基类的 __getattribute__
方法),触发 AttributeError 异常并自动捕获,然后才调用 __getattr__
方法。
错误用法示例如下:
重载了 __getattribute__
方法,却没有主动抛出 AttributeError 异常的机制,或者抛出一个其它类型的异常,__getattr__
方法都不会执行
__setattr__
试图给属性赋值时自动调用该方法,例如:
之所以会执行三次 print 函数,是因为在 __init__
方法中,对象 A 初始化时给属性 name 和 age 赋值时,触发了 __setattr__
方法。使用该方法是同样需要十分小心避免无限循环陷阱。
错误用法示例如下:
可以看出,在 __setattr__
方法中,不能直接给属性赋值,而通常的 做法是使用 __dict__
魔法属性。
__dict__
属性是一个字典,所有的实例属性都存储在这个字典中,而修改 __dict__
字典中的键值对成员不会触发 __setattr__
方法,这里应注意与直接修改 __dict__
的值的区别。
注意:如果定义 __setattr__
方法的同时定义了 __getattribute__
方法,那么在修改 __dict__
字典中的键值对时,由于调用了 self.__dict__
属性,同样会触发 __getattribute__
方法,使用时应格外小心。代码示例如下:
上图示例代码中,每调用一次 __setattr__
就会调用一次 __getattribute__
。
注意赋值语句与属性调用的区别:self.__dict__ = {}
是赋值语句,不会触发 __getattribute__
方法,但触发 __setattr__
方法;self.__dict__[name] = value
语句,先调用 self.__dict__
属性,得到 dict 对象后再修改其成员,因此会触发 __getattribute__
方法。
__enter__
与 __exit__
在 python 中实现了 __enter__
和 __exit__
方法,即支持上下文管理器协议。
上下文管理器就是支持上下文管理器协议的对象,它是为了 with 而生。
- 当 with 语句在开始运行时,会在 上下文管理器对象上调用
__enter__
方法。 - with 语句运行结束后,会在上下文管理器对象上调用
__exit__
方法
with 的语法:
with EXPR as VAR:
BLOCK
这是上面语法的伪代码:
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
- 生成上下文管理器 mgr
- 如果没有发现
__exit__
,__enter__
两个方法,解释器会抛出 AttributeError 异常 - 调用上下文管理器的
__enter__()
方法 - 如果语法里的 as VAR 没有写,那么伪代码里的 VAR= 这部分也会同样被忽略
- 如果 BLOCK 中的代码正常结束,或者是通过 break,continue,return 来结束,
__exit__()
会使用三个 None 的参数来返回 - 如果执行过程中出现异常,则使用
sys.exc_info
的异常信息为参数调用__exit__(exc_type, exc_value, exc_traceback)
之前我们对文件的操作是这样的:
try:
f = open('filename')
except:
print("Unexpected error:", sys.exc_info()[0])
else:
print(f.readlines())
f.close()
现在有了 with 语句可以使代码更加简洁,减少编码量,下面的语句会在执行完后自动关闭文件(即使出现异常也会):
with open('example.info', 'r') as f:
print(f.readlines())
一个例子:
class TmpTest:
def __init__(self,filename):
self.filename=filename
def __enter__(self):
self.f = open(self.filename, 'r')
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
test=TmpTest('file')
with test as t:
print ('test result: {}'.format(t))返回:
返回:
test result: None
如果在 __init__
或者 __enter__
中抛出异常,则不会进入到 __exit__
中:
class TmpTest:
def __init__(self,filename):
self.filename=filename
print("__init__")
raise ImportError
def __enter__(self):
self.f = open(self.filename, 'r')
print("__enter__")
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__")
self.f.close()
test = TmpTest('file')
with test as t:
print ('test result: {}'.format(t))
返回:
__init__
Traceback (most recent call last):
File "D:/pythonScript/leetcode/leetcode.py", line 14, in <module>
test=TmpTest('file')
File "D:/pythonScript/leetcode/leetcode.py", line 5, in __init__
raise ImportError
ImportError
如果在 __exit__
中返回 True,则不会产生异常:
class TmpTest:
def __init__(self,filename):
self.filename=filename
print("__init__")
def __enter__(self):
self.f = open(self.filename, 'r')
print("__enter__")
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__ {} ".format(exc_type))
self.f.close()
return True
test=TmpTest('file')
with test as t:
print ('test result: {}'.format(t))
raise ImportError
print("no error")
返回:
__init__
__enter__
test result: <_io.TextIOWrapper name='file' mode='r' encoding='cp936'>
__exit__ <class 'ImportError'>
no error
结构化模式匹配
Structural Pattern Match
Python 3.10 的新特性
在模式匹配出现之前,对于分支相当多的判断语句,Python 建议通过字典映射(dictionary mapping)来实现。
旧方法:字典映射
def function_map(option):
return {
1: lambda : print('You have chose option 1.'),
2: lambda : print('You have chose option 2.'),
3: lambda : print('You have chose option 3.')
}.get(option, lambda: print('Sorry you chose an invalid option.'))
function_map(3)()
借助字典这种数据结构,以匹配条件作为键值,一一对应匹配后需要执行的命令。将 switch
结构中的条件判断转化为对字典键值的搜索匹配。
Pattern Match
用模式匹配实现 switch-case
语法,从形式上看就直观了很多:
option = 3
match option:
case 1:
print("You have chosen option 1.")
case 2:
print("You have chosen option 2.")
case 3:
print("You have chosen option 3.")
case _:
print("You chose an invalid option.")
实际上模式匹配不只有创建流程上的分支结构这一种功能,它的作用可以比单纯的 switch-case
语法强大的多。
模式匹配其实可以拆成两部分来理解:匹配和模式。
- 匹配部分可以发挥类似于
if-else
和switch
等条件判断语句的作用,生成一种分支结构; - 模式则定义了特定的规则即匹配的具体条件。更进一步的,还会对匹配到的对象进行解构(destructuring)或者说拆包(unpacking)
以不同于模式匹配的正则表达式来说:
import re
source_str = 'cats are cute'
pattern = re.compile('(.*) are (.*)')
matched = re.match(pattern, source_str)
print(matched.groups())
# => ('cats', 'cute')
正则表达式规则中的 (.*)
分别匹配到源字符串中的 cats
和 cute
,与此同时,还把这两个匹配项提取了出来。
而模式匹配相对来说,则不仅仅能够匹配和提取 cats
、cute
等字符串类型,还能够匹配更复杂类型的对象,同时对匹配到的对象进行拆包操作
比如下面的代码就对类型为元组的对象进行了匹配和拆包:
def match_person(person):
match person:
case (name, 'M', age):
print(f'He is {name}, aged {age}.')
case (name, 'F', age):
print(f'She is {name}, aged {age}.')
case (name,):
print(f'We only know the name is {name}, others are secrets.')
person_A = ('John', 'M', 20)
person_B = ('Jenny', 'F', 18)
person_C = ('Lily',)
match_person(person_A)
# => He is John, aged 20.
match_person(person_B)
# => She is Jenny, aged 18.
match_person(person_C)
# => We only know the name is Lily, others are secrets.
⭐ match
关键字后面被匹配的对象,支持很多种复杂的类型。对应的 case
关键字后面的模式也同样灵活:
- 列表或元组,如
(name, 18)
- 字典,如
{"name": name, "age": 18}
- 使用
*
匹配列表中的剩余部分,如[first, *rest]
- 使用
**
匹配字典中的剩余部分 - 匹配对象和对象的属性
- 在模式中可以使用
|
逻辑或操作
模式匹配应用实例
创建一个 Python 程序,模拟交互式命令行的行为。