python:命名空间学习

目录

 

一、一个bug引发的思考

二、修正BUG

三、示例总结

1.全局变量、局部变量和global关键字

2、闭包和nonlocal关键字

(1)用global解决

(2)用闭包解决

(3)在闭包中使用全局变量:nonlocal

3.拓展:import和写操作的相通之处


一、一个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,后续学完基础再来完善。

加油:)

发布了159 篇原创文章 · 获赞 41 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_37768971/article/details/103374481