Python进阶与拾遗1:Python中的模块
写在前面
在笔者推出的Python基础与拾遗9讲中,与大家分享了Python中基础的语法与注意事项,尤其讲述了Python语言与其他语言在使用上的区别。在阅读完毕Python基础与拾遗过后,应该就可以编写一些基础的Python程序,并且能够使用一些Python中独有的技巧了。但是Python语言是一门面向对象设计的语言,在几乎所有的大型Python工程中,均通过面向对象技术进行搭建。因此,Python进阶与拾遗部分就应运而生了,主要与大家分享Python中模块与面向对象程序设计的一些基础技巧,从最基本的Python模块开始,下面开始干货。
模块相关概念
模块定义
- 模块是最高级别的程序组织单元,封装程序代码与数据以便重用。
- 模块对应于Python文件,每一个文件就是一个模块,在一个模块中导入其他模块后,就可以使用导入模块定义的变量名。
- 模块可以由两个语句和一个重要的内置函数进行处理。
import # 以一个整体获取一个模块
from # 允许客户端从一个模块文件中获取特定的变量名
m.reload # 不终止Python程序的情况下,重新载入m模块文件
使用模块的好处
模块和类是重要的命名空间,在一个模块文件的顶层定义的所有变量名都成了被导入的模块对象属性,在导入时,模块全局作用域变成了模块对象的命名空间。
使用模块的好处有:
- 代码重用。模块文件可以永久地保存代码。
- 系统命名空间的划分。模块在Python中是最高级别的程序组织单元。模块将变量名封装进了自包含的软件包,避免了变量名冲突。
- 实现共享服务和数据。跨系统的共享组件,比如一个全局对象被一个以上的函数或文件使用,可以编写在一个模块中以便能够被多个客户端导入。
模块的设计理念
- 总是在Python的模块内编写代码。在交互式提示符下输入的程序代码,其实也是在内置模块__main__之内。
- 最小化模块耦合度。模块应尽可能地和其他模块的全局变量无关。
- 最大化模块的粘合性。模块的所有元素都享有共同的目的。
- 模块应该少去修改其他模块的变量。可是试着通过函数返回值传递这些结果。
Python的程序架构
- 一个Python程序里面包括了多个含有Python语句的文本文件。这些文件称为模块。
- 顶层文件包含了程序主要的执行流程,也可称为启动文件。顶层文件使用在模块文件中定义的工具,然后模块文件还可以使用在其他模块中定义的工具。
- Python自带了很多实用模块,称为标准链接库,大约有200个模块,包含与平台不相关的常见程序设计任务。比如操作系统接口(os),对象永久保存(pickle),文字模式匹配(re),网络和Internet脚本(socket),GUI建构(Tkinter)等。这些工具都不是Python语言的组成部分,但是可以在任何安装了标准python的情况下,导入并进行使用。
模块的常见使用
模块如何工作
程序在第一次导入指定文件时,会执行三个步骤:
- 搜索模块。import语句不能包含目录路径,只能包含模块的文件路径。因为Python使用标准模块搜索路径来找出import语句所对应的模块文件。
import module_a # 正确
import dir1/dir2/module_a # 错误
- 编译(可选,需要时进行)。在找到模块的源代码文件后,会将其编译成字节码pyc文件。Python会检查文件的时间戳,如果发现已有的字节码文件比源代码旧,就会重新生成字节码文件,否则直接跳过编译。如果只有字节码文件,没有源代码,就会直接加载字节码。
- 运行。运行指执行模块代码来创建所定义的对象,此过程中任何对变量名的赋值运算,都会产生所得到的模块文件属性。所以,必须保证代码的顺序合理。Python将载入的模块存储在一个名为sys.modules的列表中,在后续导入相同模块时,会跳过这三步,只提取内存中已加载的模块对象。
模块的搜索路径
import sys
sys.path # 打印模块的搜索路径
sys.path,支持打印。包含以下四类,顺序如下:
- 主目录
当你运行一个程序时,主目录就是执行文件所在的目录;当你在交互模式下工作时,主目录就是当前的工作目录。注意,主目录的文件也将覆盖路径上的其他目录中具有同样名称的模块。 - PYTHONPATH目录
PYTHONPATH环境变量设置中所罗列出的目录。体现在Linux中的bashrc文件,Windows中的环境变量。 - 标准库目录
- .pth文件目录
在Python 3.0及之后的版本中,允许把==.pth文件==添加到模块搜索路径中,里面逐行记录了模块的搜索路径。
如果要修改模块的搜索路径,可以直接修改sys.path列表,比如常见在sys.path中添加项:
import sys
sys.path.append('module_path') # 添加模块的搜索路径
导入模块时的执行细节
在import b时可能会加载:
- 源码文件b.py。
- 字节码文件b.pyc。
- 目录b,意思是导入一个包。
- 编译扩展模块(通常C或者C++编写),导入时使用动态链接(例如Linux的b.so,以及Cygwin和Windows的b.dll或b.pyd)。
- 用C编写的编译好的内置模块,并通过静态连接至Python。
- ZIP文件组件,导入时自动解压缩。
- 内存内映像,对于frozen可执行文件。
- Java类,在Jython版本的Python中。
- .NET组件,在IronPython版本的Python中。
import和from语句的使用
import
import使一个变量名引用整个模块对象,必须通过模块名称得到该模块的属性。
import numpy as np
np.pi # 3.141592653589793
from
- from把变量名复制到另一个作用域,这就让我们在脚本中直接使用赋值的变量名,无需通过模块。注意,如果使用from导入变量,而那些变量碰巧与作用域中现有变量同名,那么现有变量会被悄悄地覆盖掉。对命名空间有破坏性。
from random import shuffle
shuffle(list) # 打乱列表
from A import f
from B import f # B中的f覆盖掉了A中的f
- from *语句会取得模块顶层所有已赋值的变量名的拷贝。
from random import *
shuffle(list) # 打乱列表
randint(a, b) # 产生[a, b]范围内的随机整数
-
注意,在Python 2.6及之后的版本中,from … import *可以用在一个函数内。但是在Python 3.0及之后的版本中,from … import *只能出现在一个模块文件的顶部。
-
递归式的from导入可能会出错。
# recur1.py
X = 1
import recur2
Y = 2
# recur2.py
from recur1 import X
from recur1 import Y # 报错 ImportError: cannot import name 'Y' from partially initialized module 'recur1' (most likely due to a circular import)
import和from语句的as扩展
常用as语句,让模块可以在脚本中给予不同的变量名。
import numpy as np
from random import shuffle as sf
np.pi # 3.141592653589793
l = [1, 2, 3, 4, 5]
sf(l)
l # [5, 4, 2, 3, 1]
更改模块中的变量
- 在同一个工程(进程)中,更改了导入模块中的变量,再次导入模块,变量会变成改变后的值。因为在内存中,变量的值已经被覆盖。
- 在一个工程(进程)中,更改了导入模块中的变量,在另一个工程(进程)中导入模块,变量依旧维持原值,因为还是执行导入的标准流程,变量会重新赋值。
比如,在工程目录下,有一个mod_0.py,里面有两个变量:
# mod_0.py
a = 1
b = [2, 3, 4]
在第一个进程中:
import mod_0
mod_0.a # 1
mod_0.b # [2, 3, 4]
mod_0.a = 100
mod_0.b[0] = 200
import mod_0
mod_0.a # 100
mod_0.b # [200, 3, 4]
from mod_0 import a, b
a # 100
b # [200, 3, 4]
在第二个进程中:
import mod_0
mod_0.a # 1
mod_0.b # [2, 3, 4]
扩展功能
- 跨版本功能,如在Python 2.x中需使用Python 3.x的功能。
from __future__ import featurename
-
__name__属性。如果文件是以顶层程序文件执行,在启动时,__name__就会被设置为字符串"__main__"。如果文件被导入,__name__就会被设成客户端所了解的模块名。
-
用名称字符串导入模块,可以用exec函数或者__import__函数。
mod_name = "string"
exec("import " + mod_name)
string
<module 'string' from 'D:\\Anaconda3\\lib\\string.py'>
mod_name = "string"
string = __import__(mod_name)
string
<module 'string' from 'D:\\Anaconda3\\lib\\string.py'>
模块的命名空间
模块名生成命名空间
- 模块顶层的赋值语句会创建模块属性。模块文件顶层的def和=会建立模块的对象属性,赋值的变量名会存储在模块的命名空间内。
- 模块的命名空间可以通过属性__dict__或函数dir获取。__dict__将返回一个字典,dir返回列表。
比如,在工程目录下,有一个mod_0.py,里面有两个变量一个函数:
# mod_0.py
a = 1
b = [2, 3, 4]
def f():
pass
在一个进程中:
import mod_0
list(mod_0.__dict__.keys())
"""
输出:
['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'a', 'b', 'f']
"""
dir(mod_0)
"""
输出:
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b', 'f']
"""
- 模块是一个独立的作用域。当模块被导入后,模块文件的作用域就变成了模块对象的属性的命名空间。
属性名的点号运算
- 简单变量。直接在当前的作用域内搜索。
- 点号与多层点号运算。X.Y指在当前范围内搜索X,然后搜索对象X中的属性Y。X.Y.Z可推广。
- 通用性。点号运算可用于任何具有属性的对象:模块,类,C扩展类型等。
导入和作用域
模块程序代码绝对无法看到其他模块内的变量名,除非明确地进行了导入,全局变量亦如此。
比如,在工程目录下,有一个mod_0.py,里面内容是:
# mod_0.py
X = 88
def f():
global X
X = 99
在工程的main执行函数中:
import mod_0
def main():
X = 11
mod_0.f()
print(X, mod_0.X) # 11 99
if __name__ == "__main__":
main()
命名空间的嵌套
在被导入的模块中导入其他模块时,会创建命名空间的嵌套。
比如,在工程目录下,有一个mod_1.py,里面内容是:
# mod_3.py
X = 3
一个mod_2.py,里面内容是:
# mod_2.py
import mod_3
X = 2
一个mod_1.py,里面内容是:
# mod_1.py
import mod_2
X = 1
在工程的main执行函数中:
import mod_1
def main():
print(mod_1.X, mod_1.mod_2.X, mod_1.mod_2.mod_3.X) # 1 2 3
if __name__ == "__main__":
main()
在from *中隐藏变量
- 在变量前加_,表示隐藏变量。from *语句会复制出开头没有单下划线的变量名。
比如,在工程目录下,有一个mod_0.py,里面有三个变量:
# mod_0.py
a = 1
b = [2, 3, 4]
_c = {'a':5, 'b':6}
在工程执行main函数中:
from mod_0 import *
def main():
print(a) # 1
print(b) # [2, 3, 4]
# print(_c) # 报错 NameError: name '_c' is not defined
if __name__ == "__main__":
main()
- __all__通过列表赋值要复制的变量名。
比如,在工程目录下,有一个mod_0.py,里面有四个变量:
# mod_0.py
__all__ = ["a", "b"]
a = 1
b = [2, 3, 4]
c = {'a':5, 'b':6}
d = (7, 8, 9)
在工程执行main函数中:
from mod_0 import *
def main():
print(a) # 1
print(b) # [2, 3, 4]
# print(c) # 报错 NameError: name 'c' is not defined
# print(d) # 报错 NameError: name 'd' is not defined
if __name__ == "__main__":
main()
重载模块
reload函数会强制已加载的模块代码重新载入并执行,传递给reload的是已经存在的模块对象。在Python 2.6及之后的版本中,reload是内置函数。在Python 3.0及之后的版本中,reload函数已经被移入imp标准库模块,在使用时需要导入。
- reload会在模块当前的命名空间内执行模块文件的新代码。
- 文件中顶层赋值语句会使得变量名换成新值。
- 重载会影响所有使用import读取了模块的客户端。重载后,模块中的对象变成了新的值。
- 重载只会对重载以后使用from的客户端造成影响。之前使用from来读取属性的客户端并不会受到重载的影响,因为from在执行时是复制变量名。
比如,在工程目录下,有一个mod_0.py,里面有两个变量:
# mod_0.py
a = 1
b = [2, 3, 4]
在一个进程中:
import mod_0
mod_0.a # 1
mod_0.b # [2, 3, 4]
mod_0.a = 100
mod_0.b[0] = 200
mod_0.a # 100
mod_0.b # [200, 3, 4]
from importlib import reload
reload(mod_0)
mod_0.a # 1
mod_0.b # [2, 3, 4]
在另一个进程中:
from mod_0 import a, b
a # 1
b # [2, 3, 4]
a = 100
b[0] = 200
a # 100
b # [200, 3, 4]
from importlib import reload
reload(mod_0)
a # 100
b # [200, 3, 4]
以上,欢迎各位读者朋友提出意见或建议。
欢迎阅读笔者后续博客,各位读者朋友的支持与鼓励是我最大的动力!
written by jiong
万缕千丝终不改,任他随聚随分。
韶华休笑本无根。
好风凭借力,送我上青云。