Jason Pan

深入理解 Python 编程

潘忠显 / 2024-04-28


本页中,会整理一些对 Python 语言和编程的深入理解的小主题。

我会不断的往页面里添加新主题,每个主题也会单独的发在公众号上,欢迎关注公众号【老白码农在奋斗】。

有些没有整理清楚的,我会放在《Python 面试》页面中。已经整理清楚的列表如下:

一、nonlocal 关键字,变量、可变对象、不可变对象

先从一段报错的代码说起。下边这段代码,是在 compress 函数中定义了一个 append_compress_pair 函数并会循环调用该函数,会报错 “UnboundLocalError: local variable ‘ret_p’ referenced before assignment”。这里缺少的就是一个 nonlocal 的声明。

class Solution:
    def compress(self, chars) -> int:
        ret_p = 0

        def append_compress_pair(c):
            chars[ret_p] = c
            ret_p += 1
#...
        append_compress_pair(c)

接下来,本文会介绍一下 Python 的作用域,然后引出 nonlocal 关键字解决的问题,最后简单说明下 Python 的变量、可变对象、不可变对象的关系。

本地作用域与模块全局作用域

本节中,我们通过几个简单的 case 我们可以复习一下本地作用域和模块全局作用域。

a = 1
def f():
    print(a)

f()
a = 1
def f():
    print(a)
    a = 2

f()

依然报错:“UnboundLocalError: local variable ‘a’ referenced before assignment”,因为 f() 里边有对 a 变量的改变,因此被视为本地(局部)变量,而外边的则是模块全局变量。因此,先调用 print(a) 时,该变量实际没有被赋值。

a = 0
def f():
    global a
    print(a)
    a = 2

f()
def f():
    global a
    print(a)
    a = 2

a = 0
f()
def f():
    global a
    print(a)
    a = 2

f()
a = 0

两个作用域的局限性

如果本地作用域变量 +global 使用全局作用域变量,就类似于 C 语言中的作用域的划分—— C 语言中只有两个级别的作用域:全局作用域和局部作用域,因为C语言中,函数定义不能嵌套

但在 Python 中,虽然函数通常在顶层定义,但是允许函数定义放在任何地方,包括函数里边——函数定义的嵌套。这样的嵌套就带来了除了本地和**全局(顶级作用域)**之外的作用域的变量。

举个例子,下边用注释 <== 指示的那个 x 变量,在 inner() 函数作用域内来看,它既不是“本地”变量,也不是“全局”变量。

x = 0
def outer():
    x = 1  # <== ???
    def inner():
        x = 2
        print("inner:", x)

    inner()
    print("outer:", x)

outer()
print("global:", x)

nonlocal 关键字的引入

介绍了上边的局限性,就有必要来引入一种使用中间作用域的方式。PEP 3104 中有介绍,有很多建议,最终选择了 nonlocal。虽然它有点长,而且比其他一些选项听起来更不太好听,但它的描述确实更精确:它声明一个不是本地的名称

我们将在上边的程序的基础上,做下简单的改动。改动之前,显然的输出是:

inner: 2
outer: 1
global: 0

我们在 x = 2 之前加上我们熟悉的 global x 将会打印(inner 中修改的是最外层的 x):

inner: 2
outer: 1
global: 2

而如果我们在 x = 2 之前加上 nolocal x 将会打印(inner 中修改的是 outer() 中创建的):

inner: 2
outer: 2
global: 0

Python 中的变量、可变对象、不可变对象

我们将上边的代码再修改一下:

x = [0] * 3
def outer():
    x[1] = 1
    def inner():
        x[2] = 2
        print("inner:", x)
    inner()
    print("outer:", x)
outer()
print("global:", x)

这将会打印:

inner: [0, 1, 2]
outer: [0, 1, 2]
global: [0, 1, 2]

看上去不使用 nonlocalglobal 也能改变外边的变量?我们以此来引出变量和对象的介绍。

在 Python 中,变量实际上是对对象的引用。我们考虑下边的两行代码:

x = "abc"
x[0] = "b"  ## <= TypeError: 'str' object does not support item assignment
x = "bcd"

所以,当我们说"改变 x 的值"时,我们实际上是在说"改变x引用的对象"。


我没再考虑下下边三行代码:

x = [1, 1, 1]
x[0] = 0
x = [0, 1, 2]

通过上边两段代码,我们除了看到变量和对象的关系,还看到了可变对象和不可变对象的区别

再回到本节最开头的例子,就很清楚了,inner() 里边是可以获得外部 x 引用的哪个对象,然后修改了其对象,导致 outer() 和 全局范围都看到了对象的改变。