如何在Python中创建模块级变量?
有没有办法在一个模块里设置一个全局变量?我试着用最简单的方法来做,如下所示,但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 个回答
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)
通过直接访问模块级变量来明确访问
简单来说:这里描述的技巧和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的风格,因为它很好地体现了Python的原则:明确优于隐含。
这里发生了什么。
首先,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
。