在外部作用域中定义名称的遮蔽问题是什么?

331

我刚刚转换到PyCharm,我非常满意它提供的所有警告和提示来改善我的代码。除了这个我不理解的:

此检查检测到在外部作用域中定义的重复名称。

我知道从外部作用域访问变量是不好的做法,但是影响外部作用域有什么问题呢?

这里有一个示例,PyCharm 给出了警告信息:

data = [4, 5, 6]

def print_data(data): # <-- Warning: "Shadows 'data' from outer scope
    print data

print_data(data)

1
我还搜索了字符串"This inspection detects...",但在PyCharm在线帮助文档中没有找到:http://www.jetbrains.com/pycharm/webhelp/getting-help.html - Framester
3
在PyCharm中关闭此消息:按下 <Ctrl>+<Alt>+s(设置),然后选择 Editor,再选择 Inspections,找到 "Shadowing names from outer scopes",将其取消勾选即可。 - ChaimG
我遇到了相同的警告,但是代码看起来并没有问题。该函数使用本地参数“data”,而不是全局“data”。当然,如果您重命名参数,则必须在函数范围内重命名其出现次数。不确定插件的作者在想什么。就像他从未使用过IDE的代码重构一样。 - emeraldhieu
从PyCharm 2023开始,关闭“Shadows”警告的路径是文件 -> 设置 -> 编辑器 -> 检查 -> 外部作用域中的名称遮蔽 - user3785010
11个回答

318

在上面的代码片段中并没有什么大不了的问题,但是想象一下一个有更多参数和更多代码行的函数。然后你决定将data参数重命名为yadda,但是错过了函数体中其中一个使用它的地方...现在data指向全局,你开始出现奇怪的行为 - 如果没有全局名称data,你会有一个更明显的NameError

还要记住的是,在Python中,所有内容都是对象(包括模块、类和函数),因此函数、模块或类之间没有不同的命名空间。另一个场景是,在模块顶部导入函数foo,并在函数体中的某个地方使用它。然后你给函数添加了一个新的参数,并将其命名为-倒霉的是-foo

最后,内置的函数和类型也存在于同一个命名空间中,可以以相同的方式被影响。

如果你的函数很短,命名合理并且具有良好的单元测试覆盖率,那么这些都不是大问题,但是有时候你必须维护不太完美的代码,并且被警告此类可能出现的问题可能会有所帮助。


44
幸运的是,PyCharm(由OP使用)有一个非常好的重命名操作,可以在相同作用域内将变量的名称重命名到所有使用该变量的地方,从而减少了重命名错误的可能性。 - wojtow
3
除了PyCharm的重命名操作外,我希望还能有针对引用外部作用域变量的特殊语法高亮功能。这两项功能将使得繁琐的变量作用域解析游戏变得无关紧要。 - Leo
附注:您可以使用nonlocal关键字来明确地使外部得分引用(就像在闭包中一样)。请注意,这与遮蔽不同,因为它明确地不会遮蔽来自外部的变量。 - Felix D.
我认为这不是正确的答案,也没有提出解决方案。我认为这应该是答案:https://dev59.com/B2Ij5IYBdhLWcg3wkl2U#40008745 - Hanan Shteingart
@HananShteingart 我已经评论了为什么你认为的答案不是正确的,这也是我自己答案的一部分。 - bruno desthuilliers

264

目前投票最高且被接受的答案以及大部分回答都没有抓住重点。

你的函数有多长,你的变量名称是否具有描述性(以希望最小化可能的名称冲突的机会)并不重要。

事实上,你的函数的局部变量或其参数恰好在全局范围内共享一个名称是完全无关紧要的。而且实际上,无论你选择地多么仔细,你的本地变量名都不能预见“我的酷炫名称yadda将来也会用作全局变量吗?”。解决方法?只需不必担心!正确的想法是设计你的函数从其签名参数中获取输入。这样你就不需要关心全局范围内是什么(或将会是什么),阴影问题就根本不是问题了。

