如何在Python中定义自由变量?

54

Python文档中获取的 局部/全局/自由变量 的定义:

如果一个名称在一个代码块内被绑定,它是该代码块的局部变量,除非已声明为非本地变量。如果一个名称在模块级别被绑定,它是一个全局变量。(模块代码块的变量是局部和全局的。) 如果一个变量在代码块中被使用但在其中没有定义,则它是一个自由变量


代码1:

>>> x = 0
>>> def foo():
...   print(x)
...   print(locals())
... 
>>> foo()
0
{}

代码2:

>>> def bar():
...   x = 1
...   def foo():
...     print(x)
...     print(locals())
...   foo()
... 
>>> bar()
1
{'x':1}

自由变量是在函数块中调用locals()时返回的,但在类块中不会返回。


Code 1中,x是一个全局变量,它被使用但未在foo()中定义。
然而它不是一个自由变量,因为它没有被locals()返回。
我认为这不是文档所说的。是否有一个技术上的自由变量定义?


8
非常好的问题。背后隐藏着意想不到的深度。 - nalply
5个回答

55

自由变量的定义:被使用,但既不是全局变量也不是绑定变量

例如:

  1. 在代码1中,x不是自由变量,因为它是一个全局变量。
  2. 在代码2的bar()中,x不是自由变量,因为它是一个绑定变量。
  3. xfoo()中是自由变量。

Python之所以进行这样的区分,是因为闭包。自由变量在当前的环境中即局部变量集合中没有定义,并且也不是全局变量!因此它必须在其他地方定义。这就是闭包的概念。在代码2中,foo()引用了在bar()中定义的x。Python使用词法作用域。这意味着解释器能够通过仅查看代码来确定作用域。

例如:xfoo()中被视为一个变量,因为foo()bar()封闭,而xbar()中被绑定。

全局作用域在Python中被特殊处理。虽然将全局作用域视为最外层的作用域是可能的,但这样做会影响性能(我认为)。因此,x既不可能同时是自由变量又是全局变量。

例外情况

生活并不总是那么简单。存在自由全局变量。Python文档(执行模型)中说:

global语句具有与同一块中名称绑定操作相同的作用域。如果自由变量的最近封闭作用域包含一个global语句,则自由变量被视为全局变量。

>>> x = 42
>>> def foo():
...   global x
...   def baz():
...     print(x)
...     print(locals())
...   baz()
... 
>>> foo()
42
{}

我自己也不知道这一点。我们都在这里学习。


你的意思是:x 不能同时作为一个自由变量全局变量吗? - kev
1
“同一时间”是什么意思?这是词法作用域和动态作用域之间的重要区别。为了让您的生活更轻松,我只想说:不,x不能同时是自由全局的。 - nalply
1
为什么全局变量不能是自由变量?虽然文档中没有说明。 - kev
3
“自由全球”在这里有点夸张。无论是有还是没有全局x线,print(x)中的x都来自全局作用域。在baz中添加nonlocal x会导致语法错误,因为x不是自由变量,而是全局变量。 为什么?因为Python开发人员将它们定义为相互独立的,就是这样。他们专门为闭包和封闭作用域定义了“自由变量”。实际上,您可以在baz中添加“global y”,但无法添加“nonlocal y”;某些“a”既不能是“global”也不能是“nonlocal”。全局和本地作用域也是如此:在“baz”中的“print(x)”之后不能执行“x = 100”(作用域混合)。 - WloHu
4
“自由变量被视为全局变量”可能有些误导,因为它暗示某些自由变量被视为全局变量,因此既是全局的又是自由的,这是错误的。在foo内检查baz,使用baz.__code__.co_freevars显示没有自由变量。他们可能的意思是“自由变量的候选者被视为全局变量”。 - WloHu

