导入模块中全局变量的可见性

179

我在编写Python脚本时,遇到了导入模块的问题。我会尽力描述错误、原因和为什么我要采用这种特定的方法来解决我的问题(我稍后会进行描述):

假设我有一个模块,在其中定义了一些实用函数/类,它们引用了命名空间中定义的实体(我们称此实体为"a"):

module1:

def f():
    print a

然后我有一个主程序,在那里定义了变量 "a",我想导入这些实用工具:

import module1
a=3
module1.f()

执行该程序将会触发以下错误:

Traceback (most recent call last):
  File "Z:\Python\main.py", line 10, in <module>
    module1.f()
  File "Z:\Python\module1.py", line 3, in f
    print a
NameError: global name 'a' is not defined

过去(两天前)曾提出类似的问题,并提出了几种解决方案,但我认为它们都不适合我的需求。 这是我的特定背景:

我正在尝试制作一个连接到MySQL数据库服务器并使用GUI显示/修改数据的Python程序。出于清洁起见,我将大量辅助/实用程序MySQL相关函数定义在一个单独的文件中。但是,它们都有一个共同的变量,我最初定义在工具模块内部的,即来自MySQLdb模块的游标对象。

后来我意识到,应该在主模块中定义游标对象(用于与db服务器通信),以便主模块和任何导入到其中的内容都可以访问该对象。

最终结果将如下所示:

utilities_module.py:

def utility_1(args):
    code which references a variable named "cur"
def utility_n(args):
    etcetera

而我的主模块:

program.py:

