目录

《从 Gopher 到 Pythonista,Python 快速上手指南》- 包与模块

概述

关于本系列文章的详细介绍,请看这篇博客:《从 Gopher 到 Pythonista,Python 快速上手指南 - 序》

欢迎来到 Python 旅程第二站!本文我们继续来聊 Python 里的“包和模块”。

包与模块

在 Python 中,代码是通过包和模块来组织的。

Python 模块

在 Python 中,一个模块就是一个包含 Python 代码的 .py 文件。每个 Python 文件都可以被视为一个模块,可以被其他模块导入(import)。例如,如果我们有一个名为 module1.py 的文件,我们可以在另一个 Python 文件中使用 import module1 来导入它。

这是一个简单的模块示例:

1
2
3
4
5
6
7
# module1.py

def hello():
    print("Hello, World!")

if __name__ == "__main__":
    hello()

在这个例子中,我们定义了一个函数 hello,然后在 if __name__ == "__main__": 代码块中调用这个函数。这个代码块的特点是:

  1. 当模块被直接执行时,__name__ 的值为 "__main__",因此该代码块中的代码会被执行。
  2. 当模块被导入时,__name__ 的值为模块的名字,因此该代码块中的代码不会被执行。

这种设计使得我们可以在模块中定义一些只在模块被直接执行时才会运行的代码。

这个逻辑对 Gopher 来说会有点不适应,毕竟在 Golang 中首先不区分 .go 文件的身份,也就是没有“模块”的概念;其次 Golang 也不存在一个文件“可能被当作执行入口,也可能只是能被导入的普通模块”这种使用逻辑。

Python 包

在 Python 中,包是一个包含 __init__.py 文件的目录,用于组织模块和子包。__init__.py 文件可以为空,也可以包含初始化代码或定义包的属性。

例如,考虑以下的目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
my_project/

├── my_project/
   ├── __init__.py
   ├── module1.py
   ├── module2.py
   ├── package1/
      ├── __init__.py
      ├── module3.py
      └── module4.py
   └── ...

在这个例子中,my_project/my_project/ 是一个包,它包含 __init__.py 文件,两个模块 module1.pymodule2.py,以及一个子包 package1。子包 package1 也包含 __init__.py 文件,以及两个模块 module3.pymodule4.py

包的主要作用是提供一个命名空间,这使得我们可以有模块名相同但是位于不同包的模块。例如,my_project.my_project.module1my_project.my_project.package1.module1 是两个不同的模块。

细说 __init.py__

前面提到了 __init.py 这个特殊文件,我们来细看下这个文件有哪些作用:

  1. 标记目录为 Python 包:Python 将包含 __init__.py 文件的目录识别为包。这是 Python 识别包的主要机制,即使 __init__.py 文件为空。

  2. 执行包的初始化代码:当导入包时,Python 会自动执行 __init__.py 中的所有顶级代码。这使得 __init__.py 成为执行包级别初始化代码的理想场所。例如,你可以在 __init__.py 中设置包级别的变量,或执行必要的初始化过程。

  3. 控制 from package import * 的行为:当用户使用 from package import * 语句时,Python 会将 __init__.py 中的所有全局变量、函数和类导入到当前命名空间。然而,如果 __init__.py 定义了一个名为 __all__ 的列表,Python 会只导入列表中指定的模块。例如,如果 __init__.py 包含以下代码:

    1
    
    __all__ = ['submodule1', 'submodule2']
    

    那么 from package import * 只会导入 submodule1submodule2。这是一种控制包公开 API 的方式。

    此外,__init__.py 也常被用来导入包内的其他模块,这样用户可以直接从当前包中导入这些模块,而无需知道它们的具体位置。例如:

    1
    2
    3
    
    # __init__.py
    from . import module1
    from . import module2
    

    然后用户就可以直接从包中导入 module1module2

    1
    
    from package import module1, module2
    

    这种用法使得包的内部结构对调用者透明,使得你可以随时调整包的内部结构,而不影响调用者的代码。

导入模块与包(import)

在 Python 中,导入模块或包是一种常见的操作,可以让我们使用其他文件中的代码。Python 提供了多种导入方式,可以用于导入内建模块、第三方库,以及我们自定义的模块和包。下面将详细介绍 import 的各种用法和实例。

