目录
一、一个bug引发的思考
最近在学习python小项目,其中一个模块是有关时间的,就设计了一个辅助类叫做MyTimer
代码如下:(简略版)
import os
import time
class MyTimer():
# 私有方法:把时间戳转化为时间:
def __timestamp_to_time(self,timestamp):
timeStruct = time.localtime(timestamp)
return time.strftime("%Y-%m-%d %H_%M_%S",timeStruct)
# 获得文件创建时间
def get_file_createtime(self,filePath):
t = os.path.getctime(filePath)
create_time = self.__timestamp_to_time(t)
return create_time
# 测试函数
time = MyTimer()
filepath = 'G:\\Users\\IMUHERO\\old\\a.txt'
print(time1.get_file_createtime(filepath))
使用测试函数测试功能时一直报错,说没有找到 localtime
Traceback (most recent call last):
File ".\timer.py", line 67, in <module>
print(time.get_file_createtime(filepath))
File ".\timer.py", line 60, in get_file_createtime
create_time = self.__timestamp_to_time(t)
File ".\timer.py", line 54, in __timestamp_to_time
timeStruct = time.localtime(timestamp)
AttributeError: 'MyTimer' object has no attribute 'localtime'
可是在time这个模块里面确实有localtime这个方法呀,到底咋回事呢?
二、修正BUG
认真检查一下才发现,自己犯了一个低级错误,把实例化对象的命名和导入的包重复了。
这样就使得导入的time,被我自己命名的time对象覆盖了,我自己实例化的对象当然没有localtime啦,因为内部我还要调用外部time模块来实现功能呢。
把这个bug解决后也引发了我自己的思考,python的命名空间规则是怎么样的呢?全局变量,局部变量的作用范围和java有什么不同呢?读和写在python的编译期会有什么不同?
三、示例总结
下面通过几个小例子总结一下:
1.全局变量、局部变量和global关键字
范例一:
name = "test"
def Test():
print (name)
def Test1():
name = "test1"
print (name)
def Test2():
name += "1"
print (name)
Test()
Test1()
Test2()
运行这段代码的结果是什么?
test
test1
Traceback (most recent call last):
File ".\test.py", line 23, in <module>
Test2()
File ".\test.py", line 19, in Test2
name += "1"
UnboundLocalError: local variable 'name' referenced before assignment
可以看到Test()和Test1()都能正常执行
-》其中Test()函数内是没有name这个变量的,那他为什么能够执行呢?
这里的原因是:Python 的查找顺序为:局部的命名空间去 -> 全局命名空间 -> 内置命名空间
函数中定义的是局部空间,局部空间没有就去全局空间找,然后找到并打印。
-》 Test1()也是一样的道理,在局部空间我们定义了name,所以直接打印出来就结束了。
基础知识补充:
一般有三种命名空间:
- 内置名称(built-in names), Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。
- 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
- 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)
-》那么问题来了,为什么在Test2()中会报错:本地变量"name"未定义呢?
这里就涉及到python在编译期对读和写的不同操作,在Test2()中新增加了name += "1"
按道理说,局部变量没找到,应该去全局变量读取并且做修改,但是这里却没有。
我个人的理解是,一般情况下写操作是不安全的,对全局变量进行修改可能导致不可预料的结果,所以Python不允许。
-》那么我们怎么样才能对全局变量进行修改呢?
这里一个比较常用的方法是 global 关键字
这里我们顺便打印一下全局变量看一下结果:
test
test1
test2
test2
这次没有报错了,name += "2"得出了正确结果
我们可以注意到,全局变量name也发生了改变,变成了修改后的test2,说明global关键字可以对全局变量进行修改。
继续拓展:如果我们想要在函数内部获取全局变量来做操作,并且不希望改变全局变量的值怎么办?
2、闭包和nonlocal关键字
再举一个例子
假设一个人初始走0步,第一次走2步,第二次走3步,第三次走5步,那么计算他每一次走完的总步数。
看到这个问题大多数同学会想到使用 global 关键字
(1)用global解决
示例代码:
origin = 0
def factory(step):
global origin
origin += step
print(origin)
factory(2)
factory(3)
factory(5)
可以得出正确答案:
2
5
10
但是我们的origin也发生了改变,我们命名叫做origin就是起始值的意思,起始值怎么能够一直变呢。
还有没有更好的办法,不去修改起始值?
(2)用闭包解决
要理解闭包首先要理解python的独特之处
俗话说:Python一切皆对象
比较好的证明就是,python可以return一个函数,比如:
而闭包,就是函数+环境变量
闭包 = 函数+环境变量
下面的这个例子就是闭包,在函数里面嵌套函数,return的是内部函数。
def curve_pre():
a = 25
def curve(x):
return a*x*x
return curve
a = 10
f = curve_pre()
print (f(2))
结果
100
最终打印的结果是100.
说明,在闭包中调用f(2),使用的是包内存储的局部变量a =25,这个变量也将存储在闭包的环境变量中
##############################################################
如何证明:
闭包 = 函数+环境变量
闭包的环境变量保存在f.__closur__,我们将其打印出来看看
在刚刚的模块中添加下面的代码:
print(f.__closure__[0].cell_contents)
最终打印的结果为:
25
这就说明,闭包的环境变量是用来暂存我们的变量中间值的。
(3)在闭包中使用全局变量:nonlocal
说到这里我们的问题还是没有解决,因为只使用了局部变量。如何使用全局变量进行操作而不修改全局变量呢?
有了上面的基础概念,后面就容易理解了,回到刚刚走路的那道题。
origin = 0
def factory(ori):
def go(step):
new_step = ori + step
ori = new_step
return new_step
return go
tourist = factory(origin)
print(tourist(2))
print(tourist(3))
print(tourist(5))
在上面的代码中,我们尝试使用闭包来解决问题。
我们将全局变量作为参数传递进函数factory中,获得go函数,命名为tourist
并且打印走2/3/5步后的结果,运行结果是:
Traceback (most recent call last):
File ".\closePacket.py", line 37, in <module>
print(tourist(2))
File ".\closePacket.py", line 31, in go
new_step = ori + step
UnboundLocalError: local variable 'ori' referenced before assignment
为什么会报错呢?编译期提示我们,‘ori’在赋值前就被引用了,说明我们没有提前定义ori
但是我们是通过参数传递,目的就是直接使用factory的形参。怎么办呢?
python给出的解决方案是使用 nonlocal 关键字
顾名思义,这个关键字向编译期表明,ori不是本地局部变量,而是可以直接使用的环境变量。
修改后的代码:
origin = 0
def factory(ori):
def go(step):
nonlocal ori
new_step = ori + step
ori = new_step
return new_step
return go
tourist = factory(origin)
print(tourist(2))
print(tourist(3))
print(tourist(5))
print(origin)
打印结果为:
2
5
10
0
说明程序运行成功,最后一行打印了origin
可以证明我们的全局变量origin并没有被改变。
思考:
既然origin没有被改变,为什么2,5,10能够被暂存下来呢,在后续的运算是怎么累加的呢?
这关系到环境变量了,我们在每次运行之后打印一下环境变量:
print(tourist(2))
print(tourist.__closure__[0].cell_contents)
print(tourist(3))
print(tourist.__closure__[0].cell_contents)
print(tourist(5))
print(tourist.__closure__[0].cell_contents)
print(origin)
运行结果为:
2
2
5
5
10
10
0
说明了,负责暂存的是我们的环境变量,每一次改变都被存储起来以便下次操作时取用。
这也证明了 闭包 = 函数+环境变量
3.拓展:import和写操作的相通之处
在最开始的示例代码中:
name = "test"
def Test():
print (name)
def Test1():
name = "test1"
print (name)
def Test2():
name += "1"
print (name)
Test()
Test1()
Test2()
我们发现Test2()的代码运行报错,因为他进行了写操作,Python会自动检测是否存在该局部变量可供操作。
试一下下面这段代码:
import math
def Test1():
print (math.pi)
def Test2():
print (math.pi)
import math
Test1()
Test2()
运行结果为:
3.141592653589793
Traceback (most recent call last):
File ".\test3.py", line 21, in <module>
Test2()
File ".\test3.py", line 17, in Test2
print (math.pi)
UnboundLocalError: local variable 'math' referenced before assignment
说明,Test1()运行成功
而Test2()运行失败,因为math这个局部变量不存在?
看一下差别:只是在Test2()中添加了import math的代码
是不是和刚刚只是添加一行 name += "1" 特别像。
我的思考:
python对读和写的操作是不一样的,
对于读操作,默认应该是安全的,可以从局部变量->全局变量->内部变量依次查找;
对于写操作,默认应该是不安全,如果局部变量没有定义,就需要使用global关键字或者nonlocal关键字才能向上一层修改;
修改和import属于写操作的范畴,所以会报错。
更深的思考:
经常说Python的运行时从上到下的,可是在这里我们发现下一行的代码可能会影响前一行代码的检测。
Python分为编译期和运行期,在编译期会编译成pyc或者pyo转换成字节码,会有编译检查。比如刚刚import相关的代码,放在vscode中,还没运行就已经报错了,说明是编译报错,不是运行报错。
Python并不是真正的脚本,因为他有编译。
TODO:
在闭包中,我们的全局变量可以不加修改的使用它,并使用环境变量暂存中间值,以此来完成运算。
那么闭包中的全局变量是否可以改变呢?
实际上也是可以改的,不能改的原因是闭包外面那个变量固定了他的内存位置,不允许改而已。
但是如果用一个list照样可以改,这就涉及到了Python的可变和不可变,变长和不变长。
-》由于内容比较深入,暂时记录一个TODO,后续学完基础再来完善。
加油:)