我可以在Python的任何地方定义作用域吗?

11 投票
7 回答
4314 浏览
提问于 2025-04-17 05:50

有时候我发现自己在几行代码里需要用到一些很长的函数名,比如 os.path.abspathos.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 个回答

2

简单来说,答案是“不”。

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在=符号左边看到一个变量时,它会认为你是在创建一个新的局部变量。globalnonlocal关键字是用来表明你打算修改一个不在函数作用域内的变量。

5

这大致上能满足你的需求,但你需要重复写名字。

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:
    ...
20

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

撰写回答