飞道的博客

一文了解Python的模块和包

257人阅读  评论(0)

本部分主要介绍 Python 模块和包的一些基础知识。阅读本文预计需要 10 min。

1. 前言

代码 --> 函数 – > 模块 --> 包,是一个由简单到复杂,不断抽象的过程。设计这些都是为了方便我们管理和维护我们的代码。当代码越来越多,利用模块和包来管理 Python 代码就非常有必要了。本文主要介绍:

  • 模块(定义、导入、.pyc 文件、搜索路径、dir()、pip)
  • 包(定义、绝对导入、相对导入)

2. 模块

模块(module):在 Python 中一个 xxx.py 文件就是一个模块,其中,xxx 就模块名。如:test.py 模块的模块名就是 test

使用模块主要有以下好处:

  1. 提高了代码的可维护性。
  2. 可以更好地复用代码。
  3. 可以避免函数名、类名、变量名冲突。在不同模块中,出现同名函数和变量,也不会导致冲突。

命名模块名时要注意:

  1. 模块名的命名方式遵循变量名命名规则,不要出现中文和特殊字符。
  2. 不要和内置模块重名。可以在 Python 交互模式下 import xxx,检验 xxx 模块是否是内置模块冲突。

如:

>>> import collections
>>> import abc
>>> import apple
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'apple'

可以发现,collectionsabc 都被成功导入,说明它们是内置模块。不能再作为自定义的模块名,而 apple 导入失败,说明 apple 不是内置模块,可以作为自定义的模块名。

2.1 模块的创建

下面创建一个 hello.py 模块

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"a test module"

__author__ = "Jock"

import sys

def test():
    args = sys.argv
    if len(args) == 1:
        print(f"打印sys.argv:{sys.argv}")
        print("Hello world")
    elif len(args) == 2:
        print(f"打印sys.argv:{sys.argv}")
        print(f"Hello, {args[1]}")
    else:
        print(f"打印sys.argv:{sys.argv}")
        print("Too many arguments")

if __name__ == '__main__':
    test()

说明:

  • 第 1 行和第 2 行是标准注释,第 1 行可以让这个 hello.py 模块直接在 Unix/Linux/Mac 上运行,第 2 行注释表示,.py 文件本身使用标准的 UTF-8 编码。
  • 第 4 行是一个字符串,表示模块的文档注释,说明模块功能。任何模块代码的第一个字符串都被视为模块的文档注释。
  • 第 6 行使用 __author__变量把作者写进去,方便别人瞻仰你的大名。

以上部分就是 Python 模块的标准文件模板,你也可以全部删掉,不过按照标准做事准没错。

在上面的 hello.py 模块中 import sys 是导入 sys 模块。sys 是一个标准模块,我们要使用它,必须先用 import 导入。导入后,sys 变量就会指向 sys 模块,通过这个 sys 变量就可以访问 sys 模块的所有功能。
sys 模块中的 args 变量会把命令行的所有参数存为一个 list,并且第一个元素是 .py 文件名。我们可以在命令行下测试如下:

(base) C:\Users\Jock\Desktop\Python_Code>python test.py
打印sys.argv:['test.py']
Hello world

(base) C:\Users\Jock\Desktop\Python_Code>python test.py Jock
打印sys.argv:['test.py', 'Jock']
Hello, Jock

(base) C:\Users\Jock\Desktop\Python_Code>python test.py Jock Kobe
打印sys.argv:['test.py', 'Jock', 'Kobe']
Too many arguments

最后解释一下以下两行代码。

if __name__ == '__main__':
    test()

模块名可以通过 xxx.__name__ 来获取,如 hello.py 模块,那么 hello.__name__ 将返回模块名 hello。但是,当我们把 hello.py 模块放到命名行执行时,即:python hello.py,这时 Python 解释器把 __name__ 赋值为 __main__,而如果在其他地方导入该 Hello 模块时,if 判断将失败。通过添加 if __name__ == '__main__': 可以让一个模块通过命令行运行时,执行一些额外代码。

这样的设计经常用于为模块提供一个方便的用户接口,或用于模块测试。

如果我们启动交互模式,再导入 hello 模块。