Python 提供了几种导入模块和包的方式,每种方式都有其特定的用途:

  1. import module_or_package:这是最基本的导入语法。这会导入整个模块或包,你需要使用模块或包的名称来访问其内容。例如,import math 后,你就可以使用 math.sqrt() 来调用 sqrt 函数。
  2. from module_or_package import something:这种语法允许你从模块或包中导入特定的函数、类或变量。例如,from math import sqrt 后,你就可以直接使用 sqrt(),无需再加模块名前缀。这种方式有助于保持代码的简洁性,但也可能引发名称冲突。
  3. from module_or_package import *:这种语法会导入模块或包中的所有内容,而无需使用模块或包的名称。这种方式并不推荐使用,因为它可能导致名称空间的污染和意料之外的名称冲突。
  4. import module_or_package as alias:这种语法允许你为导入的模块或包起别名。例如,import numpy as np 后,你就可以使用 np.array() 来调用 numpyarray 函数。这种方式在处理有长名称的模块时非常有用。
  5. from module_or_package import something as alias:这种语法结合了上述两种语法的优点,允许你从模块或包中导入特定的内容,并为其起别名。例如,from matplotlib import pyplot as plt 后,你就可以使用 plt.plot() 来调用 matplotlib.pyplotplot 函数。

总的来说,Python 的 import 语法提供了灵活的方式来导入和使用模块或包,使你可以根据实际需要选择最合适的方式。

导入内建模块

Python 自带了一些内建模块,比如 mathossys 等,我们可以直接导入并使用:

1
2
3
4
5
import math
import os

print(math.pi)
print(os.name)

导入第三方库

Python 拥有丰富的第三方库,我们可以使用 pip 工具来安装这些库,然后像内建模块一样导入使用。pip 是 Python 的包管理器,它能帮助我们下载和安装 Python 的第三方库。

例如,我们可以安装并使用流行的数据处理库 pandas

1
pip install pandas

然后在 Python 文件中,我们可以这样使用:

1
2
3
4
5
6
import pandas as pd

data = {'Name': ['Tom', 'Nick', 'John'], 'Age': [20, 21, 19]}
df = pd.DataFrame(data)

print(df)

在这个例子中,pandas 是一个包,我们导入了这个包,并给它起了一个别名 pd。我们可以使用 pd 来访问 pandas 包中的所有模块和函数。

导入自定义模块

我们也可以创建自己的模块,然后导入使用。例如,我们有一个名为 mymodule.py 的文件:

1
2
3
4
# mymodule.py

def hello():
    print("Hello, World!")

然后我们可以在另一个 Python 文件中导入并使用这个模块:

1
2
3
import mymodule

mymodule.hello()

导入包的本质

在 Python 中,我们可以使用 import 语句来导入包。当我们导入一个包时,Python 实际上是在执行该包的 __init__.py 文件。这个文件可以为空,也可以包含一些初始化代码或者定义包的属性。

例如,假设我们有以下的包结构:

1
2
3
4
5
6
7
8
pythonCopy code
my_package/

├── __init__.py
├── module1.py
└── sub_package/
    ├── __init__.py
    └── module2.py

我们可以使用 import my_package 来导入 my_package,这时 Python 会执行 my_package/__init__.py。如果我们想要导入 sub_package,可以使用 import my_package.sub_package

注意,当我们导入一个包时,Python 不会自动导入其内部的模块或子包,除非在 __init__.py 中明确指定。例如,如果我们想在导入 my_package 时也导入 module1,我们可以在 my_package/__init__.py 中添加 import my_package.module1

相对导入和绝对导入

在 Python 中,模块和包的导入可以使用相对导入(relative import)或绝对导入(absolute import)。这两种导入方式各有优点和使用场景。

一、相对导入

相对导入是根据当前模块的路径来导入其他模块或包。在相对导入中,单个点号(.)表示当前目录,两个点号(..)表示上一级目录,三个点号(...)表示上上一级目录,以此类推。例如,如果你有如下的包结构:

1
2
3
4
5
6
7
8
9
my_package/
│
├── __init__.py
├── sub_package1/
│   ├── __init__.py
│   └── module1.py
└── sub_package2/
    ├── __init__.py
    └── module2.py

如果你想在 module1.py 中导入 module2.py,可以使用相对导入 from ..sub_package2 import module2

需要注意的是,相对导入只能在包内部使用,不能在主程序(即直接被执行的 Python 文件)中使用。如果你在主程序中使用相对导入,Python 会抛出 ImportError

二、绝对导入

绝对导入是根据 Python 解释器的 sys.path(这是一个包含了所有可导入模块和包的路径的列表)来导入模块或包。这意味着,你可以从任何地方导入任何模块或包,只要它在 sys.path 中。例如,如果你想在 module1.py 中导入 module2.py,可以使用绝对导入 from my_package.sub_package2 import module2

绝对导入更清晰和直观,因为它显示了导入的模块或包的完整路径。因此,对于一个良好的项目结构,推荐尽可能使用绝对导入,这样可以避免很多由于相对路径引起的问题。

sys.path

