百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

Python之模块和包:模块导入对命名空间的影响

off999 2024-11-26 07:19 17 浏览 0 评论

引言

前面文章中,关于Python解释器在模块导入行为背后所执行的操作,已经做了深入的介绍。本文打算在此基础上,结合实际代码案例,进行进一步的补充说明。同时,比较看似只是微小的导入方式的改变,可能会导致的难以理解的异常问题。

本文的主要内容有:

1、关于命名空间

2、模块导入对命名空间的影响


关于命名空间

有关于命名空间的介绍,前面已经很详细了,对此仍然不太熟悉的,可以翻看一下上一篇文章。这里,主要想就在Python中如何查看、操作命名空间做进一步的说明。

命名空间的作用,主要是用于进行代码中“名字”与对象之间绑定关系的存储。

所谓执行模型,更像是代码在命名空间上执行,从而实现对Python中对象模型的访问及修改。

所以,代码执行过程中,对名称的解析、对象的定位,都是基于命名空间来实现的。

在Python中我们可以通过几个内置函数查看命名空间的相关内容,前面已经简单提及,这里再回顾一下:

1、globals():查看当前全局命名空间的字典

2、locals():查看当前局部命名空间的字典

3、dir():返回当前代码所在范围的所有名字列表

可以先看下这几个内置函数的文档说明:

接下来,我们通过实际代码来看下,这几个内置函数的使用。

首先看代码执行前后全局命名空间的变化,直接看代码:

from rich.pretty import pprint
print("初始的全局命名空间:")
pprint(locals())
a = 10
b = 20


def add(n1, n2):
    return n1 + n2


print("定义了变量和函数后的全局命名空间:")
pprint(locals())

执行结果如下:

从执行结果可以看到,我们在代码中定义的变量和函数,都会把名称和对应的对象存储到全局命名空间的字典中。

需要说明的是,当我们在顶级代码块中,使用locals()函数时,返回的命名空间与globals()返回的是一样的,也就是都是该模块的全局命名空间。

感兴趣的,可以自行把代码中的globals()切换为locals()。


当locals()函数在函数体内进行调用时,则能看到真实的函数内部的局部命名空间中的名称及对象的绑定关系:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    pprint(locals())
    return a + n1 + n2


pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

执行结果:

从执行结果中可以看出,虽然add()函数的局部命名空间中只有n1和n2两个名称,但是执行到return a + n1 + n2时,并不会报错,且成功返回了我们期望的10 + 5 + 10 = 25的结果。

所以,当函数调用时,会创建局部命名空间。函数体中的代码会在局部命名空间上执行,如果在名称解析的过程中,如果遇到局部命名空间中不存在的名称,则会去更高层级的命名空间去查找,查找顺序遵循LEGB规则。


如果我们稍微调整一下add()函数体的代码,尝试修改全局变量a的取值:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    a = 1000
    pprint(locals())
    return a + n1 + n2


pprint(globals())
add(10, 20)
add(5, 10)
pprint(globals())

执行结果:

从代码中可以看出,我在顶级代码块中定义了一个全局变量a,在函数add中尝试修改全局变量a,其实并没有成功。

因为add函数体中,a = 1000这行代码,默认是在局部命名空间中执行,所以会在局部命名空间中创建一个新的名称对象绑定关系。所以,对比前面一个程序的运行结果,局部命名空间中多了一个a局部变量的名称对象绑定。

虽然全局命名空间中也有同名的变量a,但是在函数的区局作用域中,会优先查找局部命名空间来进行名称解析。

现在陷入了一个尴尬的点,我们想要修改全局变量,却导致了在局部命名空间中新增了一个局部变量。

其实有两种做法可以实现在局部作用域(也就是函数体中),对全局变量的修改。

先来看比较粗暴的做法,既然我们的意图是修改全局变量,而全局变量的名称对象绑定是存储在全局命名空间中,那么我们直接尝试修改全局命名空间中的字典,是否就实现了对全局变量的修改呢?

可以通过如下代码来验证:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    globals()['a'] = 1000
    pprint(locals())
    return a + n1 + n2


pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

我们将add()函数体中,a = 1000的代码,变换为:globals()['a'] = 1000。

