xuzhengfu / pilot

进入编程世界的第一课
1 stars 0 forks source link

p2-3-modules 第三章 模块 #33

Open xuzhengfu opened 4 years ago

xuzhengfu commented 4 years ago

1. 本章主旨

介绍如何创建自己的模块以及如何使用。

模块(modules),其实就是保存成文件的 Python 源代码。

当我们做点稍微复杂的事情时,就不太能够把所有代码写在一个文件里了,因为那样的话,文件会变得巨大,非常难查找难阅读难修改。而 module 是 Python 提供给我们的模块化方法,让我们可以把源代码分开放到不同的文件当中,并在需要时引用其他文件里的代码。

我们前面已经多次引用过 Python 的各种非内置模块,也熟悉了两种引入模块中函数或变量的语法:

  • import xxxx:引入 xxxx 模块,可以用 xxxx.func() 的语法调用其中的函数(变量也类似);
  • from xxxx import func1, func2:引入 xxxx 模块中的若干函数或变量,可以不带模块名直接使用 func1 func2

2. 创建模块

你只要把 Python 源代码存成一个 .py 的文件,这就是一个模块(module),而这个文件的文件名(不含后缀 .py)就是模块的名字。比如,我们把之前写好的函数保存为一个叫做 myutil.py 的文件。

3. 模块引入

3.0 模块引入示例

保存好 myutil.py 文件之后,只要告诉解释器在哪里可以找到这个文件,就可以用 import 语句来引入和使用了 —— 无论在 Jupyter Notebook 中还是其他 .py 文件中都是如此。

import sys
sys.path

结果如下:

['/Users/neo/Code/Repo/selfteaching/pilot',
 '/Users/neo/Code/Repo/selfteaching/pilot',
 '/Users/neo/.envs/jupyter/lib/python37.zip',
 '/Users/neo/.envs/jupyter/lib/python3.7',
 '/Users/neo/.envs/jupyter/lib/python3.7/lib-dynload',
 '/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7',
 '',
 '/Users/neo/.envs/jupyter/lib/python3.7/site-packages',
 '/Users/neo/.envs/jupyter/lib/python3.7/site-packages/IPython/extensions',
 '/Users/neo/.ipython']
import sys
sys.path
['/Users/xuzhengfu/Code/pilot-student',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python37.zip',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload',
 '',
 '/usr/local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/site-packages/IPython/extensions',
 '/Users/xuzhengfu/.ipython']

sys 是 Python 管理系统环境的模块,而 sys.path 保存着一个列表,里面是所有解释器 “认识” 的目录,这些目录都是解释器会自动去找模块的地方。

我们可以看到,运行 Python 解释器的当前目录是自动包含在里面的;然后就是 Python 环境的库所在目录,我们用 pip 安装的库都会放在这些目录下。

sys.path.append('./assets/')

再次运行 sys.path,结果如下:

['/Users/xuzhengfu/Code/pilot-student',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python37.zip',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7',
 '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload',
 '',
 '/usr/local/lib/python3.7/site-packages',
 '/usr/local/lib/python3.7/site-packages/IPython/extensions',
 '/Users/xuzhengfu/.ipython',
 './assets/']
import myutil
myutil.say_hi('Neo', 'Trinity')

在上面的例子中,当使用 import myutil 的时候,就向当前环境引入了 myutil 文件中定义的所有函数,和下面的语句等价:

from myutil import *

当然我们也可以只引入其中需要的函数,比如:

from myutil import is_prime

这种情况下,就不需要使用 myutil.is_prime(),而是直接写 is_prime() 就好。

3.1 模块的分层

设想我们在一个目录下有很多 .py 源代码文件,我们可以分若干子目录,根据用途把 .py 文件分到这些子目录中去,比如我们有这么一个目录:

~/Code/some_project:
├── modules/
│   ├── __init__.py
│   ├── auth.py
│   ├── helper.py
│   ├── math/
│   │   ├── calculus.py
│   │   ├── logic.py
│   └── string/
│       ├── codec.py
│       └── l10n.py
├── setup.py
└── main.py

我们看到我们在这个目录下有一些主程序的源文件 setup.py main.py,然后一些工具性的源代码被放到了 modules 这个子目录下,并根据其用途分了一些更细的子目录(比如 math string)。这样的结构很合理和美观,也非常有伸缩性,以后再大的工程、再多源代码,也都能组织和管理好。

第一步,你需要确保 modules 这个目录在解释器的搜索路径中,也就是将其加入 sys.path 列表(这件事可以在 setup.py 或者 main.py 里做):

sys.path.append('./modules/')

