如何在Python中创建模块级别的变量?

289

在一个模块中有没有一种方式可以设置全局变量?当我尝试以最明显的方式如下所示执行时,Python解释器会说变量 __DBNAME__ 不存在。

...
__DBNAME__ = None

def initDB(name):
    if not __DBNAME__:
        __DBNAME__ = name
    else:
        raise RuntimeError("Database name has already been set.")
...

在不同的文件中导入模块后

...
import mymodule
mymodule.initDB('mydb.sqlite')
...

同时,以下是追踪信息:

... UnboundLocalError: 引用之前未分配局部变量DBNAME ...

有什么想法吗?我正在尝试按照这位用户的建议,使用模块来设置单例模式。

5个回答

322

以下是正在发生的事情。

首先,Python真正拥有的全局变量只有模块作用域变量。你不能创建一个真正意义上的全局变量;你所能做的就是在特定作用域内创建一个变量。(如果你在Python解释器内创建了一个变量,然后导入其他模块,你的变量将位于最外层作用域,从而成为你的Python会话中的全局变量。)

要创建一个模块全局变量,你只需要简单地给一个变量名赋值即可。

想象一个名为foo.py的文件,其中仅包含这一行:

X = 1

现在想象一下你将其导入。

import foo
print(foo.X)  # prints 1

然而,假设你想在函数中使用一个模块级变量作为全局变量,就像你的例子一样。Python 的默认设置是将函数变量视为局部变量。你只需要在函数中添加一个 global 声明,在尝试使用全局变量之前声明即可。

def initDB(name):
    global __DBNAME__  # add this line!
    if __DBNAME__ is None: # see notes below; explicit test for None
        __DBNAME__ = name
    else:
        raise RuntimeError("Database name has already been set.")
顺便提一句,对于这个例子来说,简单的if not __DBNAME__测试就足够了,因为任何非空字符串都将被作为真值处理,所以任何实际的数据库名称都会被视为真。但是对于可能包含可能为0的数字值的变量,则不能仅使用if not variablename;在这种情况下,应该使用is运算符显式测试None。我修改了示例以添加一个明确的None测试。明确测试None永远不会出错,因此我默认使用它。
最后,正如其他人在本页面上所指出的,两个前导下划线表示您希望变量在模块内“私有”。如果您要执行import * from mymodule操作,Python将不会将具有两个前导下划线的名称导入您的命名空间中。但是,如果只需执行简单的import mymodule,然后说dir(mymodule),您将看到列表中的“私有”变量,并且如果您明确引用mymodule.__DBNAME__,Python不会介意,它只会让您引用它。双下划线是向您的模块用户发出的重要提示,告诉他们您不希望他们将该名称重新绑定到他们自己的某个值。
在Python中,最好的做法是不要使用import *,而是通过mymodule.something或显式执行诸如from mymodule import something之类的导入,以最小化耦合并最大化明确性。
编辑:如果出于某种原因,您需要在Python的旧版本中执行此类操作,该版本没有global关键字,则有一种简单的解决方法。不要直接设置模块全局变量,而是在模块全局级别使用可变类型,并在其中存储您的值。
在函数中,全局变量名称将为只读;您将无法重新绑定实际的全局变量名称。(如果您在函数内部分配给该变量名称,则仅会影响函数内部的局部变量名称。)但是,您可以使用该局部变量名称访问实际的全局对象并在其中存储数据。
您可以使用一个list,但代码会很丑陋:
__DBNAME__ = [None] # use length-1 list as a mutable

# later, in code:  
if __DBNAME__[0] is None:
    __DBNAME__[0] = name

使用 dict 更好。但最方便的是一个类实例,你可以使用一个简单的类:

class Box:
    pass

__m = Box()  # m will contain all module-level values
__m.dbname = None  # database name global in module

# later, in code:
if __m.dbname is None:
    __m.dbname = name

(您实际上不需要将数据库名称变量大写。)

我喜欢使用__m.dbname而不是__m["DBNAME"]的语法糖;在我看来,这似乎是最方便的解决方案。但是dict的解决方案也可以正常工作。

使用dict时,您可以使用任何可哈希值作为键,但是当您满意于名称是有效标识符时,您可以像上面那样使用一个简单的类Box


12
双下划线会导致名称混淆。通常,使用单个下划线就足以表示变量应被视为私有。https://dev59.com/X2w05IYBdhLWcg3w72XN - H.Rabiee
关于Box类,定义dbname = None在__init__函数中是否比在外部定义更好,就像示例中那样? - SuperGeo
1
Python 不关心变量如何设置。有关Box类或类似的类,定义了__init __()函数的方法,可以从kwargs中获取所有值并在类字典中设置它们。然后,您只需执行_m = Box(dbname ="whatever")即可使其整洁。自 Python 3.3以来,现在有types.SimpleNameSpace ,它是Box类的全功能实现;见:https://docs.python.org/3/library/types.html#additional-utility-classes-and-functions - steveha

129

通过在模块上明确访问它们来显式访问模块级别的变量


简而言之: 这里描述的技术与steveha的答案相同,除了不创建人为的辅助对象来显式地限定变量的作用域。 相反,模块对象本身被给予一个变量指针,因此可以在任何地方访问时提供显式的作用域。 (就像局部函数作用域中的赋值一样)

将其视为当前模块的self,而不是当前实例!

# db.py
import sys

