代码块细述(必看)
代码块可以使得一段python代码作为一个单元、一个整体执行。以下是 官方手册 的描述。
A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the ‘-c’ option) is a code block. The string argument passed to the built-in functions eval() and exec() is a code block.
所以,有以下几种类型的代码块:
- 模块文件是一个代码块
- 函数体是一个代码块
- class的定义是一个代码块
- 交互式(python idle)的每一个命令行都是一个独立的代码块
- 脚本文件是一个代码块
- 脚本命令是一个代码块(python -c "xxx")
- eval()和exec()中的内容也都有各自的代码块
代码块的作用是组织代码,同时意味着退出代码区块范围就退出了作用域范围。例如退出函数区块,就退出了函数的作用域,使得函数内的本地变量无法被函数的外界访问。
此外,python是解释性语言,读一行解释一行,这意味着每读一行就忘记前一行。但实际上更严格的说法是读一个代码块解释一个代码块,这意味着读代码块中的内容时,是暂时记住属于这个代码块中所读内容的,读完整个代码块后再以统筹的形式解释这个代码块。
先说明读一行解释一行的情况,也就是每一行都属于一个代码块,这个只能通过python的交互式工具idle工具来测试:
>>> x=2000
>>> y=2000
>>> x is y
False
>>> x=2000;y=2000
>>> x is y
True
理论上分号是语句的分隔符,并不会影响结果。但为什么第一个x is y
为False,而第二个x is y
为True?
首先分析第一个x is y
。由于交互式工具idle中每一个命令都是一个单独的语句块,这使得解释完x=2000
后立刻就忘记了2000这个数值对象,同时忘记的还有x变量本身。然后再读取解释y=2000
,因为不记得刚才解释的x=2000
,所以会在内存中重新创建一个数值结构用来保存2000这个数值,然后用y指向它。换句话说,x和y所指向的2000在内存中是不同的数据对象,所以x is y
为False。
下面的x is y
返回True:
>>> x=2000;y=2000
>>> x is y
True
因为python按行解释,一个命令是一个代码块。对于x=2000;y=2000
,python首先读取这一整行,发现x和y的数值对象都是2000,于是做个简单优化,等价于x,y=2000,2000
,这意味着它们属于一个代码块内,由于都是2000,所以只会在内存中创建一个数据对象,然后x和y都引用这个数据对象。所以,x is y
返回True。
idle工具中每个命令都是独立的代码块,但是py文件却是一个完整的代码块,其内还可以嵌套其它代码块(如函数、exec()等)。所以,如果上面的分行赋值语句放在py文件中,得到的结果将是True。
例如:
x = 2000
y = 2000
print(x is y) # True
def f1():
z=2000
z1=2000
print(x is z) # False
print(z is z1) # True
f1()
python先读取x=2000
,并在内存中创建一个属于全局作用域的2000数据对象,再解释y=2000的时候,发现这个全局对象2000已经存在了(因为x和y同处于全局代码块内),所以不会再额外创建新的2000对象。这里反映出来的结果是"同一个代码块内,虽然仍然是读一行解释一行,但在退出这个代码块之前,不会忘记这个代码块中的内容,而且会统筹安排这个代码块"。
同理def f1()
内的代码块,因为z是本地作用域的变量,更标准的是处于不同代码块内,所以会在本地作用域内存区创建新的数据对象2000,所以x is z
返回False。根据前面的解释,z1 is z
返回True。
再回顾前文多次出现的一个异常:
x = 3
def f1():
print(x)
x=4
f1()
报错信息:
UnboundLocalError: local variable 'x' referenced before assignment
当执行到def语句的时候,因为def声明函数,函数体是一个代码块,所以按照代码块的方式读取属于这个代码块中的内容。首先读取print(x)
,但并不会直接解释,而是会记住它,并继续向下读取,于是读取x=4,这意味着x是一个本地变量。然后统筹安排整个代码块,将print(x)的x认为是本地变量而非全局变量。注意,直到def退出的时候都还没有进行x的赋值,而是记录了本地变量x,赋值操作是在函数调用的时候进行的。当调用函数f()的时候,发现print(x)中的x是本地变量,但因为还没有赋值,所以报错。
但是再看下面的,为什么又返回True?
>>> x=256
>>> y=256
>>> x is y
True
因为Python在启动的时候就在内存中预先为常用的较小整数值(-5到256)创建好了对象,因为它们使用的非常频繁(有些在python的内部已经使用了)。所以,对于这个范围内的整数,都是直接引用,不会再在内存中额外创建新的数值对象,所以x is y
总是返回true。甚至,这些小值整数可以跨作用域:
x = 3
def f1():
y=3
print(x is y) # True
f1()
再看前文循环内的函数的问题。
def f1():
for i in range(5):
def n():
print(i)
return n
f1()()
前面对现象已经解释过,内部函数n()中print(i)的i不会随循环的迭代而改变,而是固定的值i=4。
python首先解释def f1()
的代码块,会记录属于这个代码块作用域内的变量i和n,但i和n都不会赋值,也就是说暂时并不知道变量n是一个函数变量。
同理,当需要解释def n()
代码块的时候,将记住这个代码块涉及到的变量i,只不过这个变量i是属于外层函数的,但不管如何,这个代码块记住了i,且记住了它是外部函数作用域的。
注意,函数的声明过程中,所有涉及到变量i的作用域内都不会对i进行赋值,仅仅只是保存了这个i变量名,只有在调用函数的时候才会进行赋值操作。
当开始调用f1()的时候,开始执行函数体中的代码,于是开始循环迭代,且多次声明函数n()
,每一次迭代生成的n()都会让原先已记录的变量n指向这个新声明的函数体(相当于赋值的操作,只不过是变量n引用的对象是函数体结构,而不是一般的数据对象),由于只是在循环中声明函数n(),并没有进行调用,所以不会对n()中的i进行赋值操作。而且,每次循环迭代都会让变量n指向新的函数体,使得先前迭代过程中定义的函数被丢弃(覆盖),所以最终只记住了最后一轮循环时声明的函数n(),并且i=4。
当调用f1()()时,表示调用f1()中返回的函数n(),直到这个时候才会对n()内的i进行赋值,赋值时将搜索它的外层函数f1()作用域,发现这个作用域内的i指向内存中的数值4,于是最终输出4。
再看下面的代码:
def f1():
for i in range(5):
def n():
print(i)
n()
return n
f1()
输出结果:
0
1
2
3
4
调用f1()的时候,执行循环的迭代,每次迭代时都会调用n(),意味着每次迭代都要对n()中的i进行赋值。
另外注意,前面说过,函数的默认参数是在函数声明时进行赋值的,所以下面的列表L中每个元素所代表的函数,它们的变量i都指向不同的数值对象。
def f1():
L = []
for i in range(5):
def n(i=i):
print(i)
L.append(n)
return L
f1()[0]()
f1()[1]()
f1()[2]()
f1()[3]()
f1()[4]()
执行结果:
0
1
2
3
4