Python 中的块范围

在使用其他语言编写代码时,有时会创建一个块作用域,如下所示:

statement
...
statement
{
statement
...
statement
}
statement
...
statement

其中一个目的是提高代码的可读性: 显示某些语句形成一个逻辑单元,或者某些局部变量仅在该块中使用。

Is there an idiomatic way of doing the same thing in Python?

35904 次浏览

Python 中的惯用方法是保持函数的简短。如果您认为需要这样做,请重构代码!:)

Python 为每个模块、类、函数、生成器表达式、结果理解、集合理解创建了一个新范围,Python 3. x 也为每个列表内涵创建了新范围。除此之外,函数内部没有嵌套的作用域。

不,对于创建块作用域没有语言支持。

以下构造创建范围:

  • 模组
  • 同学们
  • 函数(包括 lambda)
  • 生成器表达式生成器表达式
  • 理解(dict、 set、 list (在 Python 3.x 中))

通过在函数内部声明一个函数,然后立即调用它,您可以执行类似于 Python 中的 C + + 块作用域的操作。例如:

def my_func():
shared_variable = calculate_thing()


def do_first_thing():
... = shared_variable
do_first_thing()


def do_second_thing():
foo(shared_variable)
...
do_second_thing()

如果你不确定为什么要这样做,那么 this video可能会说服你。

The basic principle is to scope everything as tightly as possible without introducing any 'garbage' (extra types/functions) into a wider scope than is absolutely required - Nothing else wants to use the do_first_thing() method for example so it shouldn't be scoped outside the calling function.

我同意没有块作用域。但是在 Python3中有一个位置使它成为 看起来,就好像它有块作用域一样。

到底发生了什么事,才会有这种表情?

这在 Python2中可以正常工作,但是为了在 Python3中阻止变量泄漏,他们已经完成了这个技巧,这个更改使它看起来像是在这里有块作用域。

听我解释。


根据作用域的概念,当我们在同一作用域中引入具有相同名称的变量时,应该修改它的值。

这就是 Python 2中发生的情况:

>>> x = 'OLD'
>>> sample = [x for x in 'NEW']
>>> x
'W'

But in Python 3, even though the variable with same name is introduced, it does not override, and the list comprehension acts like a sandbox for some reason, and it seems like creating a new scope in it.

>>> x = 'OLD'
>>> sample = [x for x in 'NEW']
>>> x
'OLD'

这个答案与答案 托马斯的证词 创建作用域的唯一方法是函数、类或模块相反,因为这看起来像是另一个创建新范围的地方。

模块(和包)是将程序划分为单独名称空间的一种很好的 Python 方法,这似乎是这个问题的一个隐含目标。事实上,当我学习 Python 的基础知识时,我对缺乏块作用域特性感到沮丧。然而,一旦我理解了 Python 模块,我就可以更优雅地实现我以前的目标,而不需要使用块作用域。

作为动机,并且为了指引人们正确的方向,我认为提供一些 Python 范围结构的明确示例是有用的。首先,我解释了我使用 Python 类实现块作用域的失败尝试。接下来,我将解释如何使用 Python 模块实现更有用的东西。最后,我概述了软件包在加载和过滤数据方面的实际应用。

尝试使用类实现块范围

有那么一会儿,我想我已经通过在类声明中插入代码实现了块作用域:

x = 5
class BlockScopeAttempt:
x = 10
print(x) # Output: 10
print(x) # Output: 5

不幸的是,当定义一个函数时,这种方法就会失效:

x = 5
class BlockScopeAttempt:
x = 10
print(x) # Output: 10
def printx2():
print(x)
printx2() # Output: 5!!!

That’s because functions defined within a class use global scope. The easiest (though not the only) way to fix this is to explicitly specify the class:

x = 5
class BlockScopeAttempt:
x = 10
print(x) # Output: 10
def printx2():
print(BlockScopeAttempt.x)  # Added class name
printx2() # Output: 10

这并不那么优雅,因为必须根据函数是否包含在类中来编写不同的函数。

使用 Python 模块获得更好的结果

模块与静态类非常相似,但是根据我的经验,模块要干净得多。为了对模块进行同样的操作,我在当前工作目录中创建了一个名为 my_module.py的文件,其内容如下:

x = 10
print(x) # (A)


def printx():
print(x) # (B)


def alter_x():
global x
x = 8


def do_nothing():
# Here x is local to the function.
x = 9

Then in my main file or interactive (e.g. Jupyter) session, I do

x = 5
from my_module import printx, do_nothing, alter_x  # Output: 10 from (A)
printx()  # Output: 10 from (B)
do_nothing()
printx()  # Output: 10
alter_x()
printx()  # Output: 8
print(x) # Output: 5
from my_module import x  # Copies current value from module
print(x) # Output: 8
x = 7
printx()  # Output: 8
import my_module
my_module.x = 6
printx()  # Output: 6

作为解释,每个 Python 文件定义一个模块,该模块具有自己的全局命名空间。import my_module命令允许您使用 .语法访问此命名空间中的变量。我认为模块就像静态类。

如果在交互式会话中使用模块,则可以在开始时执行以下两行

%load_ext autoreload
%autoreload 2

和模块将自动重新加载时,其相应的文件被修改。

用于加载和筛选数据的包

包的思想是模块概念的一个小小的扩展。包是一个包含(可能是空白的) __init__.py文件的目录,该文件在导入时执行。可以使用 .语法访问此目录中的模块/包。

对于数据分析,我通常需要读取一个大型数据文件,然后交互式地应用各种过滤器。读取一个文件需要几分钟,所以我只想做一次。根据我在学校里学到的关于面向对象程序设计的知识,我曾经认为一个人应该把过滤和加载的代码作为一个类的方法来编写。这种方法的一个主要缺点是,如果重新定义过滤器,类的定义就会发生变化,因此我必须重新加载整个类,包括数据。

现在使用 Python,我定义了一个名为 my_data的包,其中包含名为 loadfilter的子模块。在 filter.py内部,我可以做一个相对导入:

from .load import raw_data

If I modify filter.py, then autoreload will detect the changes. It doesn't reload load.py, so I don't need to reload my data. This way I can prototype my filtering code in a Jupyter notebook, wrap it as a function, and then cut-paste from my notebook directly into filter.py. Figuring this out revolutionized my workflow, and converted me from a skeptic to a believer in the “Zen of Python.”

为了完整起见: 使用 del结束局部变量的作用域。参见 Del 在 Python 中什么时候有用。不过,这当然不是惯用语。

statement
statement


# Begin block
a = ...
b = ...
statement
statement
del a, b
# End block


statement

我已经想出了一个解决方案,它具有最简单的接口和最少的额外名称引入到您的代码中。

from scoping import scoping
a = 2
with scoping():
assert(2 == a)
a = 3
b = 4
scoping.keep('b')
assert(3 == a)
assert(2 == a)
assert(4 == b)

Https://pypi.org/project/scoping/