Python的模块和使用

什么是模块

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在 Python 中,常规情况下一个模块就是一个包含了 Python 定义和声明的文件,文件名就是模块名字加上 .py 的后缀。

但事实上 import 加载的模块分为四个通用类别: 

1、使用 Python 编写的代码(.py 文件)。
2、已被编译为共享库或 DLL 的 C 或 C++ 扩展。
3、包好一组模块的包。
4、使用C编写并链接到 Python 解释器的内置模块。

为什么要使用模块

如果你退出 Python 解释器然后重新进入,之前定义的函数或者变量都将丢失,因此我们通常将程序写到文件中以便永久保存下来,需要时就通过 python test.py 方式去执行,此时 test.py 被称为脚本(script)。

随着程序的开发和补充,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做可以使程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用。我们在编写程序的时候,也经常引用其他模块,包括 Python 内置的模块和来自第三方的模块。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。

如何使用模块

示例文件:文件名 hello.py,模块名 hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# hello.py

print('from the hello.py')

money = 1000


def read1():
print('hello->read1->money', 1000)


def read2():
print('hello->read2 calling read1')
read1()


def change():
global money
money = 0

import

模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次遇到导入 import 语句时才执行。

import 语句是可以在程序中的任意位置使用的,且针对同一个模块能 import 多次,为了防止重复导入,Python 的优化手段是:第一次导入之后就将模块名加载到内存,后续的 import 语句仅是对已经加载到内存中的模块对象增加了一次引用,不会重新执行模块内的语句,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# test.py

import hello
# 只在第一次导入时才执行 hello.py 内的代码,此处的显式效果是只打印一次 'from the hello.py'
# 当然其他的顶级代码也都被执行了,只不过没有显示效果。
import hello
import hello
import hello
import hello

'''
执行结果:
from the hello.py
'''

我们可以从 sys.modules 中找到当前已经加载的模块,sys.modules 是一个字典,内部包含模块名与模块对象的映射,该字典决定了导入模块时是否需要重新导入。

每个模块都是一个独立的名称空间,定义在这个模块中的函数,把这个模块的名称空间当作全局名称空间,这样我们在编写自己的模块时,就不用担心我们定义在自己模块中全局变量会在被导入时,与使用者的全局变量冲突。

测试一,moneyhello.money 不冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# test.py

import hello

money = 10
print(hello.money)

'''
执行结果:
from the hello.py
1000
'''

测试二,read1hello.read1 不冲突:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# test.py

import hello


def read1():
print('========')


hello.read1()

'''
执行结果:
from the hello.py
hello->read1->money 1000
'''

测试三,执行 hello.change() 操作的全局变量 money 仍然是 hello.py 中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# test.py

import hello

money = 1
hello.change()
print(money)

'''
执行结果:
from the hello.py
1
'''

总结:

首次导入模块 hello 时会做三件事:

1、为源文件 hello.py 创建新的名称空间,在 hello 中定义的函数和方法若是使用到了 global 时访问的就是这个命名空间。

2、在新创建的命名空间中执行模块中包含的代码。

3、创建名字 hello 来引用该命名空间。

import … as …

使用 as 可以为模块起别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# test.py

import hello as hlo

print(hlo.money)

'''
执行结果:
from the hello.py
1000
'''

为已经导入的模块起别名的方式对编写可扩展的代码很有用,假设有两个模块 xmlreader.pycsvreader.py,它们都定义了函数 read_data(filename) 用来从文件中读取一些数据,但采用不同的输入格式。可以编写代码来选择性地挑选读取模块,例如:

1
2
3
4
5
if file_format == 'xml':
import xmlreader as reader
elif file_format == 'csv':
import csvreader as reader
data = reader.read_data(filename)

在一行导入多个模块也是可行的:

1
import sys, os, re

但从维护性上考虑,不建议使用这种导入方法。

from … import …

对比 import hello,会将源文件的名称空间 hello ‘带到当前名称空间中,使用时必须是 hello. 名字的方式。而 from 语句相当于 import,也会创建新的名称空间,但是将 hello 中的名字直接导入到当前的名称空间中,在当前名称空间中直接使用名字就可以了。

1
from hello import read1,read2

这样在当前位置直接使用 read1read2 就好了,执行时仍然以 hello.py 文件作为全局名称空间。

测试一,导入的函数 read1,执行时仍然回到 hello.py 中寻找全局变量 money

1
2
3
4
5
6
7
8
9
# test.py
from hello import read1
money=1000
read1()
'''
执行结果:
from the hello.py
hello->read1->money 1000
'''

