近期Python全球交流会开展,下面一起来看看具体,现在Python上的一些问题吧,期待3.8可以解决这些问题。
Petr Viktorin:扩展模块和子解释器
当Python子解释器加载用C编写的扩展模块时,它往往会无意中与已加载相同模块的其他子解释器共享状态,除非非常仔细地编写该模块。Petr Viktorin在Python语言峰会上发表了详细描述问题的建议,并提出了一个更清晰的子解释器隔离。
基于Python的库使用子解释器进行隔离
Python可以在单个进程中运行多个解释器实例,使每个子解释器与其他解析器相对隔离。将来有两种方法可以使用此功能,但两者都需要改进Python。首先,Python可以通过为每个子解释器提供自己的全局解释器锁(GIL)并在它们之间传递消息来实现并行性。Eric Snow在PEP 554中提出了这种子解释器的使用。另一种情况是库碰巧使用Python作为其实现的一部分。例如,Viktorin描述了一个内部使用Python和NumPy的模拟库,或者使用Python和asyncio的聊天库。一个应用程序应该可以加载多个这样的库,每个库都使用Python解释器,没有交叉污染。这个用例是Viktorin演示的主题。他说,问题在于“CPython还没有为此做好准备”,因为它没有妥善管理全球状态。
有许多种全球状态
Viktorin描述了解释器中各种全局状态的层次结构,或者树。进程状态:例如,打开文件描述符。运行时状态:Python内存分配器的数据结构和GIL(直到PEP 554)。解释器状态: “builtins”模块的内容和所有导入模块的dict。线程状态:线程本地,如asyncio的当前事件循环; 幸运的是,这是每个翻译。上下文状态:隐含状态,如decimal.context。模块状态:在文件范围或使用“global”关键字声明的Python变量,实际上创建了模块本地状态。
模块状态变化的让人惊讶
通过一系列示例,Viktorin展示了模块级状态的微妙行为。首先是一个非令人惊讶的例子,通过重新导入它来重新创建纯Python模块的状态:
import enum
old_enum = enum
del sys.modules ['enum']
import enum
old_enum == enum #False
但令人惊讶的是,C扩展模块在重新导入时似乎只会重新创建:
import _sqlite3
old_sqlite3 = _sqlite3
del sys.modules ['_ sqlite3']
import _sqlite3
old_sqlite3 == _sqlite3 #False
最后一行似乎表明这两个模块是截然不同的,但正如Viktorin所说——“这是谎言。”模块的初始化不会重新运行,并且两个模块的内容是共享的:
old_sqlite3.Error is _sqlite3.Error # True
它太容易了使用这些共享内容污染其他子解释器 - 实际上,C扩展的模块状态因此是进程全局状态。
模块必须周到地重写
以新风格 编写的C扩展避免了子解释器的这个问题。并非所有标准库中的C扩展都已更新; Christian Heimes评论说,该ssl模块必须移植到新的初始化方式。虽然找到必须移植的模块很简单,但实际移植需要考虑。编码人员必须仔细区分不同类型的全球状态。C静态变量是进程全局变量,PyState_FindModule返回对模块的解释器 - 全局引用,PyModule_GetState返回模块本地状态。必须将每个模块数据块有意地放置在层次结构中的一个级别上。
作为一个多么棘手的例子,维克托林指出了一个错误
csv模块。如果导入两次,则异常处理中断:
import _csv
old_csv = _csv
del sys.modules ['_ csv']
import _csv
尝试:
#将无效数组传递给reader():应该是一个字符串,而不是1。
列表(old_csv.reader([1]))
除了old_csv.Error:
#exception子句应该捕获错误但不捕获。
通过该old_csv.reader函数应该引发一个old_csv.Error与该except子句匹配的实例。实际上,该csv模块有一个bug。重新导入时,它会覆盖解释器级别的状态,包括_csv.Error类型,而不是将其状态保持在模块本地级别。
交流会成员们一致认为这是一个错误,但Viktorin坚持认为这个特定的错误只是一个更大问题的症状:编写正确的隔离扩展模块太难了。Viktorin和三位共同作者提出了PEP 573来缓解这个问题,特别注意异常类型。Viktorin建议所有模块作者保持模块级别的状态。他意识到这并不总是可行的:例如,Python标准库readline
module包装readline库,它具有全局钩子。这些必然是过程全球状态。他问听众,这个场景应该如何处理?如果readline在多个子解释器中导入错误,应该会出错吗?他说,“有一些想法要做。”无论如何,CPython需要一个良好的默认值。
编写C扩展的正确方法是使用模块本地状态,这应该是从C存储状态的最明显的地方.Viktorin似乎最新的样式API确实强调了他想要的模块本地状态,但是它们还不为人所熟知。