深入理解 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 我们可以复习一下本地作用域和模块全局作用域。
- case 1 在函数内使用外部变量,这里只有读取的操作:
a = 1
def f():
print(a)
f()
- case 2 全局变量与局部变量
a = 1
def f():
print(a)
a = 2
f()
依然报错:“UnboundLocalError: local variable ‘a’ referenced before assignment”,因为 f()
里边有对 a
变量的改变,因此被视为本地(局部)变量,而外边的则是模块全局变量。因此,先调用 print(a)
时,该变量实际没有被赋值。
- case 3 正确的全局变量的使用(如果赋值成 2 的确定是外部的变量,则可以加上
global a
的声明:
a = 0
def f():
global a
print(a)
a = 2
f()
- case 4 调换一下全局变量的位置,下边的代码依然可以正常运行。函数调用之前赋值就可以。
def f():
global a
print(a)
a = 2
a = 0
f()
- case 5 调用之后,会有类似于找不到符号的错误。跟之前的错误有所不同,本次报错:“NameError: name ‘a’ is not defined”:
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]
看上去不使用 nonlocal
和 global
也能改变外边的变量?我们以此来引出变量和对象的介绍。
在 Python 中,变量实际上是对对象的引用。我们考虑下边的两行代码:
x = "abc"
x[0] = "b" ## <= TypeError: 'str' object does not support item assignment
x = "bcd"
- 当你写
x = "abc"
时,Python实际上做的是创建一个整数对象"abc"
,然后让变量x
引用它 - 当你接着写
x = "bcd"
时,Python会创建一个新的整数对象"bcd"
,然后改变x
的引用,让它指向新的对象"abc"
。原来的整数对象"abc"
并没有被改变,因为整数是不可变的。如果没有其他变量引用它,它将被垃圾回收。
所以,当我们说"改变 x
的值"时,我们实际上是在说"改变x
引用的对象"。
我没再考虑下下边三行代码:
x = [1, 1, 1]
x[0] = 0
x = [0, 1, 2]
x = [1, 1, 1]
:这行代码创建了一个新的列表对象[1, 1, 1]
,然后让变量x
引用它。这个列表对象是可变的。x[0] = 0
:这行代码改变了x
引用的列表对象。它把列表的第一个元素从1
改为了0
。现在,x
引用的列表对象变成了[0, 1, 1]
。这里我们改变的是对象本身,而不是x
的引用。x = [0, 1, 2]
:这行代码创建了一个新的列表对象[0, 1, 2]
,然后改变了x
的引用,让它指向新的列表。原来的列表对象[0, 1, 1]
并没有被改变,但是x
不再引用它了。如果没有其他变量引用它,它将被垃圾回收。这里,我们改变的是x
的引用,而不是对象本身。
通过上边两段代码,我们除了看到变量和对象的关系,还看到了可变对象和不可变对象的区别:
- 对于不可变对象(如整数、字符串、元组),我们不能改变对象本身,但我们可以改变变量所引用的对象
- 对于可变对象(如列表、字典、集合),我们既可以改变对象本身,也可以改变变量所引用的对象。
再回到本节最开头的例子,就很清楚了,inner()
里边是可以获得外部 x 引用的哪个对象,然后修改了其对象,导致 outer() 和 全局范围都看到了对象的改变。