geekdoc-python-zh/docs/pythonland/26.md

227 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Python 包:通过捆绑模块来构建代码
> 原文:[https://python.land/project-structure/python-packages](https://python.land/project-structure/python-packages)
我们使用 Python 包来构建和组织我们的代码。当谈到 Python 包时,人们通常指以下之一:
1. 可以用 pip 和 pipenv 等工具安装的包通常通过 Python 包索引分发。
2. 代码库中的包用于构造和组织代码。
如果你正在寻找如何安装软件包,你应该阅读关于用 [pip install](https://python.land/virtual-environments/installing-packages-with-pip) 安装软件包的文章。另外,我还可以推荐关于[虚拟环境](https://python.land/virtual-environments/virtualenv)的文章。
这篇文章是关于**创建你自己的包和模块**。我们将看看什么是包,它们是如何构造的,以及如何创建 Python 包。您还将发现包和模块如何协同工作来组织和构建您的代码库。
如果您对模块不熟悉,请先阅读我关于 [Python 模块](https://python.land/project-structure/python-modules)的文章,然后再回到这里。这两个主题密切相关。
目录
* [什么是 Python 包?](#What_are_Python_packages "What are Python packages?")
* [Python 包的结构](#Structure_of_a_Python_package "Structure of a Python package")
* [一个示例 Python 包](#An_example_Python_package "An example Python package")
* [在 __init__ 中导入模块。py](#Importing_modules_in_init_py "Importing modules in __init__.py")
* 使用 __main__ 创建一个可运行的包。py
* [模块与软件包](#Modules_vs_Packages "Modules vs. Packages")
## 什么是 Python 包?
Python 包是包含零个或多个 Python 模块的目录。Python 包可以包含子包,子包也是包含模块的目录。每个包总是包含一个名为`__init__.py`的特殊文件。您将了解这个神秘文件的确切用途,以及如何使用它使您的包更容易从导入。
## Python 包的结构
所以 Python 包是一个包含 Python 模块和一个`__init__.py`文件的文件夹。带有两个模块的简单 Python 包的结构如下:
```py
.
└── package_name
├── __init__.py
├── module1.py
└── module2.py
```
如上所述,包可以包含子包。我们可以使用子包来进一步组织我们的代码。我将在下面的一节中更详细地向您展示如何做到这一点。让我们首先来看一下带有子包的包的结构:
```py
.
└── package_name
├── __init__.py
├── subpackage1
├── __init__.py
├── module1.py
└── subpackage2
├── __init__.py
├── module2.py
```
如您所见,包是分层的,就像目录一样。
### 什么是 __init__。Python 包里的 py
`__init__.py`文件是一个特殊的文件,总是在导入包时执行。用`import package_name`从上面导入包时,执行`__init__.py`文件。
从上面导入*嵌套*包时,用`import package_name.subpackage1`执行`package_name`和`subpackage1`的`__init__.py`文件。顺序如下:
1. 首先执行`package_name`的`__init__.py`文件,
2. 然后是`subpackage1`的`__init__.py`文件。
我在所有的`__init__.py`文件中添加了简单的打印语句来演示。我们可以在`package_name`文件夹下创建一个`main.py`文件,内容如下:
```py
import package_name.subpackage1
```
如果我们运行这个程序,输出将是:
```py
$ python3 main.py
Hello from package_name
Hello from subpackage1
```
由于我们的子包的导入,两个`__init__.py`文件中的打印语句都被执行。
### 在 Python 包中组织您的代码
我们现在有了以下工具来正确组织我们的 Python 代码:
* 包装
* 子包
* 模块
您应该使用子包将相关的模块组合在一起。使用子包也有助于保持包和模块名称的简洁。当您发现自己在包名中使用下划线时,它们通常是一个不错的选择。
## 一个示例 Python 包
让我们在当前目录下创建一个名为`httptools`的包。我们的目标是,有一天,这个想象中的包将包含使用 HTTP 协议可能需要的所有工具。不过,我们从简单开始,只有 HTTP GET 和 POST 请求以及一个简单的 HTTP 服务器。
Thank you for reading my tutorials. I write these in my free time, and it requires a lot of time and effort. I use ads to keep writing these *free* articles, I hope you understand! **Support me by disabling your adblocker on my website** or, alternatively, **[buy me some coffee](https://www.buymeacoffee.com/pythonland)**. It's much appreciated and allows me to keep working on this site!
我们的包包含两个子包:`client`和`server`。初始包布局可能如下所示:
```py
httptools/
__init__.py
client/
__init__.py
get.py
post.py
server/
__init__.py
run.py
```
一些需要注意的事项:
* 这些名称简短且具有描述性。因为`client`和`server`是`httptools`的子包,所以大家都很明显这些是一个 HTTP 客户端和服务器。我们不需要称他们为`http_client`和`http_server`。
* 我们将相似的功能分组到子包中:
* 客户端包中的客户端代码,
* 和服务器代码放在服务器包中。
* 我们已经将密切相关的代码分组到模块中。例如,我们需要做的所有 HTTP get 请求都放在`get.py`模块中。
### 扩展我们的 Python 包
现在假设我们想跳到异步编程模型上。虽然我们最初的服务器曾经是同步的,但现在我们也提供了异步版本。
有三种方法可以将它添加到我们的代码库中:
1. 创建一个名为`httptools_async`的新包
2. 在`http`包中创建名为`sync`和`async`的新子包
3. 创建新模块,命名为`async_get`和`async_post`以及 async_run。
你会选择哪个选项?
#### 选项 1:创建新的包
选项 1 是最简单的。我们可以创建一个名为`httptools_async`的新包,并将`http`包的内容复制到其中。接下来,我们将所有代码改为异步。我们原始包的用户可能只需修改一行代码就可以了:将`import http`改为`import http_async`。由于异步编程是一种完全不同的范式,这种简单的交换可能还不够。
#### 选项 2:创建新的子包
选项二意味着现有的库用户,即使是那些不想使用 async 的用户,也需要修改他们的代码。重新开始时,这可能是一个好的选择,但对于现有的库来说,这不是一个好的选择。
#### 选项 3:创建新模块
还有第三种选择:创建新的模块,在模块名后面附加 async。尽管现有用户不需要更改他们的代码但我不推荐这样做。更合理的做法是将异步模块捆绑在它们自己的包中因为你不必在导入时一直重复使用前缀/后缀。它符合保持包名和模块名简洁的总体目标。
## 在 __init__ 中导入模块。py
通过导入`__init__.py`中的重要函数,我们的软件包可以得到更大的改进。`__init__.py`文件通常是导入其他模块的好地方。让我们以上面的`httptools.client`包为例。我们的 get.py 模块提供了一个返回响应的函数`get()`。我们可以将这个函数导入到`client`包的`__init__.py`文件中:
```py
from httptools.client.get import get
```
这是 Python 中的一种常见模式。在`__init__.py`文件中导入所有需要的模块是一个好主意。如果我们使用如上所述的导入,我们包的用户可以像这样导入 get 函数:
```py
from httptools.client import get
```
根据情况,也根据口味,你现在也可以这样做:
```py
from httptools import client
client.get(...)
```
如果没有`__init__.py`中的导入,我们包的用户将需要像我们自己一样使用 get 函数:
```py
from http.client.get import get
```
这对我们来说既冗长又缺乏灵活性。如果我们决定将 get 函数转移到其他文件,这将为我们代码的其他部分或我们包的用户引入一个突破性的变化。因为我们在 __init__ 中导入了函数。但是,我们可以灵活地移动这个函数,只需简单地更改 __init__.py 中的导入,对于我们包的用户来说,这是不会被注意到的。
### 绝对或相对进口
我们已经使用绝对导入从`httptools.client.get`模块中导入了`get()`函数,这意味着我们指定了指向该函数的完整树。也可以使用相对导入。相对导入的好处是可以在不知道完整路径的情况下导入想要使用的模块。在不破坏代码的情况下,包名甚至可以改变。所以相对导入使你的代码对变化更健壮。
`httptools.client`的`__init__.py`文件中的相对导入如下所示:
```py
from .get import get
```
这也是通配符有用的情况之一。我们可以使用通配符导入`httptools.client.get`模块中的所有元素:
```py
from .get import *
```
这并不坏,因为我们确切地知道我们在`get.py`中放了什么。另外,我们可以在`get.py`中改变函数名,而不需要改变 import 语句。这也使得代码更加灵活。
## 使用 __main__ 创建一个可运行的包。py
在模块一课中,我向您展示了如何创建可运行模块。我们可以通过创建一个名为`__main__.py`的文件来对包做类似的事情。
### 如何运行包中的模块
要从包中运行模块,我们可以使用以下 Python 命令:
```py
python -m <module name>
```
此命令可用于运行您的包内的特定模块,例如`python -m mypackage.mymodule`,但我们也可以使用它来运行包。
### 创建可运行的包
要创建一个可运行的包,它需要在其根文件夹中有一个文件名`__main__.py`。这个文件可以导入和引导你喜欢的任何东西。注意,我们不需要添加`if __name__=='__main__'`检查:我们可以 100%确定这个模块的名字实际上是`__main__`,因为我们是这样命名这个文件的。
如果您的模块名为`mymodule`,您现在可以用下面的命令运行它:
```py
python -m mymodule
```
## 模块与软件包
让我们总结一下关于 Python 包的信息以及我们在上一篇关于 Python 模块的文章中学到的知识。经常弹出的一个问题是这样的:模块和包有什么区别?
* 一个模块总是一个文件,而一个包可以包含许多模块。
* 模块用于捆绑 Python 函数、类、常量和任何其他您想重用的东西。反过来,一个包捆绑了模块。
* 模块可以独立存在,不需要成为包的一部分,而包需要模块才有用。
* 包和模块一起构成了组织代码的强大方式。