前面提到了 sys.path,Python 的 sys.path 是一个列表,它决定了 Python 导入模块时会在哪些目录下查找。sys.path 通常包含以下路径:

  1. 当前脚本运行的目录(或者说是当前工作目录):这是 sys.path 的第一个元素,表示 Python 会优先在当前工作目录下查找模块。
  2. Python 的环境变量 PYTHONPATH 中指定的路径:这些路径是用户可以自定义的,可以将任何需要的路径添加到 PYTHONPATH 中,Python 将在这些路径中查找模块。
  3. Python 内置模块的路径:这些路径通常包括标准库模块的安装路径。
  4. Python 安装的第三方库的路径:这些路径通常是在 Python 的 site-packagesdist-packages 目录下。

你可以使用以下代码来查看当前 Python 环境的 sys.path

1
2
import sys
print(sys.path)

输出的结果会因为你的 Python 环境和操作系统的不同而有所不同。

Gopher 怎么看 Python 的包与模块

理解不同的包和模块管理机制是转换编程语言过程中的一个重要环节。从 Golang 转到 Python,这个变化尤其明显。

Golang 的包管理系统,基于其设计哲学——明确,简洁和高效,采用了简单、直观、分布式的管理方式,其通常存储在源代码管理(SCM)系统中,如 GitHub。这种设计方式使得 Golang 的包名冲突问题几乎不可能发生,因为每个包都有其唯一的导入路径,如 import github.com/user/project。这种分布式的、基于 SCM 的方式为开发者提供了极大的自由度,但同时也需要开发者自行管理包的版本和依赖关系。然而,这种设计也有其挑战,例如依赖管理和安全性问题。

相比之下,Python 的包和模块的设计增加了一些复杂性,但也提供了更大的灵活性。Python 的包是通过目录来实现的,而且要求包内必须包含 __init__.py 文件。Python 的包是集中式管理的,大部分包都存储在 Python 包索引(PyPI)上。这使得 Python 的包名必须在全局范围内是唯一的,也就有可能导致包名冲突。在导入时,我们只需要指定包名,而无需考虑其路径,例如 import pandas。这种设计使得 Python 的导入语句更简洁,同时集中管理的方式也让版本和依赖管理变得相对简单。然而,这也意味着我们需要避免包名冲突,这对于大型项目可能会带来一些挑战。

总的来说,Python 和 Golang 的包管理系统都有其优缺点,它们各自适应了其编程语言和社区的特点和需求。无论是分布式的 Golang 还是集中式的 Python,只是体现了不同的设计哲学。或许这里不应该去纠结孰优孰劣,存在即合理。

Gopher 注意事项

由于 Golang 和 Python 在包和模块的概念上存在明显的差异,我想有必要总结一些 Gopher 在使用 Python 包和模块时可能会遇到的常见问题和注意事项:

  1. 包和模块的理解:在 Golang 中,一个包就是一个目录,该目录包含了一系列 .go 文件,而在 Python 中,一个模块是一个 .py 文件,而一个包是一个包含 __init__.py 文件的目录。所以 Gopher 可能会下意识地以为 import 一个包就可以用包内的所有模块了,其实不然。
  2. 导入方式:在 Golang 中,我们导入包的方式是 import "package-name",而在 Python 中,我们可以导入整个包,也可以只导入包中的某个模块,甚至只导入模块中的某个函数或变量。例如,import package.modulefrom package.module import function 是 Python 中常见的两种导入方式。再次强调 Python 里导入一个包并不意味着自动导入内部所有的模块。
  3. 相对路径:在 Golang 中,导入包的路径通常从项目的根目录开始,而在 Python 中,导入模块或包的路径可以是相对路径(相对于当前模块),也可以是绝对路径(从项目的根目录开始)。例如:from . import modulefrom package import module
  4. 导入路径:在 Golang 中,包的导入路径通常会包含完整的 URL,例如 import "github.com/gin-gonic/gin",这种方式反映了 Go 的分布式依赖管理方式,每个包的路径直接对应其在版本控制系统中的位置。而在 Python 中,包和模块的导入路径通常只包含包名或模块名,例如 import pandas 或者 from os import path,并不需要指定包的来源。Python 的依赖管理主要是集中式的,通过 PyPI(Python Package Index)来统一管理和分发包,所以在导入包或模块时,我们并不需要关心其在哪个版本控制系统中。

总的来说,尽管 Python 和 Golang 都使用了 import 语句来导入代码,但这两种语言在导入机制上的差异可能会给人造成困扰。在使用 Python 时,理解 Python 的模块和包的概念,以及其导入规则和使用方式,显然是非常重要的。

(本系列文章将在微信公众号“胡说云原生”连载)