测试二,导入的函数 read2,执行时需要调用 read1(),仍然回到 hello.py 中找 read1()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# test.py
from hello import read2


def read1():
print('==========')


read2()

'''
执行结果:
from the hello.py
hello->read2 calling read1
hello->read1->money 1000
'''

如果当前有重名 read1 或者 read2,那么会有覆盖效果。

测试三,导入的函数 read1,被当前位置定义的 read1 覆盖掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# test.py
from hello import read1


def read1():
print('==========')


read1()
'''
执行结果:
from the hello.py
==========
'''

需要特别强调的一点是:Python 中的变量赋值不是一种存储操作,而只是一种绑定关系,如下:

1
2
3
4
5
6
7
8
9
10
11
from hello import money, read1

money = 100 # 将当前位置的名字 money 绑定到了100
print(money) # 打印当前的名字
read1() # 读取 hello.py 中的名字 money,仍然为1000

'''
from the hello.py
100
hello->read1->money 1000
'''

支持 as

1
from hello import read1 as read

也支持导入多行

1
2
3
from hello import (money,
read1,
read2)

from … import *

from hello import * 会把 hello 中所有的不是以下划线(_)开头的名字都导入到当前位置,大部分情况下我们的 Python 程序不应该使用这种导入方式,因为 * 不知道你想要导入什么名字,很有可能会覆盖掉之前已经定义的名字。而且可读性极其差,不过,在交互式编译器中为了节省打字可以这么用。

可以使用 __all__ 来控制 *(多用来发布新版本)。在 hello.py 中新增一行:

1
__all__ = ['money', 'read1']

这样在另外一个文件中用 from hello import * 就只能导入列表中规定的两个名字。

考虑到性能的原因,每个模块只被导入一次,放入字典 sys.modules 中,如果改变了模块的内容就必须重启程序,Python 不支持重新加载或卸载之前导入的模块。

你可能会想到直接从 sys.modules 中删除一个模块不就可以卸载了吗?请注意,当删除了 sys.modules 中的模块对象,仍然可能被其他程序的组件所引用,因而不会被清除。

特别是对于我们引用了这个模块中的一个类,用这个类产生了很多对象,因而这些对象都有关于这个模块的引用。如果只是交互测试一个模块,使用 importlib.reload(), 比如 import importlib; importlib.reload(modulename),这只能用于测试环境。

把模块当做脚本执行

我们可以通过模块的全局变量 __name__来查看模块名:

  • 当做脚本运行时,__name__ 等于 '__main__'
  • 当做模块导入时,__name__ 等于 模块名。

作用:用来控制 .py 文件在不同的应用场景下执行不同的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def fib(n):
pre, curr = 0, 1
while curr < n:
print(curr, end=' ')
pre, curr = curr, pre + curr
print()


if __name__ == "__main__":
print(__name__)
num = input('num :')
fib(int(num))

if __name__ == "__main__": 的意思是:当 .py 文件被直接运行时,if __name__ == '__main__' 之下的代码块将被运行;当 .py 文件以模块形式被导入时,if __name__ == '__main__' 之下的代码块不被运行。

模块搜索路径

Python 解释器在启动时会自动加载一些模块,可以使用 sys.modules查看。在第一次导入某个模块时(比如 hello),会先检查该模块是否已经被加载到内存中(当前执行文件的名称空间对应的内存),如果有则直接引用。如果没有,解释器则会查找同名的内建模块,如果还没有找到就从 sys.path 给出的目录列表中依次寻找 hello.py 文件。

所以总结模块的查找顺序是:内存中已经加载的模块 => 内置模块 => sys.path 路径中包含的模块。

需要特别注意的是:自定义的模块名不应该与系统内置模块重名

sys.path 变量是一个字符串列表,用于确定解释器的模块搜索路径。该变量被初始化为从环境变量 PYTHONPATH 获取的默认路径,或者如果 PYTHONPATH 未设置,则从内置默认路径初始化。在初始化后,Python 程序可以修改 sys.path,路径放到前面的优先于标准库被加载。

1
2
3
>>> import sys
>>> sys.path.append('/a/b/c/d')
>>> sys.path.insert(0,'/x/y/z') #排在前的目录,优先被搜索

搜索时按照 sys.path 中从左到右的顺序查找,位于前的优先被查找,sys.path 中还可能包含 .zip 归档文件和 .egg 文件,Python 会把 .zip 归档文件当成一个目录去处理。

至于 .egg 文件是由 setuptools 创建的包,这是按照第三方 Pthon 和扩展时使用的一种常见格式,·egg 文件实际上只是添加了额外元数据(如版本号,依赖项等)的 .zip 文件。

