我可以在Python的任何地方定义作用域吗?
有时候我发现自己在几行代码里需要用到一些很长的函数名,比如 os.path.abspath
和 os.path.dirname
,而且用得非常频繁。我觉得把这些函数放到全局命名空间里不太合适,但如果能在我需要这些函数的代码块周围定义一个范围,那就太好了。举个例子,这样做会很完美:
import os, sys
closure:
abspath = os.path.abspath
dirname = os.path.dirname
# 15 lines of heavy usage of those functions
# Can't access abspath or dirname here
我很想知道这样做是否可行
7 个回答
简单来说,答案是“不”。
Python有三种作用域。分别是函数作用域、全局(也叫模块)作用域和内置作用域。你不能再声明其他作用域。
一个class
的声明看起来有点像一个作用域,但其实不是。它基本上是给一个对象分配一堆字段的简写。这个类里的函数不能直接访问这些字段,必须通过它们所定义的对象来访问。
这听起来限制有点多,其实并没有那么糟。在Python中,你还可以嵌套定义函数。嵌套的函数可以只读访问外层的作用域。这是“动态”的。函数定义之前不需要提到名字。下面是一个例子:
def joe(x):
def bar():
return y
def baz(z):
y = x + 20
return x
y = x+5
return bar, baz
>>> a, b = joe(5)
>>> b(20)
5
>>> a()
10
所以,你可以通过定义一个嵌套函数来实现这个效果,这个嵌套函数创建你需要的值,使用它们,然后返回结果,这样就不会牺牲太多的局部性。
我记得在学习Python的时候,适应那些比较奇怪的作用域规则是比较困难的部分之一。当引入嵌套函数时,我觉得它们让作用域规则变得更奇怪,因为外层作用域是只读的,而闭包的作用域是动态的。
显然,在Python3中,有一种方法可以使用nonlocal
关键字“导入”来自外层作用域的变量(类似于global
关键字),这样你就可以在读写的上下文中使用它:
def joe(x):
def bar():
return y
def baz(z):
nonlocal y
y = x + 20
return x
y = x+5
return bar, baz
>>> a, b = joe(5)
>>> b(20)
5
>>> a()
25
否则,每当Python在=
符号左边看到一个变量时,它会认为你是在创建一个新的局部变量。global
和nonlocal
关键字是用来表明你打算修改一个不在函数作用域内的变量。
这大致上能满足你的需求,但你需要重复写名字。
try:
abspath = os.path.abspath
dirname = os.path.dirname
# fifteen lines of code
finally:
del abspath
del dirname
这样做可以避免在下面这种情况下出现命名空间污染的问题。
try:
...
try:
abspath = os.path.abspath
dirname = os.path.dirname
# fifteen lines of code
finally:
del abspath
del dirname
... # don't want abspath or dirname in scope here even if there was
... # an exception in the above block
except:
...
Python没有像Lisp或Scheme中的let那样的临时命名空间工具。
在Python中,通常的做法是把名字放在当前的命名空间中,等用完了再把它们拿掉。这种方法在标准库中使用得非常多:
abspath = os.path.abspath
dirname = os.path.dirname
# 15 lines of heavy usage of those functions
a = abspath(somepath)
d = dirname(somepath)
...
del abspath, dirname
另一种减少输入工作量的方法是缩短重复的前缀:
>>> import math as m
>>> m.sin(x / 2.0) + m.sin(x * m.pi)
>>> p = os.path
...
>>> a = p.abspath(somepath)
>>> d = p.dirname(somepath)
标准库中常用的另一种方法是,不用担心污染模块的命名空间,而是依靠__all__来列出你打算公开的名字。关于__all__导入语句的文档中找到讨论。
当然,你也可以通过把名字存储在字典中来创建自己的命名空间(不过这种方法不太常见):
d = dict(abspath = os.path.abspath,
dirname = os.path.dirname)
...
a = d['abspath'](somepath)
d = d['dirname'](somepath)
最后,你可以把所有代码放在一个函数里(函数有自己的局部命名空间),但这样有一些缺点:
- 设置起来比较麻烦(这是一种不常见且有点神秘的函数用法)
- 你需要把任何想要做的非临时赋值声明为global。
- 代码在你调用函数之前是不会运行的。
def temp(): # disadvantage 1: awkward setup global a, d # disadvantage 2: global declarations abspath = os.path.abspath dirname = os.path.dirname # 15 lines of heavy usage of those functions a = abspath(somepath) d = dirname(somepath) temp() # disadvantage 3: invoking the code