换句话说,当你的函数需要使用相同名称的局部变量全局变量时,阴影问题才真正有意义。但你应该避免这样的设计。OP的代码并没有真正存在这样的设计问题。只是PyCharm不够聪明,并在必要时发出警告。因此,只是为了让PyCharm高兴,也为了让我们的代码更干净,可以查看来自silyevsk的答案的解决方案,将全局变量完全删除。

def print_data(data):
    print data

def main():
    data = [4, 5, 6]
    print_data(data)

main()

这是正确的方式来“解决”这个问题,通过修复/删除你的全局事件,而不是调整你当前的本地函数。


20
当然,在完美的世界中,你永远不会打错字,或者当你改变参数时忘记其中一个搜索替换,但是错误总是会发生的,这就是PyCharm所说的:“警告-技术上没有错误,但这很容易成为一个问题”。 - dwanderson
7
我完全同意函数应尽可能地“纯净”,但你完全忽略了两个重要点:如果没有在本地定义,那么无法限制Python在封闭范围中查找名称,而且所有内容(模块、函数、类等)都是对象,并且与任何其他“变量”存在于同一命名空间中。在你提供的代码片段中,print_data 是一个全局变量。想一想…… - bruno desthuilliers
2
我来到这个线程是因为我正在使用在函数中定义的函数,以使外部函数更易读,而不会弄乱全局命名空间或笨拙地使用单独的文件。这个例子并不适用于那种一般情况,即非本地非全局变量被遮蔽。 - micseydel
3
同意。这里的问题是Python作用域。在当前作用域之外隐式访问对象会引发麻烦。谁会想要那样!可惜,否则Python是一种思路相当成熟的语言(尽管模块命名存在类似的歧义)。 - CodeCabbie
2
@florianH,我不使用PyCharm,也许你可以在main()的结尾处设置一个断点? - RayLuo
显示剩余6条评论

61

在某些情况下,一个好的解决方法可能是将变量和代码移动到另一个函数中:

def print_data(data):
    print data

def main():
    data = [4, 5, 6]
    print_data(data)

main()

1
是的。我认为一个好的 IDE 能够通过重构来处理局部变量和全局变量。你的提示确实有助于消除原始 IDE 的潜在风险。 - stanleyxu2005
我不会称之为变通方法,而是更好的设计。 - pabouk - Ukraine stay strong

6

我希望在PyCharm的右上角看到一个绿色的勾号。我会在变量名后附加下划线以清除此警告,以便我可以集中注意力处理重要的警告。

data = [4, 5, 6]

def print_data(data_):
    print(data_)

print_data(data)

7
容易出错的时候,当忘记了 _ 。 - o17t H1H' S'k
3
我最初点赞了这个,然后也这样做了。我现在正在跨所有项目恢复代码以_有意地_掩盖全局变量(当我不想或不需要它时)。我同意@eyaler的看法,这极易出错。 - Christopher Galpin
2
# noinspection PyShadowingNames - Christopher Galpin

5

这取决于函数的长度。函数越长,未来进行修改的人写data以为它指的是全局变量的可能性就越大。实际上,它指的是局部变量,但因为该函数非常长,他们不知道存在一个同名的局部变量。

对于您的示例函数,我认为覆盖全局变量并不算坏事。


3

看起来这是100%的 pytest 代码模式。

参见:

pytest fixtures: 明确、模块化、可扩展

我也遇到了同样的问题,这就是我找到这篇文章的原因 ;)

# ./tests/test_twitter1.py
import os
import pytest

from mylib import db
# ...

@pytest.fixture
def twitter():
    twitter_ = db.Twitter()
    twitter_._debug = True
    return twitter_

@pytest.mark.parametrize("query,expected", [
    ("BANCO PROVINCIAL", 8),
    ("name", 6),
    ("castlabs", 42),
])
def test_search(twitter: db.Twitter, query: str, expected: int):

    for query in queries:
        res = twitter.search(query)
        print(res)
        assert res