需要强调的一点是:只能从 .zip 文件中导入 .py.pyc 等文件。使用 C 编写的共享库和扩展块无法直接从 .zip 文件中加载(此时 setuptools 等打包系统有时能提供一种规避方法),且从 .zip 中加载文件不会创建 .pyc 或者 .pyo 文件,因此一定要事先创建他们,来避免加载模块时性能下降。

编译 Python 文件

为了加速模块载入(提高的是加载速度而程序非运行速度),Python 在 __pycache__ 目录里缓存了每个模块的编译后版本,名称为 module.version.pyc ,通常会包含 Python 的版本号。例如,在CPython3.7 版本下,hello.py 模块会被缓存成 __pycache__/hello.cpython-37.pyc。这种命名规范保证了编译后的结果多版本共存。

Python检查源文件的修改时间与编译的版本进行对比,如果过期就需要重新编译。这是完全自动的过程。此外,编译的模块与平台无关,所以相同的库可以在不同的架构的系统之间共享,即 pyc 是一种跨平台的字节码,类似于 JAVA 、.NET,是由 Python 虚拟机来执行的,但是 pyc 的内容跟 Python 的版本相关,不同的版本编译后的 pyc 文件不同,2.5 编译的 pyc 文件不能到 3.5 上执行,并且 pyc 文件是可以反编译的,因而它的出现仅仅是用来提升模块的加载速度的。

Python 在两种情况下不会检查缓存。首先,对于从命令行直接载入的模块,它从来都是重新编译并且不存储编译结果;其次,如果源文件不存在,那么缓存的结果也不会被使用,它不会检查缓存。为了支持无源文件(仅编译)发行版本, 编译模块必须是在源目录下,并且绝对不能有源模块。

给专业人士的一些小建议:

1、模块名区分大小写,foo.pyFOO.py 代表的是两个模块。

2、你可以使用 -O 或者 -OO 转换 Python 命令来减少编译模块的大小。-O 开关去除(assert)断言语句,-OO 开关同时去除(assert)断言语句和 __doc__ 字符串。由于一些程序可能依赖于assert语句或文档字符串,你应该在在确认需要的情况下使用这些选项。

3、一个从 .pyc 文件读出的程序并不会比它从 .py 读出时运行的更快,.pyc 文件唯一快的地方在于载入速度。

4、只有使用 import 语句时才将文件自动编译为 .pyc 文件,在命令行或标准输入中指定运行脚本则不会生成这类文件,因而我们可以使用 compieall 模块为一个目录中的所有模块创建 .pyc 文件。

标准模块

python提供了一个标准模块库,一些模块被内置到解释器中,这些提供了不属于语言核心部分的操作的访问,但它们是内置的,无论是为了效率还是提供对操作系统原语的访问。这些模块集合是依赖于底层平台的配置项,如winreg模块只能用于windows系统。特别需要注意的是,sys模块内建在每一个python解释器

Python 附带了一个标准模块库,一些模块内置于解释器中,它们提供对不属于语言核心但仍然内置的操作的访问,以提高效率或提供对系统调用等操作系统原语的访问。这些模块的集合是一个配置选项,它也取决于底层平台。例如 winreg 模块只在 Windows 操作系统上提供。一个特别值得注意的模块 sys 它被内嵌到每一个 Python 解释器中。变量 sys.ps1sys.ps2 定义用作主要和辅助提示的字符串:

1
2
3
4
5
6
7
8
9
>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!
C>

这两个变量只有在编译器是交互模式下才被定义。

dir() 函数

内置函数 dir() 用于查找模块定义的名称。 它返回一个排序过的字符串列表:

1
2
3
4
5
6
7
8
9
import hello

print(dir(hello))

'''
执行结果:
from the hello.py
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'change', 'money', 'read1', 'read2']
'''

如果没有参数 dir() 会列出你当前定义的名称:

1
2
3
4
5
6
7
8
9
10
import hello

a = [1, 2, 3, 4, 5]
print(dir())

'''
执行结果:
from the hello.py
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'hello']
'''

注意:它列出所有类型的名称:变量,模块,函数,等等。

dir() 不会列出内置函数和变量的名称。如果你想要这些,它们的定义是在标准模块 builtins 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented',
'NotImplementedError', 'OSError', 'OverflowError',
'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError',
'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError',
'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__',
'__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs',
'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits',
'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit',
'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview',
'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars',
'zip']

官方链接:

有钱任性,请我吃包辣条
0%