在开发大型 Python 应用程序时,有时需要多个模块共享和管理全局数据。如何优雅地在 Python 包内的不同模块间共享全局数据是一个常见的设计问题。我们希望避免全局变量的混乱和难以维护的代码,但同时能够安全、高效地管理这些共享数据。
下面我们将探讨几种常用的全局数据管理方法,以及如何在模块间合理共享和修改全局数据。
1、问题背景
在Python或其他编程语言中,如何管理跨包的模块中全局数据?在设计语言Heron的包和模块系统时,我受Python模块系统启发很大。Python有丰富的模块选择,这似乎对其成功有很大贡献。其中存在疑问的是,如果在一个Python模块中包含了两个不同的已编译包,会发生什么情况:是制作数据副本还是共享数据?与此相关的是一系列侧问题:
我假设包在Python中可以被编译,是否正确?
模块数据复制或共享的两种方法有什么优缺点?
从Python社区的角度来看,Python的模块系统存在哪些众所周知的问吗?例如,是否有正在考虑用于增强模块/包的PEP?
Python模块/包系统中哪些方面对编译语言来说行不通?
2、解决方案
回答1:
a. Python代码被词法分析并编译成Python特定指令,但没有被编译成机器可执行代码。".pyc"文件会在运行与现有.pyc时间戳不匹配的Python代码时自动创建。可以关闭此功能。可以使用dis模块来查看这些指令。
b. 导入模块时,它将在其命名空间中(从上到下)执行,并将该命名空间全局缓存。从另一个模块导入时,该模块不会再次执行。请记住,def只是一个语句。可能需要在代码中放置一个print(‘compiling this module’)语句来跟踪它。
这取决于具体情况。
最近有一些增强,主要围绕指定需要加载哪些模块。模块可以有相对路径,以便一个大项目中有多个具有相同名称的模块。
Python本身不能用于编译语言。在Google中搜索“unladen swallow blog”,查看试图加速语言的磨难。“a = sum(b)”在执行之间可以改变含义。撇开极端情况,模块系统在源代码和编译库系统之间形成了一个很好的桥梁。这种方法很有效,Python轻松地封装C代码(swig等)有所帮助。
示例.py:
print “Creating %s module.” % name
def show_def(f):
print “Creating function %s.%s.” % (name, f.name)
return f
@show_def
def a():
print “called: %s.a” % name
交互式会话:
import example
先检查sys.modules[‘example’]
由于它不存在,可以找到example.py并将它“编译”到example.pyc
(由于example.pyc不存在,如果它是过时的,也会发生同样的情况,等等)
Creating example example module. # 执行模块代码
Creating function example.a. # 执行def语句
example.a()
called: example.a
import example
找到sys.modules[‘example’],将局部变量example分配给该对象
没有 ‘Creating …’ 输出
d = {“name”: “fake”}
exec open(“example.py”) in d
本次会话中的第一次导入与此非常相似
它创建一个模块对象(具有__dict__),初始化其中的几个变量
(builtins, name__和其他变量—包的__init
模块也有自己的变量—查看some_module.dict.keys()或
dir(some_module))
并执行example.py中的代码(或存储在example.pyc中的代码对象)
Creating fake module. # 执行模块代码
Creating function fake.a. # 执行def语句
d.keys()
[‘builtins’, ‘name’, ‘a’, ‘show_def’]
d’a’
called: fake.a
解答2:
模块是Python中唯一真正的全局对象,所有其他全局数据都基于模块系统(它使用sys.modules作为注册表)。包只是具有导入子模块的特殊语义的模块。“在某种意义上讲,编译”一个.py文件成.pyc或.pyo并不是大多数语言所了解的编译:它只检查语法并创建一个在解释器中执行时创建模块对象的代码对象。
示例.py:
print “Creating %s module.” % name
def show_def(f):
print “Creating function %s.%s.” % (name, f.name)
return f
@show_def
def a():
print “called: %s.a” % name
交互式会话:
import example
先检查sys.modules[‘example’]
由于它不存在,可以找到example.py并将它“编译”到example.pyc
(由于example.pyc不存在,如果它是过时的,也会发生同样的情况,等等)
Creating example example module. # 执行模块代码
Creating function example.a. # 执行def语句
example.a()
called: example.a
import example
找到sys.modules[‘example’],将局部变量example分配给该对象
没有 ‘Creating …’ 输出
d = {“name”: “fake”}
exec open(“example.py”) in d
本次会话中的第一次导入与此非常相似
它创建一个模块对象(具有__dict__),初始化其中的几个变量
(builtins, name__和其他变量—包的__init
模块也有自己的变量—查看some_module.dict.keys()或
dir(some_module))
并执行example.py中的代码(或存储在example.pyc中的代码对象)
Creating fake module. # 执行模块代码
Creating function fake.a. # 执行def语句
d.keys()
[‘builtins’, ‘name’, ‘a’, ‘show_def’]
d’a’
called: fake.a
你的问题:
它们在某种意义上是编译的,但如果你熟悉C编译器的工作方式,那么它们与你的预期不符。
如果数据是不可变的,那么复制是可行的,除了对象标识符(Python中的is运算符和id())外,它与共享应该是无法区分的。
导入可能会或可能不会执行代码(它们总是会将局部变量分配给一个对象,但这不会产生问题),并且可能会或可能不会修改sys.modules。必须小心不要在多线程中导入,通常最好在每个模块的顶部进行所有导入:这将导致一个级联图,以便立即完成所有导入,然后__main__继续并执行真正的任务。
我不知道当前是否有任何PEP,但已经有很多复杂的机制到位。例如,包可以具有__path__属性(实际上是一个路径列表),因此子模块不必位于同一目录中,这些路径甚至可以在运行时计算!(请看下面的mungepath包示例。)你可以拥有自己的导入挂钩,在函数中使用import语句,直接调用__import__,而且我不会感到惊讶会找到2-3其他独特的方法来使用包和模块。
导入系统的一个子集可以在传统编译语言中使用,只要它类似于C的#include即可。可以在编译器中运行“第一级”执行(创建模块对象),并编译那些结果。然而,这样做有显着的缺点,等于模块级代码和运行时执行的函数的分离执行上下文(有些函数必须在这两个上下文中运行!)。(请记住在Python中每条语句都在运行时执行,即使是def和class语句也是如此。)
我认为这是传统编译语言将“顶层”代码限制为类、函数和对象声明、消除第二个上下文的主要原因。即使在那时,你也会遇到C/C++(和其他语言)中全局对象的初始化问题,除非小心地管理。
mungepath/init.py:
print path
path.append(“.”) # CWD,在非示例代码中会不同
print path
from . import example # 这是上面示例的example.py,不在mungepath/中
注意这是一个退化的情况,因为现在我们用两个名称来表示
“相同”的模块:example和mungepath.example,但它们实际上是
具有不同函数的不同模块(使用 ‘is’ 或 ‘id()’ 来验证)
交互式会话:
import example
Creating example module.
Creating function example.a.
example.dict.keys()
[‘a’, ‘builtins’, ‘file’, ‘show_def’, ‘package’,
‘name’, ‘doc’]
import mungepath
[‘mungepath’]
[‘mungepath’, ‘.’]
Creating mungepath.example module.
Creating function mungepath.example.a.
mungepath.example.a()
called: mungepath.example.a
example is mungepath.example
False
example.a is mungepath.example.a
False
解答3:
全局数据在解释器级别进行控制。
“包”可以被编译,因为包只是可以编译的模块的集合。
我不确定我在给定数据确定的作用域下理解。
在 Python 包中管理全局数据的方法有多种,具体选择取决于应用的规模和需求:
- 简单项目:可以使用专门的模块存储全局数据,适用于全局数据较少且简单的情况。
- 面向对象项目:使用单例模式是一个更优雅的选择,尤其在需要数据封装时。
- 多线程/异步项目:
contextvars
提供了线程安全的全局数据管理方法。 - 结构化数据:使用
dataclasses
或配置对象可以提供更强的数据结构化管理。 - 跨进程:环境变量适合用于跨进程或容器化应用。
根据项目的需求和复杂度,选择合适的全局数据管理方法能够提高代码的可维护性和可扩展性。