它将使用This inspection detects shadowing names defined in outer scopes.警告。

要解决这个问题,只需将twitter fixture移动到./tests/conftest.py中即可。

# ./tests/conftest.py
import pytest

from syntropy import db


@pytest.fixture
def twitter():
    twitter_ = db.Twitter()
    twitter_._debug = True
    return twitter_

请删除 twitter fixture,就像在 ./tests/test_twitter2.py 中所做的那样:

# ./tests/test_twitter2.py
import os
import pytest

from mylib import db
# ...

@pytest.mark.parametrize("query,expected", [
    ("BANCO PROVINCIAL", 8),
    ("name", 6),
    ("castlabs", 42),
])
def test_search(twitter: db.Twitter, query: str, expected: int):

    for query in queries:
        res = twitter.search(query)
        print(res)
        assert res

这将让QA、PyCharm和所有人都感到高兴。


2
data = [4, 5, 6] # Your global variable

def print_data(data): # <-- Pass in a parameter called "data"
    print data  # <-- Note: You can access global variable inside your function, BUT for now, which is which? the parameter or the global variable? Confused, huh?

print_data(data)

60
我不为所动,很明显是参数的问题。 - user395760
2
@delnan,你可能在这个简单的例子中不会感到困惑,但是如果其他函数在附近定义并使用了全局变量 data,都深埋在几百行的代码之中呢? - John Colanduoni
15
@HevyLight 我不需要查看其他附近的函数。我只看这个函数,就可以看到data函数中的本地名称,因此我甚至不会费心去检查/记住是否存在同名的全局变量,更不用说它包含了什么内容了。 - user395760
4
我不认为这种推理是正确的,因为如果要使用全局变量,你需要在函数内定义“全局数据”,否则该全局变量将无法访问。 - CodyF
3
如果没有定义,只是尝试使用data,会在不同的作用域中查找直到找到一个变量,因此它会找到全局变量data,并返回Falsedata = [1, 2, 3]; def foo(): print(data); foo() - dwanderson

2

我认为这个规则并没有太大的帮助。我只是通过进入 设置 -> 编辑器 -> 检查,然后取消勾选这个规则来禁用它:

Shadowing names from outer scope

1

请执行以下操作:

data = [4, 5, 6]

def print_data():
    global data
    print(data)

print_data()

  1. 这个函数不再有参数了。
  2. 如果你只需要读取全局变量,就不需要使用 global 声明一个全局变量。
- pabouk - Ukraine stay strong
不要这样做!尽可能避免使用global - OBu

0

要忽略这个警告,就像Christopher在评论中说的那样,你可以在它上面加注释

# noinspection PyShadowingNames

忽视一个警告只因为它让人烦恼,并不能真正回答提问者的问题,这个问题实际上是 - 是什么原因导致了这个问题,而不仅仅是“我不喜欢看到警告”。 - Michael Tuchman
@MichaelTuchman 这是StackOverflow的工作方式:如果原帖的问题已经得到了足够的答案,就像在这个例子中一样,发布其他人可能会发现有用的答案是很有意义的。 - Joshua Wolff
谢谢你分享你的观点。我的观点是,教人们如何忽视警告而不去面对引起问题的原因很少有用。但你随意吧。 - Michael Tuchman
1
@MichaelTuchman 这个工具非常有用,因为尽管PyCharm很好,但它还是存在一些bug,会错误地显示警告。除非你想要屏蔽该类别的所有警告,否则唯一的解决办法就是逐行或逐类忽略它。所以它不仅显然有用,我每天都使用它,因为这是必需的。我也发布过类似性质的答案,并且得到了很多赞同(显然是有用的)。 - Joshua Wolff
我接受了。我取消了负面投票。不过,我确实看到了太多这种“如何忽略它”的类型。@Joshua Wolff - Michael Tuchman
好的,谢谢你,你不必这么做。@MichaelTuchman - Joshua Wolff

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