程序员文章、书籍推荐和程序员创业信息与资源分享平台

网站首页 > 技术文章 正文

python散装笔记——13: 可变范围和约束力

hfteth 2025-02-10 12:35:36 技术文章 14 ℃

1: 非本地变量

Python 3.x 版本 ≥ 3.0

Python 3 增加了一个新关键字,叫做 nonlocalnonlocal 关键字为内部作用域添加了一个作用域覆盖。您可以在 PEP 3104 中阅读有关它的全部内容。最好用几个代码示例来说明这一点。最常见的例子之一是创建可以递增的函数:

Bash
def counter():
    num = 0
    def incrementer():
        num += 1
        return num
    return incrementer

如果您尝试运行这段代码,就会收到 UnboundLocalError,因为 num 变量在最内层函数中赋值之前就被引用了。让我们把非本地添加到组合中:

Bash
def counter():
    num = 0
    def incrementer():
        nonlocal num
        num += 1
        return num
    return incrementer

c = counter()
c() # = 1
c() # = 2
c() # = 3

基本上,nonlocal 允许你赋值给外层作用域中的变量,但不允许赋值给全局作用域中的变量。因此,你不能在我们的计数器函数中使用 nonlocal,因为它会试图赋值给全局作用域。试一试,你很快就会得到一个 SyntaxError。相反,您必须在嵌套函数中使用 nonlocal

(请注意,这里介绍的功能最好使用生成器来实现。)

2: 全局变量

在 Python 中,当且仅当函数内部的变量出现在赋值语句的左侧或其他绑定情况下,它们才被视为局部变量;否则,这种绑定将在外层函数中查找,直到全局作用域。即使赋值语句从未被执行,情况也是如此。

x = 'Hi'

def read_x():
  print(x) # x 刚刚被引用,因此假定是全局的
    
read_x() # 打印出 Hi

def read_y():
  print(y) # 这里的 y 只是引用,因此假定是全局的

read_y() # NameError: global name 'y' is not defined

def read_y():
    y = 'Hey' # y 出现在赋值中,因此它是本地的
    print(y) # 将找到本地 y
    
read_y() # 打印出 Hey

def read_x_local_fail():
    if False:
      x = 'Hey' # x 出现在一个赋值中,因此它是本地的
    print(x) # 将查找_local_ z,但它没有被赋值,因此不会被找到
    
read_x_local_fail() # UnboundLocalError: local variable 'x' referenced before assignment

通常,作用域内的赋值会对同名的外层变量产生阴影:

x = 'Hi'

def change_local_x():
    x = 'Bye'
    print(x)
change_local_x() # 打印出 Bye
print(x) # 打印出 Hi

声明一个名称 global 意味着,在作用域的其余部分,对该名称的任何赋值都将在模块的顶层进行:

x = 'Hi'

def change_global_x():
    global x
    x = 'Bye'
    print(x)
    
change_global_x() # 打印出 Bye
print(x) # 打印出 Bye

global 关键字意味着赋值发生在模块的顶层,而不是程序的顶层。其他模块仍然需要对模块内的变量进行通常的点式访问。

