本文探讨Python模块和Python包,这两种机制有助于模块化编程。
模块化编程是指将大型笨拙的编程任务分解为单独的,较小的,更易于管理的子任务或模块的过程。然后可以像构建模块一样将各个模块拼凑在一起以创建更大的应用程序。
在大型应用程序中模块化代码有几个优点:
函数,模块和包都是Python中促进代码模块化的构造。
# 多行输出
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
实际上,在Python中定义模块的方式有三种:
在所有三种情况下,都以相同的方式访问模块的内容:使用import语句。在这里,重点将主要放在用Python编写的模块上。用Python编写的模块的妙处在于它们的构建极其简单。您需要做的就是创建一个包含合法Python代码的文件,然后为该文件命名并带有.py扩展名。
例如,假设有一个mod.py文件包含以下内容:
s = "Hello world!"
a = [100, 200, 300]
def foo(arg):
print(f'arg = {arg}')
class Foo:
pass
在mod.py中定义了以下几个对象:
假设mod.py位于适当的位置,稍后您将了解更多信息,可以通过以下方式导入模块来访问这些对象:
import mod
print(mod.s)
mod.a
mod.foo(['quux', 'corge', 'grault'])
arg = ['quux', 'corge', 'grault']
x = mod.Foo()
Hello world!
arg = ['quux', 'corge', 'grault']
[100, 200, 300]
继续上面的示例,让我们看一下Python执行该语句时发生的情况:
import mod
当解释器执行上面的import语句时,它会在一个目录列表中搜索mod.py,这些目录由以下来源组成:
结果搜索路径可在Python变量中访问,该变量sys.path从名为的模块获取sys:
import sys
sys.path
['/home/aistudio',
'/opt/conda/envs/python35-paddle120-env/lib/python37.zip',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/lib-dynload',
'',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/IPython/extensions',
'/home/aistudio/.ipython']
的确切内容sys.path取决于安装。几乎可以肯定,上述内容在您的计算机上看起来会稍有不同。因此,为了确保找到您的模块,您需要执行以下操作之一:
实际上,还有一个附加选项:您可以将模块文件放在您选择的任何目录中,然后sys.path在运行时进行修改,使其包含该目录。例如,在这种情况下,您可以放入目录./data,然后输入以下语句:
sys.path.append(r'./data')
sys.path
import mod
['/home/aistudio',
'/opt/conda/envs/python35-paddle120-env/lib/python37.zip',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/lib-dynload',
'',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages',
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/IPython/extensions',
'/home/aistudio/.ipython',
'./data']
导入模块后,您可以使用模块的__file__属性确定找到模块的位置:
import mod
mod.__file__
import re
re.__file__
'/home/aistudio/mod.py'
'/opt/conda/envs/python35-paddle120-env/lib/python3.7/re.py'
import语句可将模块内容提供给调用方。该import语句采用许多不同的形式,如下所示。
import
最简单的形式是上面已经显示的形式。请注意,这不会使调用者可以直接访问模块内容。每个模块都有其自己的专用符号表,该符号表用作模块中定义的所有对象的全局符号表。因此,模块已经创建了一个单独的名称空间。该语句import
从调用方来看,只有通过点表示法以
import mod
mod
但是s并且foo保留在模块的专用符号表中,并且在本地上下文中没有意义:
s
foo('quux')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
in
----> 1 s
2 foo('quux')
NameError: name 's' is not defined
要在本地上下文中访问,模块中定义的对象名称必须加上mod前缀:
mod.s
mod.foo('quux')
'Hello world!'
arg = quux
此外,在一个import语句中可以指定几个逗号分隔的模块:
import [, ...]
from
import语句的另一种形式允许模块中的单个对象直接导入到调用者的符号表中:
from
import
执行完以上语句后,
from mod import s, foo,Foo
s
foo('quux')
x = Foo()
x
'Hello world!'
arg = quux
因为这种形式的导入将对象名称直接放入调用者的符号表中,所以任何已经存在的同名对象都将被覆盖,如下所示:
a = ['foo', 'bar', 'baz']
a
from mod import a
a
['foo', 'bar', 'baz']
[100, 200, 300]
它甚至可以不加选择地从一个模块导入所有内容:
from import *
这会将把
from mod import *
s
a
foo
Foo
'Hello world!'
[100, 200, 300]
mod.Foo
在大规模生产代码中并不推荐这样做。这有点危险,因为您是在将名称全部输入到本地符号表中。除非您对它们都很了解,并且确信不会发生冲突,否则您很有可能会不小心覆盖现有的名称。但是,当您只是为了测试或发现的目的而随意使用交互式解释器时,这种语法非常方便,因为它可以让您快速访问模块必须提供的所有内容,而无需大量输入。
from
也可以导入单独的对象,但是使用替代名称将它们输入到本地符号表中:
from import as [, as …]
这样就可以将名称直接放置在本地符号表中,但可以避免与以前存在的名称冲突:
s = 'foo'
a = ['foo', 'bar', 'baz']
from mod import s as string, a as alist
s
string
a
alist
'foo'
'Hello world!'
['foo', 'bar', 'baz']
[100, 200, 300]
import
您也可以使用备用名称导入整个模块:
import as
import mod as my_module
my_module.a
my_module.foo('qux')
[100, 200, 300]
arg = qux
可以从函数定义中导入模块内容。在这种情况下,import只有在调用该函数后,才会发生:
def bar():
from mod import foo
foo('corge')
bar()
arg = corge
但是,Python 3不允许在函数内任意导入*的语法,如下所示:
def bar():
from mod import *
File "", line 4
SyntaxError: import * only allowed at module level
最后,一个带有except ImportError子句的try语句可以用来防止不成功的导入尝试:
try:
# Non-existent module
import baz
except ImportError:
print('Module not found')
Module not found
内置函数dir()返回一个名称空间中定义的名称列表。如果没有参数,它会在当前本地符号表中产生一个按字母顺序排序的名称列表:
dir()[0:5]
['Foo', 'In', 'InteractiveShell', 'Out', '_']
qux = [1, 2, 3, 4, 5]
dir()[-10:-1]
['get_ipython', 'mod', 'my_module', 'quit', 'qux', 're', 's', 'string', 'sys']
注意dir()上面的第一个调用是如何列出几个自动定义的名称的,这些名称在解释器启动时已经存在于名称空间中。随着新的名称定义(qux),它们出现在的后续,调用dir()。这对于识别由import语句确切添加到名称空间的内容很有用。当给定参数作为模块名称时,dir()列出模块中定义的名称:
import mod
dir(mod)
['Foo',
'__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__spec__',
'a',
'foo',
's']
任何.py包含模块的文件本质上也是Python 脚本,没有任何理由它不能像一个脚本一样执行。
这里还是mod.py,正如上面定义的那样。比如命令行:
python mod.py
此外在mod.py文件中添加输出内容,如下所示:
s = "Hello world!"
a = [100, 200, 300]
def foo(arg):
print(f'arg = {arg}')
class Foo:
pass
print(s)
print(a)
foo('quux')
x = Foo()
print(x)
现在输出如下:
Hello world!
[100, 200, 300]
arg = quux
<__main__.Foo object at 0x7f1e792b9e50>
不幸的是,现在当作为模块导入时,它还会生成输出:
import mod
Hello world!
[100, 200, 300]
arg = quux
这可能不是您想要的。导入模块时,模块通常不生成输出。如果您可以区分文件是作为模块加载时还是作为独立脚本运行时,这不是很好吗?
将.py文件导入为模块时,Python 会将特殊的dunder变量设置为模块__name__的名称。但是,如果文件作为独立脚本运行,__name__则(创造性地)设置为string ‘__main__’。利用这个事实,您可以识别出运行时是哪种情况,并相应地更改mod.py为:
s = "Hello world!"
a = [100, 200, 300]
def foo(arg):
print(f'arg = {arg}')
class Foo:
pass
if (__name__ == '__main__'):
print('Executing as standalone script')
print(s)
print(a)
foo('quux')
x = Foo()
print(x)
为了提高效率,每个解释器会话仅加载一次模块。对于函数和类定义来说,这很好,它们通常占模块内容的大部分。但是一个模块也可以包含可执行语句,通常用于初始化。请注意,这些语句仅在第一次导入模块时执行。
考虑以下文件mod.py:
a = [100, 200, 300]
print('a =', a)
调用mod模块会以下结果:
>>> import mod
a = [100, 200, 300]
>>> import mod
>>> import mod
>>> mod.a
[100, 200, 300]
该print()语句不会在后续导入上执行。(就此而言,赋值语句也不是,而是作为mod.ashows 值的最终显示,这无关紧要。完成赋值后,它会保留下来。)如果对模块进行了更改并需要重新加载,则需要重新启动解释器或使用reload()从module 调用的函数importlib:
>>> import mod
a = [100, 200, 300]
>>> import mod
>>> import importlib
>>> importlib.reload(mod)
a = [100, 200, 300]
假设您开发了一个非常大的应用程序,其中包含许多模块。随着模块数量的增加,如果将它们倾倒到一个位置,则很难跟踪所有模块。如果它们具有相似的名称或功能,则尤其如此。您可能希望有一种分组和组织的方法。
包允许使用点表示法对模块名称空间进行分层结构。就像模块帮助避免全局变量名之间的冲突一样,包帮助避免模块名之间的冲突。
创建软件包非常简单,因为它利用了操作系统固有的分层文件结构。考虑以下结构目录安排:
- work
- mod1.py
- mod2.py
mod1.py:
def foo():
print('[mod1] foo()')
class Foo:
pass
mod2.py:
def bar():
print('[mod2] bar()')
class Bar:
pass
基于这种结构,如果mod1.py和mod2.py为workd文件夹下,你可以使用点符号引用这两个模块(work.mod1, work.mod2),并使用你已经熟悉的语法导入它们:
import [, ...]
from import
from import as
from import [, ...]
from import as
import work.mod1, work.mod2
work.mod1.foo()
x = work.mod2.Bar()
x
[mod1] foo()
也可以直接导入包
import work
但这没有用。尽管严格来说,这是语法上正确的Python语句,但它并没有做任何有用的事情。特别是,它不会将work任何模块放入本地名称空间中:
>>> import work
>>> work.mod1
Traceback (most recent call last):
File "", line 1, in
AttributeError: module 'work' has no attribute 'mod1'
>>> work.mod2
Traceback (most recent call last):
File "", line 1, in
AttributeError: module 'work' has no attribute 'mod2'
要实际导入模块或其内容,您需要使用上面显示的形式之一。
如果包目录中存在一个名为__init__.py的文件,则在导入包或包中的模块时将调用该文件。这可用于执行程序包初始化代码,例如程序包级数据的初始化。
例如,考虑以下__init__.py文件:
print(f'Invoking __init__.py for {__name__}')
A = ['quux', 'corge', 'grault']
让我们从上面的例子将这个文件添加到work目录:
- work
- __init__.py
- mod1.py
- mod2.py
现在,当导入包时,将A初始化全局列表:
import work
work.A
Invoking __init__.py for work
['quux', 'corge', 'grault']
包中的模块可以通过依次导入全局变量来访问全局变量,让我们修改mod1.py
mod1.py
def foo():
from work import A
print('[mod1] foo() / A = ', A)
class Foo:
pass
from work import mod1
mod1.foo()
[mod1] foo()
__init__.py也可以用于从软件包中自动导入模块。例如,在前面您已经看到,该语句import work仅将名称work放置在调用者的本地符号表中,而不会导入任何模块。但是,如果__init__.py在work目录中包含以下内容:
print(f'Invoking __init__.py for {__name__}')
import work.mod1, work.mod2
模块mod1和mod2会自动导入
出于以下讨论的目的,先前定义的程序包已扩展为包含一些其他模块:
- work
- mod1.py
- mod2.py
- mod3.py
- mod4.py
现在,work目录中定义了四个模块。它们的内容如下所示:
mod1.py
def foo():
print('[mod1] foo()')
class Foo:
pass
mod2.py
def bar():
print('[mod2] bar()')
class Bar:
pass
mod3.py
def baz():
print('[mod3] baz()')
class Baz:
pass
mod4.py
def qux():
print('[mod4] qux()')
class Qux:
pass
您已经看到,当import *用于模块时,模块中的所有对象都将导入到本地符号表中,除了那些名称以下划线开头的对象外,与往常一样。ython遵循以下约定:如果package目录中的__init__.py文件包含名为__all__的列表,则这个列表中视为要导入的模块。
对于本示例,假设您在work录中创建一个__init__.py:
__all__ = [
'mod1',
'mod2',
'mod3',
'mod4'
]
将会在from work import *自动导入所有四个模块
from work import *
mod1
mod2
顺便说一句,__all__也可以在模块中定义它,并且具有相同的目的:控制使用导入的内容import *。例如,修改mod1.py如下:
__all__ = ['foo']
def foo():
print('[mod1] foo()')
class Foo:
pass
from work.mod1 import *只会导入__all__中的内容
from work.mod1 import *
>>> foo()
[mod1] foo()
>>> Foo
Traceback (most recent call last):
File "", line 1, in
Foo
NameError: name 'Foo' is not defined
总之,包和模块__all__都使用它来控制在指定时导入的内容。但是默认行为不同:import *
程序包可以包含嵌套子程序包到任意深度。例如,让我们对示例包目录进行如下修改:
- work
- subpackage1
- mod1.py
- mod2.py
- subpackage2
- mod3.py
- mod4.py
mod1等四个模块如先前定义,但引入两个子包。导入仍然与之前显示的相同。语法类似,但是使用其他点号将包名与子包名分开:
import work.subpackage1.mod1
work.subpackage1.mod1.foo()
from work.subpackage1 import mod2
mod2.bar()
from work.subpackage2.mod3 import baz
baz()
from work.subpackage2.mod4 import qux as grault
grault()
[mod1] foo()
[mod2] bar()
[mod1] foo()
[mod3] baz()
[mod4] qux()
此外,一个子包中的模块可以引用同级子包中的对象(如果同级包中包含您需要的某些功能)。例如,假设您希望从模块mod3中导入和执行函数foo()(在模块mod1中定义)。你可以使用绝对导入:
work/subpackage2/mod3.py中的内容如下:
def baz():
print('[mod3] baz()')
class Baz:
pass
from work.subpackage1.mod1 import foo
foo()
from work.subpackage2 import mod3
mod3.foo()
[mod1] foo()
https://realpython.com/python-modules-packages/