看下执行结果:

从执行结果看出,这种做法,确实直接修改了全局命名空间中a变量,而且没有在局部命名空间中,引入新的局部变量,从而实现了我们的目的。

我们也可以不通过定义变量,直接通过这种操作全局命名空间字典的方式,引入新的变量,感兴趣的同学,可以自行尝试。

之所以说,这种做法比较粗鲁,一方面是因为有点繁琐,另外一方面也不太安全,稍有不慎,会影响后续代码中的命名解析。

其实,Python中有更简单的做法,就是通过global关键字,来声明变量为全局变量,代码如下:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    print(dir())
    global a
    a = 1000
    pprint(locals())
    return a + n1 + n2


pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

执行结果,跟粗鲁的做法是完全相同的,这里就不再贴出来了。

此外,dir()函数,不传参时,能直接获取当前作用域中可见的名称列表,这里就不再演示了。


模块导入对命名空间的影响

接下来看一下,模块导入会对命名空间产生怎样的影响。

首先,我们定义一个m1.py的模块,代码如下:

a = 100


def add(n1, n2):
    print("在m1的add函数中")
    print(locals())
    return a + n1 + n2


print("在m1模块中")
print(dir())

然后,我们在入口文件中导入该模块,代码如下:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    print("在入口文件的add函数中")
    pprint(locals())
    return a + n1 + n2


pprint(globals())
import m1

c = add(5, 10)
d = m1.add(20, 10)
m1.a = 1000
e = m1.add(20, 10)
pprint(globals())



执行结果如下:

可以看到,通过import m1的方式导入模块,在全局命名空间中只增加了一个m1的模块名与模块对象的绑定关系。虽然我们在模块m1和入口文件中,有同名的全局变量a和add()函数,但是模块中的是通过m1.的前缀访问的。所以,可以各自访问,并不会有冲突或者覆盖的情况。


但是,如果导入方式调整为 from m1 import a, add

代码如下:

from rich.pretty import pprint

a = 10


def add(n1, n2):
    print("在入口文件的add函数中")
    pprint(locals())
    return a + n1 + n2


pprint(globals())
from m1 import a, add

d = add(20, 10)
a = 1000
e = add(20, 10)
pprint(globals())


执行结果:

从执行结果中,可以看出,通过from m1 import a, add的方式进行导入时,全局命名空间中原有的名称a和add的绑定关系发生了变化。也就是入口文件中定义的变量a和函数add()已经被覆盖。

同时会发现,不同于通过m1.a = 1000进行模块m1中全局变量的修改,这里m1中的变量a直接被加入到了入口文件的全局命名空间,所以,a = 1000,并没有影响函数add()的计算结果,因为没有真正修改add()函数中引用的m1.a变量的取值。这点,是需要格外注意的。


总结

本文首先介绍了命名空间相关的补充内容,然后演示了模块导入对命名空间的影响。需要注意的有这几点:

1、当在顶级代码块或者全局作用域中,globals()和locals()获取的命名空间字典都是相同的,都是该模块的全局命名空间。

2、在函数体内或者局部作用域中,globals()是全局命名空间,locals()则是真正的局部命名空间,只有函数的形参及函数体内定义的变量。

3、要在局部作用域中修改全局变量,可以通过globals()['变量名'] 的方式进行修改,也可以通过global关键字的方式进行修改,推进使用后一种方法。

4、import 模块名的方式,只会在全局命名空间中添加模块名与模块对象的名称绑定关系,所以,不会导致命名冲突或者相互覆盖的情况。

5、from 模块名 import xxx的导入方式,会导致模块中具体的变量名、函数名等添加到全局命名空间中,如果已经存在相同的名称,则会覆盖。最终的结果是,最后一次导入的真正生效,之前的都会被覆盖掉。此外,当模块中的函数需要访问模块中的全局变量时,此种导入方式,会导致无法修改模块全局变量的情况,需要特别注意!


感谢您的拨冗阅读,如果对您学习Python有所帮助,欢迎点赞、关注。

相关推荐

让 Python 代码飙升330倍:从入门到精通的四种性能优化实践