(base) C:\Users\Jock\Desktop\Python_Code>python
Python 3.7.6 (default, Jan  8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello

这里并没有打印 Hello world,因为模块被导入时,模块名为 hello,所以if __name__ == '__main__': 判断为假,后面的代码 test() 不会执行。只有调用 hello.test() 时才会打印 Hello world。

>>> hello.test()
打印sys.argv:['']
Hello world

我们可以发现 `hello.test() 写起来有点麻烦,如果经常使用这个函数,可以把它赋值给一个局部变量。

>>> test = hello.test
>>> test()
打印sys.argv:['']
Hello world

2.2 模块的导入方式

导入模块的方式有两种:

  • import module_name
  • from module_name import A

前面已经用过 import module_name 的形式导入模块,这种方法会把整个模块导入。如果我们只想导入模块的一部分,那么我们可以用 from A import B 的形式,它表示从模块 A 中导入需要的 B,B 可以是类或者函数或者变量等,其余模块部分我们不需要就不导入,从而节省内存,避免命名冲突等。以上面的 hello 模块为例:

>>> from hello import test
>>> test()
打印sys.argv:['']
Hello world

如果有时候模块 A 或者导入的 B 名字太长,我们写起来很麻烦,这时可以用 as 来设置别名,简化调用,如:

>>> import hello as h
>>> h.test()
打印sys.argv:['']
Hello world
>>> from hello import test as t
>>> t()
打印sys.argv:['']
Hello world

其实我们也可以用 from A import * 的形式把模块 A 全部导入,但是一般不推荐这么做,因为这可能会带来副作用,会降低导入速度,导致命名冲突。

注意:出于效率的考虑,每个模块在每个解释器会话中只被导入一次。因此,如果更改了模块,则必须重新启动解释器,或者,如果只是一个要交互式地测试模块,可以使用 importlib.reload(),例如 import importlib; importlib.reload(modulename)。

2.3 模块的作用域问题

在一个模块中,我们可以定义很多函数和变量,我们希望一部分函数和变量给别人调用,有一部分函数和变量仅限于模块内部使用。Python 中可以通过 _ 前缀来实现仅限模块内部使用。

我们希望给别人调用的函数和变量定义为公开的(public),如:test、get、PI等。
类似于 __xxx__ 这样的变量是特殊变量,可以直接被引用,但是有特殊用途,比如前面的 __author____name__等就是特殊变量。模块文档注释可以通过 __doc__ 特殊变量访问,自定义的变量一般不要用这样的形式定义。

类似于 _xx__xx 这样的函数或变量是私有的(private),不应该被直接引用,如:_abc__abc 等。

举个栗子:

def _private_1(name):
    return f"Hello, {name}"

def _private_2(name):
    return f"Hi, {name}"

def greeting(name):
    if len(name) > 3:
        return _private_1(name)
    else:
        return _private_2(name)

在这个模块中,我们公开 greeting()函数,给外界调用,而把内部逻辑用 private 函数隐藏起来。这其实就是一种代码封装和抽象的方法,调用者只要知道调用,不用了解具体的实现。即:外部不需要引用的函数全部定义为 private,只有外部需要的函数才定义为 public。

2.4 模块搜索路径

当我们导入一个模块时,Python 会从指定的路径下搜索对应的模块,如果没有找到,就报错。

>>> import good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'good'

通常情况下,Python 解释器会搜索当前目录,所有已安装的内置模块和第三方模块,搜索路径存放在 sys 模块的 path 变量中。

>>> import sys
>>> sys.path
['', 'C:\\Users\\Jock\\anaconda3\\python37.zip', 'C:\\Users\\Jock\\anaconda3\\DLLs', 'C:\\Users\\Jock\\anaconda3\\lib', 'C:\\Users\\Jock\\anaconda3', 'C:\\Users\\Jock\\anaconda3\\lib\\si
te-packages', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\win32', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\win32\\lib', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\Pyth
onwin']

我们可以通过两种方式添加自己的搜索目录:

  • 直接修改 sys.path,添加要搜索的变量:
>>> import sys
>>> sys.path.append("C:\\Users\\Jock\\mylib")
>>> sys.path
['', 'C:\\Users\\Jock\\anaconda3\\python37.zip', 'C:\\Users\\Jock\\anaconda3\\DLLs', 'C:\\Users\\Jock\\anaconda3\\lib', 'C:\\Users\\Jock\\anaconda3', 'C:\\Users\\Jock\\anaconda3\\lib\\si
te-packages', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\win32', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\win32\\lib', 'C:\\Users\\Jock\\anaconda3\\lib\\site-packages\\Pyth
onwin', 'C:\\Users\\Jock\\mylib']
  • 第二种方法是设置环境变量,环境变量中的内容会被自动添加到模块搜索路径中。

2.5 “编译过的” Python 文件

为了加速模块载入,Python 会自动在 __pycache__ 目录里,缓存每个模块编译后的版本,名称为 module.version.pyc ,这就是.pyc 文件的由来。其中 version 一般使用 Python 版本号。例如,在 CPython 版本 3.7 中,spam.py 模块的编译版本将被缓存为 __pycache__/spam.cpython-37.pyc。此命名约定允许来自不同发行版和不同版本的 Python 的已编译模块共存。

Python 通过检查源模块的修改日期,来确定模块的编译版本是否已过期,如果过期就会重新编译,并缓存。这是一个完全自动化的过程。此外,编译的模块(.pyc文件)与平台无关,因此可以在具有不同操作系统之间共享。

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

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

  1. 你可以在 Python 命令中使用 -O 或者 -OO 开关, 以减小编译后模块的大小。 -O 开关去除断言语句,-OO 开关同时去除断言语句和 __doc__ 字符串。由于有些程序可能依赖于这些,你应当只在清楚自己在做什么时才使用这个选项。“优化过的”模块有一个 opt- 标签并且通常小些。将来的发行版本或许会更改优化的效果。
  2. 引入.pyc 文件的目的是为了加快载入速度,并不会影响执行速度,.pyc 文件和 源文件执行的速度是一样的。
  3. compileall 模块可以为一个目录下的所有模块创建 .pyc 文件。
  4. 更多的细节可以参看,PEP 3147

2.6 dir()函数

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

>>> import hello
>>> dir(hello)
['__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'sys', 'test']

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

>>> a = [1, 2, 3, 4, 5]
>>> import hello
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'hello']

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

但 dir() 不会列出内置函数和变量的名称。如果要列出这些,可以这么做:

>>> import builtins
>>> dir(builtins)
['ArithmeticError', ...省略... 'type', 'vars', 'zip']

2.7 安装模块

Python 有非常强大的第三方库,在 Python 中我们可以用包管理工具 pip 来完成安装第三方模块。如:安装 requests 库,直接在命令行用命令:pip install requests安装即可。
此外,我们可以直接安装 Anaconda,它内置了非常多的第三方模块,非常强大,这样我们就不用一个一个的去安装。

3. 包

如果有很多模块需要管理,这时候包(package)就派上用场了。包是按目录来组织管理模块的。

举个栗子:

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

注意目录下必须有 __init__.py 文件,这样这个目录才会被 Python 当成包,否则 Python 会把它当做一个普通目录。在最简单的情况下,__init__.py 可以只是一个空文件,但它也可以执行包的初始化代码或设置 __all__ 变量。__init__.py 本身就是一个模块,它的模块名是相应的目录名。如:formats 目录下的 __init__.py 模块名就是 formats。多级层次的包结构中,echo.py 文件的模块名是 sound.effects.echo

我们可以导入包中的一个模块,如:

import sound.effects.echo

这会加载子模块 sound.effects.echo 。但引用它时必须使用它的全名。

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

还可以用另一种方法导入子模块,并使其在没有包前缀的情况下可用:

from sound.effects import echo

echo.echofilter(input, output, delay=0.7, atten=4)

另一种形式是直接导入所需的函数或变量,如:导入函数 echofilter():

from sound.effects.echo import echofilter

echofilter(input, output, delay=0.7, atten=4)

小结:

  • 当使用 from package import item 时,item 可以是包的子包,也可以是包中定义的模块,甚至函数、类和变量。

不推荐使用 from sound.effects import * 的形式导入整个包,因为这会使得导入花费很长时间,而且还可能要有副作用。

唯一的解决方案是让包作者提供一个包的显式索引。import 语句使用下面的规范:如果一个包的 __init__.py 代码定义了一个名为 __all__ 的列表,它会被视为在遇到 from package import * 时应该导入的模块名列表。例如,文件 sound/effects/__init__.py 包含以下代码:__all__ = ["echo", "surround", "reverse"]。这意味着 from sound.effects import * 将导入 sound 包的三个命名子模块。这部分还需要留作以后深究。

总之, 使用 from package import specific_submodule 没有任何问题,也是推荐的做法。

有时我们会见到一些相对导入。

from . import echo
from .. import formats
from ..filters import equalizer

说明:. 代表当前包,.. 代表上一级包,... 代表上上级包。
请注意,相对导入是基于当前模块的名称进行导入的。由于主模块的名称总是 '__main__' ,因此用作 Python 应用程序主模块的模块必须始终使用绝对导入。

一些规范:

  1. import 语句总是放在文件开头的地方。
  2. 引入模块的时候,from math import sqrt 比 import math 更好。
  3. 如果有多个 import 语句,应该将其分为三部分,从上到下分别是 Python 标准模块、第三方模块和自定义模块,每个部分内部应该按照模块名称的字母表顺序来排列。

4. 巨人的肩膀

  1. The Python Tutorial Modules
  2. 廖雪峰 Python3 教程

推荐阅读:

  1. 编程小白安装Python开发环境及PyCharm的基本用法
  2. 一文了解Python基础知识
  3. 一文了解Python数据结构
  4. 一文了解Python流程控制
  5. 一文了解Python函数
  6. 一文了解Python部分高级特性

如果我的笔记对你有所帮助,欢迎关注我的微信公众号,加我好友,一起学习进步!


转载:https://blog.csdn.net/qq_27283619/article/details/105956548
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场