第二步,要确保 modules 目录下有一个叫做 __init__.py 的文件,这个文件通常为空文件,用于标识该目录是一个包含多个模块的包(package),所有这些模块都处在这个包的独立名字空间(namespace)下。

然后就可以引入 modules 目录下的所有这些模块了,比如我们想引入 auth.py 里的所有函数,可以这样(假定是要在 main.py 里使用,下同):

import auth

如果要使用 math 目录下的 calculus.py 文件里的所有函数,可以:

import math.calculus

也可以写成:

from math import calculus

或者:

from math.calculus import *

如果我们只要其中某几个函数,可以:

from math.calculus import ode, pde

如果我们要一次引入 string 目录下的所有模块,可以:

from string import *

只要一个目录满足下面两个条件就是一个 Python 的软件包(package):

  1. sys.path 列表中;
  2. 里面有个 __init__.py 文件。

以这个目录作为根,下面所有 .py 源文件(包括无论多少级子目录下的)都可以作为 module 被其他 Python 程序引入和使用。引入的方法就是逐级目录加最后的文件名,中间用 . 隔开即可。

3.2 引入中的别名

引入模块或者模块中的函数时,可以设置模块或者函数的别名,这个能力很重要,因为难免我们引入的模块或者函数可能与我们正在写的东西重名,别名可以避免发生冲突。

别名的语法很简单,在引入的时候加上 as xxx 就行,可以用于模块也可以用于函数,比如:

from myutil import is_prime as isp
isp(57)
import myutil as m
m.is_prime(27)

4. 查看系统内置模块

前面用到的 sys 模块还有别的用途,可以用它提供的 sys.builtin_module_names 列表查看所有系统内置模块或者检查某个模块是不是内置模块:

sys.builtin_module_names

和之前介绍的系统关键字列表一样,我们也最好不要用这里面有的名字,无论是作为我们自己的模块、类、函数还是变量名。

5. 模块中不只有函数

一个模块文件中,不一定只包含函数;它也可以包含函数之外的可执行代码。import 这个命令不过是让解释器加载指定的模块(源代码),并且从头到尾执行一遍,其中定义函数的部分让这些函数可以被我们使用,其余部分就在 import 语句执行时执行一次而已。

比如 this 模块的源代码,虽然这个模块里什么函数都没有,但是 import 之后,里面所有全局变量我们都可以使用。

详见 notebook 代码。

6. dir() 函数

系统内置函数 dir() 可以查看一个已经引入的模块里有哪些可以访问的变量和函数:

dir(myutil)
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'is_prime',
 'say_hi',
 'sqrt']

可以看到前面有一大堆都不是我们写的,而是系统自动加进去的,这属于模块的元数据(metadata)。看看里面是什么,比如:

myutil.__name__
'myutil'

7. __main__ 模块

Python 对此情况也提供了一个解决方案,这个方案就是使用特殊的模块名 __main__

  1. 前面看到,当我们 import 一个模块之后,可以用 myutil.__name__ 查看它的模块名,通常是它的文件名;
  2. Python 做了一个特殊的设定,那就是:当我们在 命令行界面 用 python myutil.py 来运行这个文件时,它的模块名 __name__ 的值为 '__main__',这是个独一无二的值,不会出现在任何引入的模块中,这样我们就可以区分开 “运行一个文件” 和 “引入一个模块” 这两种行为,加以区别对待。

也就是把模块写成这个样子:

def func1();
    ...

def func2();
    ...

if __name__ == '__main__':
    # 把所有 测试 或者 不希望被引入时执行的代码 放在这里

这么做的结果是:

  • 当 Python 文件被当作模块,被 import 语句导入时,if 判断失败,下面的代码不被执行;
  • 当 Python 文件被 python -m 运行的时候,if 判断成功,下面的代码被执行。

小结

  • 模块是 Python 管理多个源代码文件的基本单元,一个模块对应一个 .py 源文件;
  • 模块名可以包含若干节,用 . 分开,. 分开的每一节对应子目录和文件名,比如当前目录下 foo/goo/bar.py 这个文件的模块名就是 foo.goo.bar
  • 可以用 import 语句来引入解释器能找到的任何模块,可以全模块引入也可以只引入指定的函数或者变量,还可以给引入的模块、函数或变量用 as 指定别名;
  • 解释器会在 sys.path 这个变量保存的所有目录下查找我们想引入的模块;
  • 了解 sys.builtin_module_namesdir() 的用法,有助于我们探索当前环境的可用模块以及了解模块的信息;
  • 可以把不希望被引入时执行的代码放在 if __name__ == '__main__': 判断下面,让一个文件既可以被当做模块引入,也可以作为一个程序独立执行。

Logging

2020-03-03 20:10:27 initialize