# this is a pointer to the module object instance itself.
this = sys.modules[__name__]

# we can explicitly make assignments on it 
this.db_name = None

def initialize_db(name):
    if (this.db_name is None):
        # also in local function scope. no scope specifier like global is needed
        this.db_name = name
        # also the name remains free for local use
        db_name = "Locally scoped db_name variable. Doesn't do anything here."
    else:
        msg = "Database is already initialized to {0}."
        raise RuntimeError(msg.format(this.db_name))

由于模块被缓存,因此只会导入一次,您可以在任意数量的客户端上多次导入db.py,操作相同的通用状态:

# client_a.py
import db

db.initialize_db('mongo')

# client_b.py
import db

if (db.db_name == 'mongo'):
    db.db_name = None  # this is the preferred way of usage, as it updates the value for all clients, because they access the same reference from the same module object

# client_c.py
from db import db_name
# be careful when importing like this, as a new reference "db_name" will
# be created in the module namespace of client_c, which points to the value 
# that "db.db_name" has at import time of "client_c".

if (db_name == 'mongo'):  # checking is fine if "db.db_name" doesn't change
    db_name = None  # be careful, because this only assigns the reference client_c.db_name to a new value, but leaves db.db_name pointing to its current value.

作为额外的奖励,我发现它总体上非常符合Python的政策:明确优于隐含。

1
我喜欢第二个模块中可以使用更精确的“from db import”,即使你必须在主模块中执行较大的“import db”。如果您跳过“sys”魔法并在initialize_db中使用“global”,似乎也是如此。您能否评论全局变量与您的答案的优缺点,因为它们似乎都可以达到相同的效果? - Alain Collins
3
在我看来,这似乎是做这件事最干净的方式,但我的代码检查工具不允许这样做。我是做错了什么还是你/其他人也遇到了这个问题?非常感谢,Chris。 - ThePosey
将模块全局变量绑定到模块本身看起来非常酷,但现在如果客户想要更改模块的全局变量,他们只能使用 import db,不能再使用更明确的 from db import something。从可用性的角度来看,这并不那么酷,是吧? - Alex Che
已经在生产代码中成功使用了一段时间。最近开始使用mypy,但它并不喜欢这种模式。由于变量在模块首次初始化时被声明并设置为None,并且直到调用“create”函数后才获得其值,因此它会抱怨属性不存在,并且不能在“非self属性”上声明类型。至少删除this可以消除第二个错误。 - pyansharp
同意,我也想要一个关于如何在mypy /静态分析中正确使用此模式的指南。这还不可能吗?将来可能吗?虽然可能但需要付出不合理的努力吗?这只是动态特性的有效用法和静态类型检查之间的严重不匹配吗? - timmwagener
显示剩余2条评论

36

Steveha的答案对我有所帮助,但是忽略了一个重要点(我认为wisty想到了这一点)。如果在函数中只访问变量而不对其进行赋值,则不需要使用global关键字。

如果您在没有使用global关键字的情况下分配变量,则Python会创建一个新的本地变量--模块变量的值现在将隐藏在函数内部。使用global关键字在函数内部分配模块变量。

Pylint 1.3.1在Python 2.7下执行,如果您不分配变量,则不应使用global。

module_var = '/dev/hello'

def readonly_access():
    connect(module_var)

def readwrite_access():
    global module_var
    module_var = '/dev/hello2'
    connect(module_var)

8

为此,您需要将变量声明为全局变量。然而,使用module_name.var_name,也可以从模块外部访问全局变量。请将以下内容添加为模块的第一行:

global __DBNAME__

谢谢,Jarret。不幸的是,当我尝试这样做并在控制台上运行dir(mymodule)时,它显示变量可用,我可以访问它们。我是否误解了您的意思? - daveslab
请记住,在Python中,按照惯例,_DBNAME(单下划线)被视为私有变量。这仅对类进行了半强制执行,并且对于“裸”代码根本没有执行,但是大多数优秀的程序员都会将_var视为私有。 - Chinmay Kanchi
没错,@cgkanchi,但我想知道它是否严格可执行。 - daveslab
1
把整个东西放在一个类中。这样,至少想要访问私有变量的人必须做一些工作。 - Chinmay Kanchi
3
无法强制实施,daveslab。在Python中,我们都是成年人,私有和受保护的变量最好通过契约和惯例来实现,而不是通过任何严格强制执行的编译机制。 - Jarret Hardie
显示剩余2条评论

-13

你正在陷入一个微妙的怪异之中。在Python函数内部,你不能重新分配模块级变量。我认为这是为了防止人们意外地在函数内部重新分配东西。

你可以访问模块命名空间,但不应该尝试重新分配。如果你的函数分配了某些东西,它会自动成为一个函数变量 - Python不会查找模块命名空间。

你可以这样做:

__DB_NAME__ = None

def func():
    if __DB_NAME__:
        connect(__DB_NAME__)
    else:
        connect(Default_value)

但你不能在函数内重新分配__DB_NAME__

一种解决方法:

__DB_NAME__ = [None]

def func():
    if __DB_NAME__[0]:
        connect(__DB_NAME__[0])
    else:
        __DB_NAME__[0] = Default_value

请注意,我并没有重新分配__DB_NAME__,我只是修改了它的内容。

13
这不是真的。global 关键字允许你设置模块级别的变量名。 - dbn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接