import MySQLdb, Tkinter
db=MySQLdb.connect(#blahblah) ; cur=db.cursor()  #cur is defined!
from utilities_module import *

然后,一旦我尝试调用任何utilities函数,就会触发上述的“全局名称未定义”错误。

一个特别的建议是在utilities文件中使用“from program import cur”的语句,如下所示:

utilities_module.py:

from program import cur
#rest of function definitions

程序.py:

import Tkinter, MySQLdb
db=MySQLdb.connect(#blahblah) ; cur=db.cursor()  #cur is defined!
from utilities_module import *

但那是循环导入或类似的问题,总之,它也会崩溃。所以我的问题是:

我该如何让主模块中定义的“cur”对象对导入到其中的那些辅助函数可见?

感谢您的时间,如果解决方案已在其他地方发布,我深表歉意。我只是找不到答案,已经没有更多的技巧了。


1
根据您的更新:您可能不想要一个共享的单一光标。一个共享的连接是可以的,但是光标很便宜,并且通常有多个光标同时存在的好理由(例如,这样您就可以通过两个光标进行迭代而不必fetch_all并遍历两个列表,或者只是因为您可以有两个不同的线程/绿色线程/回调链/任何东西使用数据库而没有冲突)。 - abarnert
1
无论如何,你想分享什么,我认为在这里的答案是将 db(如果坚持要用,则包括 cur)移入一个单独的模块中,让 programutilities_module 都从该模块导入。这样你就不会遇到循环依赖(即从程序导入模块,再从这些模块导入程序)及由此带来的混乱。 - abarnert
9个回答

346
在Python中,全局变量是针对一个模块而言的,不同于所有模块间共享。这让许多人感到困惑,因为在C语言中,除非你明确将变量声明为"static",否则全局变量在所有实现文件中都是相同的。
根据您实际的用例,有不同的解决方法。
在采取这种方法之前,首先要问自己是否真的需要使其成为全局变量。也许您真正想要的是类,并将"f"作为实例方法,而不仅仅是自由函数?那么您可以像这样做:
import module1
thingy1 = module1.Thingy(a=3)
thingy1.f()

如果你只是想要一个全局变量,但是它仅供 module1 使用,请在该模块中设置它。
import module1
module1.a=3
module1.f()

另一方面,如果a被许多模块共享,请将其放在其他地方,并让每个人导入它:
import shared_stuff
import module1
shared_stuff.a = 3
module1.f()

...同时,在module1.py中:

import shared_stuff
def f():
    print shared_stuff.a

不要使用 from 导入,除非该变量是常量。例如,from shared_stuff import a 会创建一个新的 a 变量,其初始值为导入时 shared_stuff.a 所表示的值,并且这个新的 a 变量不会受到对 shared_stuff.a 赋值的影响。


或者,在极少数情况下,如果你真的需要将它作为全局变量在任何地方都可用,就像内置变量一样,请将它添加到内置模块中。具体细节在 Python 2.x 和 3.x 中有所不同。在 3.x 中,它的工作方式如下:

import builtins
import module1
builtins.a = 3
module1.f()

谢谢你的回答。我会尝试使用import shared_stuff方法,虽然我无法忽视在主模块中定义的变量并不是真正的全局变量这一事实 - 难道没有任何方法可以使一个名称真正地对所有人都可访问,无论在程序的哪个位置? - Nubarke
2
让某些东西“对所有程序都可访问”非常违背了Python的Python之禅,特别是“明确优于隐式”。Python具有非常良好的面向对象设计,如果您使用得当,您可能会在Python职业生涯的其余时间里再也不需要使用全局关键字了。 - Bi Rico
1
更新:谢谢!使用 shared_stuff 方法非常好用,我认为我可以解决任何其他问题。 - Nubarke
2
@DanielArmengod:我添加了内置答案,以防您确实需要它。 (如果您需要更多详细信息,请搜索SO;至少有两个关于如何正确添加东西到内置的问题。)但是,正如Bi Rico所说,您几乎肯定不真正需要它,也不想要它。 - abarnert
1
@BiRico:进一步思考后,"命名空间是一个非常棒的想法"比"显式优于隐式"更为贴切。每个模块都有自己的全局变量是这个伟大想法的典型案例,其他所有东西都是基于它构建的(至少在概念上;在细节方面,新式类可能是最好的范例)。 - abarnert
这句话让人茅塞顿开:“除非变量是常量,否则不要使用from导入。from shared_stuff import a会创建一个新的a变量,其初始值为导入时shared_stuff.a所引用的值,而这个新的a变量不会受到对shared_stuff.a的赋值影响。” - undefined

18

作为解决方法,您可以考虑在外层设置环境变量,像这样。

main.py:


import os
os.environ['MYVAL'] = str(myintvariable)

mymodule.py:

=>

mymodule.py:

import os

myval = None
if 'MYVAL' in os.environ:
    myval = os.environ['MYVAL']

作为额外的预防措施,在模块内处理MYVAL未被定义的情况。


7

这篇文章是我遇到的Python行为的观察。也许你读到的建议如果你像下面所做的一样,可能对你没有用。

具体来说,我有一个模块,其中包含全局/共享变量(如上所建议):

#sharedstuff.py

globaltimes_randomnode=[]
globalist_randomnode=[]

然后我有一个主模块,导入了共享组件,代码如下:

import sharedstuff as shared

还有一些其他模块实际上填充了这些数组,它们由主模块调用。当退出这些其他模块时,我可以清楚地看到这些数组已经被填充了。但是,在主模块中读取它们时,它们却是空的。对我来说这很奇怪(好吧,我是新手)。然而,当我改变在主模块中导入sharedstuff.py的方式为:

from globals import *

它有效了(数组已经被填充)。

就是这样。


6
一个函数使用所在模块的全局变量。例如,不应该设置 a = 3,而应该设置 module1.a = 3。因此,如果要在 utilities_module 中将 cur 设置为全局变量,请设置 utilities_module.cur
更好的解决方案:不要使用全局变量。将需要的变量传递到需要它们的函数中,或创建一个类来捆绑所有数据,并在初始化实例时传递它。

如果用户写的是'from module1 import f' 而非 'import module1',那么f将会出现在main.py的全局命名空间中。现在在main.py中如果我们使用f(),那么由于a=3和f(函数定义)都在main的全局命名空间中,这是一个解决方案吗?如果我错了,请指导我任何关于此主题的文章。 - variable
我使用了上述方法,并在实例化使用全局变量的类时将全局变量作为参数传递。这样做还可以,但后来我们启用了Sonarqube代码检查,发现函数有太多参数。因此,我们不得不寻找另一种解决方案。现在我们使用环境变量,每个需要它们的模块都会读取它们。虽然这不是真正的面向对象编程规范,但就是这样。这仅在代码执行期间全局变量不改变时才有效。 - rimetnac

3

由于全局变量是模块特定的,您可以将以下函数添加到所有导入的模块中,然后使用它来:

  • 将单个变量(以字典格式)作为全局变量添加到这些模块中
  • 将您的模块全局变量传递给它。

addglobals = lambda x: globals().update(x)

然后,您只需要传递当前全局变量即可:

import module

module.addglobals(globals())


3

解决这个问题最简单的方法就是在模块中添加另一个函数,将光标存储在一个全局变量中。然后所有其他函数也可以使用它。

module1:

cursor = None

def setCursor(cur):
    global cursor
    cursor = cur

def method(some, args):
    global cursor
    do_stuff(cursor, some, args)

主程序:

import module1

cursor = get_a_cursor()
module1.setCursor(cursor)
module1.method()

2

更新

为了测试这个理论,我创建了一个模块并将其放在了pypi上。一切都完美地运作。

pip install superglobals

简短回答

这在Python 2或3中都可以正常工作:

import inspect

def superglobals():
    _globals = dict(inspect.getmembers(
                inspect.stack()[len(inspect.stack()) - 1][0]))["f_globals"]
    return _globals

将文件名保存为superglobals.py,然后在另一个模块中这样使用:

from superglobals import *

superglobals()['var'] = value

扩展答案

您可以添加一些额外的功能,使事情更加吸引人。


def superglobals():
    _globals = dict(inspect.getmembers(
                inspect.stack()[len(inspect.stack()) - 1][0]))["f_globals"]
    return _globals

def getglobal(key, default=None):
    """
    getglobal(key[, default]) -> value
    
    Return the value for key if key is in the global dictionary, else default.
    """
    _globals = dict(inspect.getmembers(
                inspect.stack()[len(inspect.stack()) - 1][0]))["f_globals"]
    return _globals.get(key, default)

def setglobal(key, value):
    _globals = superglobals()
    _globals[key] = value

def defaultglobal(key, value):
    """
    defaultglobal(key, value)

    Set the value of global variable `key` if it is not otherwise st
    """
    _globals = superglobals()
    if key not in _globals:
        _globals[key] = value

然后这样使用:
from superglobals import *

setglobal('test', 123)
defaultglobal('test', 456)
assert(getglobal('test') == 123)

正当性

这个问题中充斥着“Python纯洁主义”的答案,虽然完全正确,但在某些环境下(比如基本上是单线程的具有大型全局实例化API的IDAPython),这并不是那么重要。

这仍然是一种不好的形式和不良习惯,不应该鼓励,但有时候这样做更容易。特别是当你编写的代码的生命周期不会很长时。


嘿!这是一些很棒的东西,但我认为它不适用于ipython,只适用于python。你有没有什么想法,怎样才能使它在ipython中以稳健的方式工作?谢谢! :) - Gabi
1
ipython是魔鬼。不过你可以尝试将第二行改为_globals = globals() - Orwellophile

1

由于我在之前的回答中没有看到,所以我想添加一个简单的解决方法,即向需要调用模块全局变量的函数添加一个global_dict参数,然后在调用函数时将字典传递给函数; 例如:

# external_module
def imported_function(global_dict=None):
    print(global_dict["a"])


# calling_module
a = 12
from external_module import imported_function
imported_function(global_dict=globals())

>>> 12

0

面向对象编程的方式是将您的模块制作成一个类,而不是一组未绑定的方法。然后,您可以使用__init__或setter方法从调用者那里设置变量,以便在模块方法中使用。


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