花下猫语:性能优化是每个程序员的必修课,但你是否想过,除了更换算法,还有哪些“大招”?这篇文章堪称典范,它将一个普通的函数,通过四套组合拳,硬生生把性能提升了330倍!作者不仅展示了“术”,更传授...

7 段不到 50 行的 Python 脚本,解决 7 个真实麻烦:代码、场景与可复制

“本文整理自开发者AbdurRahman在Stackademic的真实记录,所有代码均经过最小化删减,确保在50行内即可运行。每段脚本都对应一个日常场景,拿来即用,无需额外依赖。一、在朋...

Python3.14:终于摆脱了GIL的限制

前言Python中最遭人诟病的设计之一就是GIL。GIL(全局解释器锁)是CPython的一个互斥锁,确保任何时刻只有一个线程可以执行Python字节码,这样可以避免多个线程同时操作内部数据结...

Python Web开发实战:3小时从零搭建个人博客

一、为什么选Python做Web开发?Python在Web领域的优势很突出:o开发快:Django、Flask这些框架把常用功能都封装好了,不用重复写代码,能快速把想法变成能用的产品o需求多:行业...

图解Python编程:从入门到精通系列教程(附全套速查表)

引言本系列教程展开讲解Python编程语言,Python是一门开源免费、通用型的脚本编程语言,它上手简单,功能强大,它也是互联网最热门的编程语言之一。Python生态丰富,库(模块)极其丰富,这使...

Python 并发编程实战:从基础到实战应用

并发编程是提升Python程序效率的关键技能,尤其在处理多任务场景时作用显著。本文将系统介绍Python中主流的并发实现方式,帮助你根据场景选择最优方案。一、多线程编程(threading)核...

吴恩达亲自授课,适合初学者的Python编程课程上线

吴恩达教授开新课了,还是亲自授课!今天,人工智能著名学者、斯坦福大学教授吴恩达在社交平台X上发帖介绍了一门新课程——AIPythonforBeginners,旨在从头开始讲授Python...

Python GUI 编程:tkinter 初学者入门指南——Ttk 小部件

在本文中,将介绍Tkinter.ttk主题小部件,是常规Tkinter小部件的升级版本。Tkinter有两种小部件:经典小部件、主题小部件。Tkinter于1991年推出了经典小部件,...

Python turtle模块编程实践教程

一、模块概述与核心概念1.1turtle模块简介定义:turtle是Python标准库中的2D绘图模块,基于Logo语言的海龟绘图理念实现。核心原理:坐标系系统:原点(0,0)位于画布中心X轴:向右...

Python 中的asyncio 编程入门示例-1

Python的asyncio库是用于编写并发代码的,它使用async/await语法。它为编写异步程序提供了基础,通过非阻塞调用高效处理I/O密集型操作,适用于涉及网络连接、文件I/O...

30天学会Python,开启编程新世界

在当今这个数字化无处不在的时代,Python凭借其精炼的语法架构、卓越的性能以及多元化的应用领域,稳坐编程语言排行榜的前列。无论是投身于数据分析、人工智能的探索,还是Web开发的构建,亦或是自动化办公...

Python基础知识(IO编程)

1.文件读写读写文件是Python语言最常见的IO操作。通过数据盘读写文件的功能都是由操作系统提供的,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个...

Python零基础到精通,这8个入门技巧让你少走弯路,7天速通编程!

Python学习就像玩积木,从最基础的块开始,一步步搭建出复杂的作品。我记得刚开始学Python时也是一头雾水,走了不少弯路。现在回头看,其实掌握几个核心概念,就能快速入门这门编程语言。来聊聊怎么用最...

一文带你了解Python Socket 编程

大家好,我是皮皮。前言Socket又称为套接字,它是所有网络通信的基础。网络通信其实就是进程间的通信,Socket主要是使用IP地址,协议,端口号来标识一个进程。端口号的范围为0~65535(用户端口...

Python-面向对象编程入门

面向对象编程是一种非常流行的编程范式(programmingparadigm),所谓编程范式就是程序设计的方法论,简单的说就是程序员对程序的认知和理解以及他们编写代码的方式。类和对象面向对象编程:把...

取消回复欢迎 发表评论: