当前位置: 首页 > news >正文

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

引言

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

本文的主要内容有:

1、关于命名空间

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

关于命名空间

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

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

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

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

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

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

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

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

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

607a8868f3000a8945835e2f92c4152e.jpeg

7d9c7346c271635629be590aec0d2506.jpeg

da2be96b415dda1dd85f56e035508f3e.jpeg

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

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

from rich.pretty import pprint
print("初始的全局命名空间:")
pprint(locals())
a = 10
b = 20def add(n1, n2):return n1 + n2print("定义了变量和函数后的全局命名空间:")
pprint(locals())

执行结果如下:

b62d35fe7a85dbc3512c3b193497df25.jpeg

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

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

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

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

from rich.pretty import pprinta = 10def add(n1, n2):pprint(locals())return a + n1 + n2pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

执行结果:

1bc18de926de9169151f87660bc98157.jpeg

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

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

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

from rich.pretty import pprinta = 10def add(n1, n2):a = 1000pprint(locals())return a + n1 + n2pprint(globals())
add(10, 20)
add(5, 10)
pprint(globals())

执行结果:

0eec43441ab0fd349d2682c915ccf144.jpeg

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

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

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

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

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

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

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

from rich.pretty import pprinta = 10def add(n1, n2):globals()['a'] = 1000pprint(locals())return a + n1 + n2pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

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

看下执行结果:

488658db0ae08f981ea785a8a4764d55.jpeg

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

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

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

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

from rich.pretty import pprinta = 10def add(n1, n2):print(dir())global aa = 1000pprint(locals())return a + n1 + n2pprint(globals())
add(10, 20)
c = add(5, 10)
pprint(globals())

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

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

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

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

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

a = 100def add(n1, n2):print("在m1的add函数中")print(locals())return a + n1 + n2print("在m1模块中")
print(dir())

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

from rich.pretty import pprinta = 10def add(n1, n2):print("在入口文件的add函数中")pprint(locals())return a + n1 + n2pprint(globals())
import m1c = add(5, 10)
d = m1.add(20, 10)
m1.a = 1000
e = m1.add(20, 10)
pprint(globals())

执行结果如下:

70e7f70ca49b97de733f3e24b2073a47.jpeg

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

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

代码如下:

from rich.pretty import pprinta = 10def add(n1, n2):print("在入口文件的add函数中")pprint(locals())return a + n1 + n2pprint(globals())
from m1 import a, addd = add(20, 10)
a = 1000
e = add(20, 10)
pprint(globals())

执行结果:

a144c11fbf9233a66d8edcc02122ffb9.jpeg

从执行结果中,可以看出,通过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有所帮助,欢迎点赞、关注。

ac169efeca159192bf68361bd2c20e2c.jpeg

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • MessageDialog 是 Qt Quick Controls 中的一个组件,用于显示消息对话框
  • 解锁C#性能监控:内置性能计数器全解析
  • 结构型模式之代理模式
  • Python习题 148:返回每个单词长度的列表
  • K8s之自动扩缩容
  • 【Python脚本】爬取网络小说
  • 谷歌、火狐及Edge等浏览器中实现allWebPlugin中间件自动安装及升级
  • docker基本环境搭建
  • 火语言RPA流程组件介绍--提取应用内嵌资源
  • java操作zookeeper
  • 【原创】java+swing+mysql健身房管理系统设计与实现
  • Adobe After Effects的插件--------CC Ball Action
  • CMake编译指令极简说明
  • Redis计数器:数字的秘密
  • vue项目关于ERR_OSSL_EVP_UNSUPPORTED的问题
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • 【Amaple教程】5. 插件
  • 8年软件测试工程师感悟——写给还在迷茫中的朋友
  • bearychat的java client
  • HTTP--网络协议分层,http历史(二)
  • JAVA并发编程--1.基础概念
  • Java到底能干嘛?
  • Java基本数据类型之Number
  • k8s如何管理Pod
  • XForms - 更强大的Form
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 复杂数据处理
  • 构建工具 - 收藏集 - 掘金
  • 前端_面试
  • 如何胜任知名企业的商业数据分析师?
  • 学习Vue.js的五个小例子
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • ​1:1公有云能力整体输出,腾讯云“七剑”下云端
  • #{} 和 ${}区别
  • #如何使用 Qt 5.6 在 Android 上启用 NFC
  • (3) cmake编译多个cpp文件
  • (52)只出现一次的数字III
  • (附源码)spring boot基于小程序酒店疫情系统 毕业设计 091931
  • (附源码)springboot青少年公共卫生教育平台 毕业设计 643214
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验
  • (一)VirtualBox安装增强功能
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • (转)我也是一只IT小小鸟
  • .jks文件(JAVA KeyStore)
  • .NET C# 操作Neo4j图数据库
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .net core 管理用户机密
  • .Net Core 中间件验签
  • .net core开源商城系统源码,支持可视化布局小程序
  • .Net 高效开发之不可错过的实用工具
  • .Net 基于.Net8开发的一个Asp.Net Core Webapi小型易用框架
  • .NET设计模式(8):适配器模式(Adapter Pattern)
  • .net中的Queue和Stack
  • ?