总结一下:为了知道变量 x 是否是函数的局部变量,你应该读取 entire 函数:

  1. 如果找到了 global x,那么 x 就是一个 global 变量; 2.
  2. 如果找到了 nonlocal x,那么 x 属于外层函数,既不是局部变量也不是全局变量
  3. 如果你找到了 x = 5for x in range(3) 或其他绑定,那么 x 是一个 `局部变量

否则 x 属于某个外层作用域(函数作用域、全局作用域或内置变量)

3: 本地(局部)变量

如果一个名称在函数中被 bound,默认情况下它只能在函数中被访问:

def foo():
    a = 5
    print(a) # ok

print(a) # NameError: name 'a' is not defined

控制流构造对作用域没有影响(except 除外),但访问尚未赋值的变量是一个错误:

def foo():
    if True:
      a = 5
    print(a) # ok
    
b = 3
def bar():
    if False:
      b = 5
    print(b) # UnboundLocalError: local variable 'b' referenced before assignment

常见的绑定操作包括赋值、for 循环和增强赋值,如 a += 5

4: del命令

该命令有几种既相关又不同的形式。

del v

如果 v 是一个变量,命令 del v 会从变量的作用域中删除该变量。例如

x = 5
print(x) # 输出: 5
del x
print(x) # NameError: name 'x' is not defined

请注意,del 是一种绑定发生方式,这意味着除非另有明确说明(使用非本地或全局),否则 del v 将使 v 成为当前作用域的本地变量。如果您打算在外层作用域中删除 v,请在 del v 语句的同一作用域中使用非本地 v 或全局 v

在下文中,命令的意图是一种默认行为,但并不是由语言强制执行的。类的编写方式可能会使这一意图失效。

del v.name

这条命令会调用 v.__delattr__(name)

其目的是使属性名不可用。例如

class A:
  pass

a = A()
a.x = 7
print(a.x) # 输出: 7
del a.x
print(a.x) # error: AttributeError: 'A' object has no attribute 'x'
del v[item]

这条命令会触发对 v.__delitem__(item) 的调用。

这样做的目的是使 item 不属于对象 v 实现的映射:

x = {'a': 1, 'b': 2}
del x['a']
print(x) # 输出: {'b': 2}
print(x['a']) # error: KeyError: 'a'
del v[a:b]

这实际上调用了 v.__delslice__(a,b)。 其意图与上述类似,但使用的是片段--项目范围而不是单个项目。例如

x = [0, 1, 2, 3, 4]
del x[1:3]
print(x) # 输出: [0, 3, 4]

5: 函数在查找名称时跳过类范围

类在定义时具有局部作用域,但类内部的函数在查找名称时不会使用该作用域。由于 lambdas 是函数,而理解是使用函数作用域实现的,这可能会导致一些令人惊讶的行为。

a = 'global'

class Fred:
  a = 'class' # class scope
  b = (a for i in range(10)) # function scope
  c = [a for i in range(10)] # function scope
  d = a # class scope
  e = lambda: a # function scope
  f = lambda a=a: a # 默认参数使用类范围
  
  @staticmethod # 或 @classmethod,或普通实例方法
  def g(): # function scope
    return a
  
print(Fred.a) # class
print(next(Fred.b)) # global
print(Fred.c[0]) # class in Python 2, global in Python 3
print(Fred.d) # class
print(Fred.e()) # global
print(Fred.f()) # class
print(Fred.g()) # global

不熟悉此作用域工作原理的用户可能会认为 bce 是打印类。

摘自 PEP 227:

类作用域中的名称不可访问。名称在最内层的外层函数作用域中解析。 如果类定义出现在嵌套作用域链中,解析过程会跳过类定义。

摘自 Python 关于命名和绑定的文档:

在类块中定义的名称的作用域仅限于类块;它不会扩展到方法的代码块 - 这包括理解式和生成器表达式,因为它们是使用函数作用域实现的。这意味着以下操作将失败:

class A:
a = 42
b = list(a + i for i in range(10))

6: 局部范围与全局范围

什么是局部作用域和全局作用域? 所有在代码中的某个点可以访问的 Python 变量要么在局部作用域中,要么在全局作用域中。

本地作用域包括在当前函数中定义的所有变量,而全局作用域包括在当前函数之外定义的变量。

foo = 1 # global

def func():
  bar = 2 # local
  print(foo) # 从全局范围打印变量 foo
  print(bar) # 从本地作用域打印变量 bar

我们可以检查哪些变量在哪个作用域中。内置函数 locals()globals() 会以字典形式返回整个作用域。

foo = 1

def func():
  bar = 2
  print(globals().keys()) # 打印全局范围内的所有变量名
  print(locals().keys()) # 打印本地作用域中的所有变量名

名称冲突会怎样?

foo = 1
def func():
  foo = 2 # 在本地作用域创建一个新变量 foo,全局变量 foo 不受影响

  print(foo) # 打印出 2

  # 全局变量 foo 依然存在,没有变化:
  print(globals()['foo']) # 打印出 1
  print(locals()['foo']) # 打印出 2

要修改全局变量,请使用关键字 global

foo = 1

def func():
  global foo
  foo = 2 # 会修改全局变量 foo,而不是创建一个本地变量

作用域是为整个函数体定义的!

这意味着一个变量绝不会在函数的半部分是全局变量,而在函数的半部分之后是局部变量,反之亦然。

foo = 1

def func():
    # 这个函数有一个局部变量 foo,因为它是在下面定义的。
    # 因此,从这一点来看,foo 是本地的。全局的 foo 是隐藏的。

    print(foo) # raises UnboundLocalError, because local foo is not yet initialized
    foo = 7
    print(foo)

同样,反过来也是一样:

foo = 1

def func():
    # 在这个函数中,foo 从一开始就是一个全局变量

    foo = 7 # 全局 foo 被修改
    print(foo) # 7
    print(globals()['foo']) # 7
    global foo # 可以在函数的任何位置
    print(foo) # 7

函数中的函数

函数中可能嵌套有多级函数,但在任何一个函数中,该函数只有一个局部作用域和全局作用域。没有中间作用域。

foo = 1

def f1():
    bar = 1
    
    def f2():
        baz = 2
        # 这里,foo 是全局变量,baz 是局部变量
        # baz不在这两个范围内
        print(locals().keys()) # ['baz']
        print('bar' in locals()) # False
        print('bar' in globals()) # False
        
    def f3():
        baz = 3
        print(bar) # f1 中的 bar 被引用,因此它进入了 f3 的局部作用域(闭包)
        print(locals().keys()) # ['bar', 'baz']
        print('bar' in locals()) # True
        print('bar' in globals()) # False
        
    def f4():
        bar = 4 # 一个新的局部变量 bar 从 f1 的局部作用域中隐藏了 bar
        baz = 4
        print(bar)
        print(locals().keys()) # ['bar', 'baz']
        print('bar' in locals()) # True
        print('bar' in globals()) # False

全局 “与 ”非本地"(仅适用于 Python 3)

这两个关键字都用于获得对非当前函数本地变量的写访问权限。

global 关键字声明一个名称应被视为全局变量。

foo = 0 # global foo]

def f1():
    foo = 1 # a new foo local in f1
    
    def f2():
        foo = 2 # a new foo local in f2
        
        def f3():
            foo = 3 # a new foo local in f3
            print(foo) # 3
            foo = 30 # modifies local foo in f3 only
        
        def f4():
            global foo
            print(foo) # 0
            foo = 100 # modifies global foo

另一方面,在 Python 3 中可用的 nonlocal (见 非局部变量 ),将局部变量从外层作用域带入当前函数的局部作用域。

摘自关于 nonlocal 的 Python 文档:

非局部语句会使列出的标识符引用最近的外层作用域中先前绑定的变量,不包括全局变量。

Python 3.x 版本 ≥ 3.0

def f1():
    
    def f2():
      foo = 2 # a new foo local in f2
        
        def f3():
          nonlocal foo # foo from f2, which is the nearest enclosing scope
          print(foo) # 2
          foo = 20 # modifies foo from f2!

7: 绑定事件

x = 5
x += 7
for x in iterable: pass

上述每条语句都是绑定语句 -- x5 所表示的对象绑定。 如果该语句出现在函数内部,则 x 默认为函数本地语句。有关绑定语句的列表。

最近发表
标签列表