8
从我所理解的来看,文档在自由变量上的确有点模糊不清。有一些是被视为普通全局变量的“自由全局变量”和“词法绑定的自由变量”。Eli Bendersky 在 关于符号表的博客文章 中对此进行了很好的总结。
不幸的是,Python 核心中存在一种简写方式,可能会最初让读者对何为“自由”变量感到困惑。幸运的是,这只是一个非常微小的混淆,很容易澄清。执行模型参考说:
如果一个变量在代码块中被使用但没有在那里定义,它就是一个自由变量。
这与正式定义是一致的。然而,在源代码中,“free”实际上被用作“词法绑定的自由变量”的简写(即在封闭作用域中找到了绑定的变量),而“global”则用于指代所有剩余的自由变量。因此,在阅读 CPython 源代码时,重要的是要记住,完整的自由变量集包括既被标记为“free”的变量,也被标记为“global”的变量。
因此,为避免混淆,当我想引用 CPython 中实际处理为自由变量的变量时,我会说“词法绑定”。
这个缩写被使用的原因可能是因为当你有一个全局自由变量时,字节码发出的实际上没有任何变化。如果一个全局变量是'free'或者不是都不会改变该名称查找将使用LOAD_GLOBAL的事实。因此,全局自由变量并不是那么特殊。
另一方面,词法绑定变量被特别处理,并且被包含在cell对象中,这些对象是存储词法绑定自由变量的空间,并位于给定函数的__closure__属性中。对于这些变量,创建了一个特殊的LOAD_DEREF指令,它检查自由变量存在的单元格。 LOAD_DEREF指令的描述如下:

LOAD_DEREF(i)

加载存储在单元格和自由变量存储器的第i个插槽中的单元格

在Python中,自由变量只有在一个具有状态的对象的定义在另一个具有状态的对象的定义内部时,才会作为一个概念产生影响。请注意,这里的“静态嵌套”指的是词法上的嵌套。

3
为了避免新手被误导,上面Nikhil的评论“变量只是保留内存位置来存储值”的说法对于Python是完全错误的。
在Python中有“名称”和“值”。值具有类型,而不是名称。为值保留内存空间,而不是名称。例如,我们可以先有x = 1,然后在代码的后面有x =“a string”,最后是x = [3,9.1]。在这些赋值过程中,名称x首先指向整数,然后指向字符串,最后指向列表。当进行赋值时,赋值语句左侧的名称被指向右侧的值。值可以是不可更改(不可变)或可更改(可变)。整数、字符串、元组等是不可变的;列表等是可变的。由于整数是不可变的,因此当有两个语句如下时:
x = 4
x = x +1
第二个语句使x指向一个新值5,它并没有改变指向x所指向的内存位置的值从4到5!
这与C语言的模型非常不同!

2

在Python中,没有明确的关键字来声明自由变量。基于函数的定义以及其中和周围的语句,Python会将变量分类为绑定变量、单元格变量和自由变量。

下面的示例使用函数的代码对象来说明这个概念,代码对象封装了上一段提到的变量。

def func(arg1, arg2=2):
    def inner_func(arg3=arg2):
        return arg1, arg3
    arg1, arg2 = None
    return inner_func

对于'func':
• 'arg1'和'arg2'是'绑定变量'
• 'arg1'是'单元变量',因为它是'inner_func'内部的'自由变量'
• 没有'自由变量'。
func.__code__.co_varnames

('arg1', 'arg2', 'inner_func')

func.__code__.co_cellvars

('arg1',)

func.__code__.co_freevars

()

对于 'inner_func':

arg3 是一个 绑定变量

arg1 是一个 自由变量

• 没有单元格变量

inner_func.__code__.co_varnames

('arg3',)

inner_func.__code__.co_freevars

('arg1')

inner_func.__code__.co_cellvars

()


0
变量只是保留内存位置以存储值。这意味着当您创建一个变量时,会在内存中保留一些空间。
根据变量的数据类型,解释器会分配内存并决定可以存储什么样的值在预留的内存中。因此,通过给变量分配不同的数据类型,您可以在这些变量中存储整数、小数或字符。
给变量赋值
Python变量不需要显式声明来保留内存空间。当您给变量赋值时,声明会自动发生。等号(=)用于将值赋给变量。

这看起来是一个不错的答案,但您能否确切地解释一下“自由变量”的定义是什么? - RedBassett
这与问题无关。 - Mad Physicist
答案甚至没有提到“免费”这个词,更不用说具有误导性了。 - Mad Physicist

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