【Python】变量作用域

Posted by 西维蜀黍 on 2019-10-27, Last Modified on 2024-01-09

Python的作用域分类

Python的作用域一共有4种,分别是:

  • L (Local) 局部作用域
  • E (Enclosing) 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域

以 L –> E –> G –>B 的顺序去查找,即:当在局部作用域中找不到时,便会去局部作用域之外的作用域去找(例如闭包),再找不到就会去全局作用域中找,再者去内建作用域中找。最终,如果仍没有找到,则会抛出NameError错误。

下面举一个实用LEGB法则的例子:

globalVar = 100           #G

def test_scope():
    enclosingVar = 200    #E
    def func():
        localVar = 300    #L
print __name__            #B

Python的作用域分析

除了defclasslambda 之外,其他如 ifelifelsetry - exceptforwhile的 statement并不能改变变量的作用域。换句话说,在这些 statement 之内声明的变量,在它们之外还是可以访问的,比如:

>>> if True:
>>> ...     a = 'I am A'
>>> ... 
>>> a
>>> 'I am A'

在这个例子中,定义在 if 语句中的变量a,在 if 语句块的外部还是被可以访问的。

这意味着,如果if 语句块被 defclasslambda 包裹,而且变量在 if 语句块中被声明,则该变量就是此函数、或类、或 lambda 的局部变量。


defclasslambda 中声明的变量,其作用域就是对应方法、或者类、或者 lambda 的局部作用域。

局部作用域会覆盖全局作用域,但不会影响全局作用域。

g = 1  #全局的
def fun():
    g = 2 #局部的
    return g

print fun()
# 结果为2
print g
# 结果为1

改变作用域的关键字

global 关键字

经典问题 - UnboundLocalError: local variable <X> referenced before assignment

Problem

要注意,如果在函数内部修改了全局变量,就会出现以下错误:

#file1.py
var = 1
def fun1():
    print var
    var = 200
print fun()

#file2.py
var = 1
def fun2():
    var = var + 1
    return var
print fun()

这两个函数都会报错:UnboundLocalError: local variable ‘var’ referenced before assignment。

file1.py 的中,变量 var 被赋值了,其实上面的实现等价于:

#file1.py
var = 1
def fun():
	  var = None
    print var
    var = 200
print fun()

Analysis

因为,这个 var 变量的作用域其实是整个 fun1() 函数,因此 Python Interpreter 会将所有在这个作用域下的变量在函数开头先进行声明(declaration)。

这样看,就很好理解为什么local variable 'var' referenced before assignment,即在进行赋值操作前( var = 200),该变量就被引用(referenced)了(即在 print var 语句中被引用了)。

你可能会说,我们已经在全局域下给 var 赋值了(var = 1),因此不应该报错。别忘了,当在局部作用域中找不到时,便会去局部作用域之外的作用域去找(在这个 case 中,局部作用域之外的作用域就是全局作用域)。而事实上,在函数的局部作用域中,我们已经声明了一个 var 变量(var = None),因此就不会再去部作用域之外的作用域找了。

Solution

结论就是,如果我们要在函数中修改一个全局变量的值,需要在函数定义最开始的位置下声明 global {variable name}(在这个 case 中,就是 global var),这其实是告诉 Python Interpreter ,在当前作用域中的变量 var,就是全局作用域下的那个 var

闭包(Closure) - nonlocal 关键字

闭包的定义:如果在一个内部函数里,对在外部函数中声明(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。

a = 1
def external():
    global a
    a = 200
    print a

    b = 100
    def internal():
        # nonlocal b
        print b
        b = 200
        return b

    internal()
    print b

print external()

如果运行这段代码,会在 print b 位置报 UnboundLocalError: local variable 'b' referenced before assignment 的错误。

这其实还是因为我们在更小的作用域(即 internal()中)修改了在更大的作用域(即 external()中)中声明的变量,虽然错误不是报在修改变量的那一行。

在Python3,有个关键字nonlocal可以解决这个问题;而在Python2中,没有这个关键字,因此不要尝试修改闭包中的变量。

locals()globals()

globals()locals() 提供了基于字典的访问全局变量和局部变量的方式

globals()

比如:如果在函数 a() 内需要定义一个局部变量,而这个局部变量的名字和另一个函数B(对应于下面 case 的 b())的名字相同,但又要在函数 A内引用这个函数B。

def b():
    print("call b()")

def a():
    b = 'Just a String'
    b_function = globals()['b']
    print b
    b_function()
    print(type(b_function))

a()
# Just a String
# call b()
# <type 'function'>

locals()

如果你使用过Python的Web框架,那么你一定经历过需要把一个视图函数内很多的局部变量传递给模板引擎,然后作用在HTML上。虽然你可以有一些更聪明的做法,还你是仍想一次传递多个变量。

@app.route('/')
def view():
    user = User.query.all()
    article = Article.query.all()
    ip = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
    s = 'Just a String'
    return render_template('index.html', user=user,
            article = article, ip=ip, s=s)
    #或者 return render_template('index.html', **locals())

Reference