Python 包结构 非常简单的 Python:项目结构和导入 设置存储库 PEP 8 和命名包和模块 导入的工作原理 导入的注意事项 在项目内导入 __main__.py 总结 选项 1:本机脚本 选项 2:Python 脚本

2025-05-24

Python 包结构非常简单的 Python:项目结构和导入

设置存储库

PEP 8 和命名

包和模块

导入工作原理

进口注意事项

在您的项目中导入

__main__.py

总结

选项 1:原生脚本

选项 2:Python 脚本

喜欢这些文章吗?那就买本书吧! Jason C. McDonald 的《Dead Simple Python》现已由 No Starch Press 出版。


教程最糟糕的地方总是过于简单,不是吗?你很少会找到一个包含多个文件的教程,更别提包含多个目录的教程了。

我发现,构建 Python 项目是 Python 教学中最常被忽视的部分之一。更糟糕的是,许多开发人员犯了错,在一堆常见错误中摸索,最终找到一个至少能用得上的方案。

好消息是:您不必成为他们中的一员!

在“极简 Python”系列的这一期中,我们将探索import语句、模块、包,以及如何轻松整合所有内容。我们甚至会涉及 VCS、PEP 和 Python 之禅。系好安全带!

设置存储库

在深入探讨实际项目结构之前,我们先来聊聊它如何与我们的版本控制系统 (VCS) 相契合……首先,你需要一个 VCS!原因如下……

  • 追踪你所做的每一个改变,
  • 弄清楚你什么时候弄坏了什么东西,
  • 能够查看代码的旧版本,
  • 备份你的代码,以及
  • 与他人合作。

你有很多选择。Git最显而易见的选择,尤其是在你不知道还有什么好用的时候。你可以在 GitHub、GitLab、Bitbucket 或 Gitote 等平台上免费托管你的 Git 仓库。如果你不想使用 Git,还有许多其他选择,包括 Mercurial、Bazaar 和 Subversion(不过,如果你使用 Subversion,你的同行可能会觉得你过时了)。

我将默认您在本指南的其余部分使用 Git,因为这是我专门使用的。

创建仓库并将本地副本克隆到计算机后,即可开始设置项目。至少,您需要创建以下内容:

  • README.md:您的项目及其目标的描述。
  • LICENSE.md:如果您的项目是开源的,则需要提供其许可证。(有关如何选择许可证的更多信息,请参阅opensource.org。)
  • .gitignore:一个特殊文件,用于告诉 Git 哪些文件和目录需要忽略。(如果您使用的是其他版本控制系统,则此文件的名称可能有所不同。请自行查找。)
  • 包含您的项目名称的目录。

没错……我们的 Python 代码文件实际上应该放在一个单独的子目录中!这一点非常重要,因为我们的仓库根目录会堆满构建文件、打包脚本、虚拟环境以及各种其他不属于源代码的内容,变得非常杂乱。

仅为了举例,我们将我们的虚构项目称为awesomething

PEP 8 和命名

Python 的风格主要由一组名为“Python 增强提案”(简称PEP)的文档所规范。当然,并非所有 PEP 都会被采纳——这也是它们被称为“提案”的原因——但有些 PEP 确实被采纳了。您可以在 Python 官方网站上浏览 PEP 主索引。该索引的正式名称是PEP 0

目前,我们主要关注的是PEP 8,它最初由 Python 语言创始人 Guido van Rossum 于 2001 年撰写。它是一份官方文档,概述了所有 Python 开发人员通常应遵循的编码风格。请把它放在你的枕头下!学习它,遵循它,并鼓励其他人也这样做。

(附注:PEP 8 指出样式规则总有例外。它是一个指南,而不是命令。)

现在,我们主要关注题为“包和模块名称”的部分......

模块名称应简短,且全部小写。如果下划线可以提高可读性,模块名称中可以使用下划线。Python 包名称也应简短,且全部小写,但不建议使用下划线。

我们稍后会了解模块包到底是什么,但是现在,请理解模块是通过文件名命名的,而包是通过其目录名命名的

换句话说,文件名应该全部小写,如果为了提高可读性,可以添加下划线。同样,目录名也应该全部小写,如果可以避免,可以省略下划线。换句话说……

  • 这样做:awesomething/data/load_settings.py
  • 不是这个:awesomething/Data/LoadSettings.py

我知道,我知道,用这么长的方式来表达观点,但至少我给你打了个招呼。(你好?这个东西开着吗?

包和模块

这会让人感觉有点虎头蛇尾,但以下是承诺的定义:

任何 Python ( .py) 文件都是一个模块,目录中的一堆模块就是一个

嗯……差不多了。要把一个目录变成包,你还需要做一件事,那就是把一个叫 的文件粘贴__init__.py到里面。实际上,你不需要往里面放任何东西。它只要在那里就行了。

您还可以做其他很酷的事情__init__.py,但这超出了本指南的范围,因此请阅读文档以了解更多信息

如果你真的忘记了__init__.py在包中添加命名空间,它将会做出比失败更奇怪的事情,因为这会让它成为一个隐式命名空间包。你可以用这种特殊类型的包做一些巧妙的事情,但我在这里就不展开了。像往常一样,你可以通过阅读文档了解更多信息:PEP 420:隐式命名空间包

所以,如果我们看一下我们的项目结构,awesomething它实际上是一个包,它可以包含其他包。因此,我们可以将顶层包awesomething称为,并将其下的所有包称为子包。这在我们导入内容时非常重要。

让我们看一下我的真实世界项目的快照,omission以了解我们如何构建东西......

omission-git
├── LICENSE.md
├── omission
│   ├── app.py
│   ├── common
│   │   ├── classproperty.py
│   │   ├── constants.py
│   │   ├── game_enums.py
│   │   └── __init__.py
│   ├── data
│   │   ├── data_loader.py
│   │   ├── game_round_settings.py
│   │   ├── __init__.py
│   │   ├── scoreboard.py
│   │   └── settings.py
│   ├── game
│   │   ├── content_loader.py
│   │   ├── game_item.py
│   │   ├── game_round.py
│   │   ├── __init__.py
│   │   └── timer.py
│   ├── __init__.py
│   ├── __main__.py
│   ├── resources
│   └── tests
│       ├── __init__.py
│       ├── test_game_item.py
│       ├── test_game_round_settings.py
│       ├── test_scoreboard.py
│       ├── test_settings.py
│       ├── test_test.py
│       └── test_timer.py
├── pylintrc
├── README.md
└── .gitignore
Enter fullscreen mode Exit fullscreen mode

(如果您好奇的话,我使用 UNIX 程序tree制作了上面的小图。)

您会看到我有一个名为的顶级包omission,它有四个子包:common、、。我还有目录,但其中仅包含游戏音频、图像等(为简洁起见,此处省略)。data不是一个包,因为它不包含gametestsresourcesresources__init__.py

我的顶级包中还有另一个特殊文件:__main__.py。当我们通过 直接执行顶级包时,会运行这个文件。我们稍后python -m omission会讨论它里面的内容。__main__.py

导入工作原理

如果你之前写过任何有意义的 Python 代码,你肯定对这个import语句很熟悉。例如……

import re
Enter fullscreen mode Exit fullscreen mode

了解这一点很有帮助,当我们导入一个模块时,我们实际上正在运行它。这意味着import模块中的任何语句也都在运行。

例如,re.py它本身就有几个 import 语句,当我们说 时就会执行import re。这并不意味着它们对我们导入re 的 的文件可用,但这确实意味着这些文件必须存在。如果(由于某些不太可能的原因)enum.py在你的环境中被删除了,而你又运行import re,它就会失败并出现错误……

回溯(最近一次调用最后一次):
文件“weird.py”,第 1 行,在
导入 re
文件“re.py”,第 122 行,在
导入 enum
ModuleNotFoundError:没有名为“enum”的模块

读到这里,你可能会感到有些困惑。有人问我,为什么re找不到外部模块(本例中为 )。还有人想知道,enum既然他们并没有在代码中直接请求导入内部模块(此处),为什么还要导入它呢?答案很简单:我们导入了re,而它又导入了enum

当然,上述场景是虚构的:import enumimport re在正常情况下永远不会失败,因为这两个模块都是 Python 核心库的一部分。这只是一个比较简单的示例。;)

进口注意事项

实际上,导入的方法有很多种,但大多数方法很少使用,甚至根本不使用。

对于下面的所有示例,我们假设有一个名为的文件smart_door.py

# smart_door.py
def close():
    print("Ahhhhhhhhhhhh.")

def open():
    print("Thank you for making a simple door very happy.")
Enter fullscreen mode Exit fullscreen mode

仅举例来说,我们将在 Python 交互式 shell 中从与 相同的目录运行本节中的其余代码smart_door.py

如果我们要运行该函数open(),必须先导入该模块smart_door。最简单的方法是……

import smart_door
smart_door.open()
smart_door.close()
Enter fullscreen mode Exit fullscreen mode

我们实际上会说那smart_door命名空间。Python 开发人员非常喜欢命名空间,因为它们可以清楚地显示函数和其他东西的来源。open()close()

(顺便说一句,不要将namespaceimplicit namespace package混淆。它们是两个不同的东西。)

Python 之禅也称为PEP 20)定义了 Python 语言背后的哲学。其中最后一行有一句话阐述了这一点:

命名空间是一个非常棒的主意——让我们多做一些这样的事情!

然而,到了一定程度,命名空间可能会变得很麻烦,尤其是在嵌套包的情况下。foo.bar.baz.whatever.doThing()这简直太丑了。还好,我们确实有办法避免每次调用函数时都必须使用命名空间。

如果我们希望能够使用该open()函数而不必在其前面不断加上模块名称,我们可以这样做......

from smart_door import open
open()
Enter fullscreen mode Exit fullscreen mode

不过请注意,在最后一种情况下, close()norsmart_door.close()函数不起作用,因为我们没有直接导入该函数。要使用它,我们必须将代码改成这样……

from smart_door import open, close
open()
close()
Enter fullscreen mode Exit fullscreen mode

在之前那个可怕的嵌套包噩梦中,我们现在可以说from foo.bar.baz.whatever import doThing,然后doThing()直接使用。或者,如果我们想要一点点命名空间,我们可以说from foo.bar.baz import whatever,然后说whatever.doThing()

import系统非常灵活。

不过,不久之后,你可能会发现自己说:“但是我的模块中有数百个函数,我想全部使用它们!”很多开发人员正是在这个时候偏离了轨道,这样做……

from smart_door import *
Enter fullscreen mode Exit fullscreen mode

这太糟糕了!简而言之,它直接导入了模块里的所有内容,这本身就有问题。想象一下下面的代码……

from smart_door import *
from gzip import *
open()
Enter fullscreen mode Exit fullscreen mode

你认为会发生什么?答案是,gzip.open()将是被调用的函数,因为这是open()我们代码中导入的最后一个版本,因此也是定义的。smart_door.open()已经被遮蔽了——我们不能将它调用为open(),这意味着我们实际上根本不能调用它。

当然,由于我们通常不知道,或者至少不记得每个导入的模块中的每个函数、类和变量,因此我们很容易陷入一大堆混乱。

Python 之禅解决了这种情况......

明确优于隐含。

你永远不需要猜测函数或变量的来源。文件中的某个地方应该有明确说明其来源的代码。前两个场景就证明了这一点。

我还应该提一下,之前foo.bar.baz.whatever.doThing()提到的情况是 Python 开发者们最不愿意看到的。同样出自《Python 之禅》 ……

扁平比嵌套更好。

一些包的嵌套是可以的,但是当你的项目开始看起来像一堆精致的俄罗斯套娃时,你就做错了。将模块组织到包中,但要保持合理的简洁。

在您的项目中导入

我们之前创建的项目文件结构即将派上用场。回想一下我的omission项目……

omission-git
├── LICENSE.md
├── omission
│   ├── app.py
│   ├── common
│   │   ├── classproperty.py
│   │   ├── constants.py
│   │   ├── game_enums.py
│   │   └── __init__.py
│   ├── data
│   │   ├── data_loader.py
│   │   ├── game_round_settings.py
│   │   ├── __init__.py
│   │   ├── scoreboard.py
│   │   └── settings.py
│   ├── game
│   │   ├── content_loader.py
│   │   ├── game_item.py
│   │   ├── game_round.py
│   │   ├── __init__.py
│   │   └── timer.py
│   ├── __init__.py
│   ├── __main__.py
│   ├── resources
│   └── tests
│       ├── __init__.py
│       ├── test_game_item.py
│       ├── test_game_round_settings.py
│       ├── test_scoreboard.py
│       ├── test_settings.py
│       ├── test_test.py
│       └── test_timer.py
├── pylintrc
├── README.md
└── .gitignore
Enter fullscreen mode Exit fullscreen mode

在我的game_round_settings模块(由 定义)中omission/data/game_round_settings.py,我想使用我的GameMode类。该类是在 中定义的omission/common/game_enums.py。我该如何获取它?

因为我将其定义omission为一个包,并将模块组织到子包中,所以这实际上非常简单。在 中game_round_settings.py,我说……

from omission.common.game_enums import GameMode
Enter fullscreen mode Exit fullscreen mode

这称为绝对导入。它从顶级包开始,omission然后向下进入common包,在那里查找game_enums.py

有些开发者向我咨询类似这样的 import 语句from common.game_enums import GameMode,并想知道为什么它不起作用。简而言之,data包(所在的game_round_settings.py包)不知道它的兄弟包。

然而,它确实知道它的父级。因此,Python 提供了一个名为“相对导入”的功能,让我们可以像这样做同样的事情……

from ..common.game_enums import GameMode
Enter fullscreen mode Exit fullscreen mode

的意思..是“这个包的直接父包”,在本例中是omission。因此,导入操作后退一级,向下进入common,然后找到game_enums.py

关于使用绝对导入还是相对导入,存在很多争议。我个人倾向于尽可能使用绝对导入,因为它能显著提升代码的可读性。不过,您可以自行决定。唯一重要的是,结果必须显而易见——任何内容的来源都不应令人感到神秘。

(继续阅读:真正的 Python - Python 中的绝对导入与相对导入

这里还有一个隐藏的陷阱!在 中omission/data/settings.py,我有这样一行:

from omission.data.game_round_settings import GameRoundSettings
Enter fullscreen mode Exit fullscreen mode

当然,由于这两个模块都在同一个包中,我们应该能够说from game_round_settings import GameRoundSettings,对吗?

错了!它实际上会找不到game_round_settings.py。这是因为我们正在运行顶级包omission,这意味着搜索路径(Python 查找模块的位置和顺序)的工作方式有所不同。

但是,我们可以使用相对导入来代替:

from .game_round_settings import GameRoundSettings
Enter fullscreen mode Exit fullscreen mode

在这种情况下,单曲的.意思是“这个包裹”。

如果你熟悉典型的 UNIX 文件系统,那么你应该能理解这一点。..表示“后退一级”,.表示“当前位置”。当然,Python 更进一步:...表示“后退两级”,....表示“后退三级”,等等。

但是,请记住,这些“层级”并非仅仅是普通的目录,而是包。如果在一个并非包的普通目录中有两个不同的包,则无法使用相对导入从一个包跳转到另一个包。您必须使用 Python 搜索路径来实现这一点,但这超出了本指南的讨论范围。(请参阅本文末尾的文档。)

__main__.py

还记得我提到过__main__.py在顶级包中创建一个 吗?这是一个特殊的文件,当我们直接用 Python 运行包时会执行它。我的omission包可以从我的仓库根目录运行python -m omission

以下是该文件的内容:

from omission import app

if __name__ == '__main__':
    app.run()
Enter fullscreen mode Exit fullscreen mode

是的,就是这样!我正在app从顶级包中导入我的模块omission

记住,我也可以说成from . import appinstead。或者,如果我只想说run()instead of app.run(),我也可以写成from omission.app import runor from .app import run。最终,只要代码可读性好,我如何导入并没有太大的技术区别。

(旁注:我们可以争论一下将app.py我的主run()功能分开是否合乎逻辑,但我有我的理由……而且它们超出了本指南的范围。)

一开始让大多数人感到困惑的是整个语句if __name__ == '__main__'。Python 没有太多样板代码——这些代码几乎不需要修改就能通用——但这是其中罕见的部分之一。

__name__是每个 Python 模块的一个特殊字符串属性。如果我把这行代码print(__name__)放在 的顶部omission/data/settings.py,当该模块被导入(并运行)时,我们就会看到打印出“omission.data.settings”。

当通过直接运行模块时python -m some_module,该模块会被分配一个特殊值__name__:“ main ”。

因此,实际上是在检查模块是否作为if __name__ == '__main__':模块执行。如果是,则根据条件运行代码。

app.py你可以通过另一种方式来查看它的实际效果。如果我将以下内容添加到...的底部

if __name__ == '__main__':
    run()
Enter fullscreen mode Exit fullscreen mode

...然后我可以直接通过 执行该模块python -m omission.app,结果与 相同python -m omission。现在__main__.py被完全忽略,并且__name__omission/app.py"__main__.py"

同时,如果我只是运行python -m omission,那么中的特殊代码app.py将被忽略,因为它__name__现在omission.app又是。

看看它是如何工作的?

总结

我们来回顾一下。

  • 每个项目都应该使用一个版本控制系统 (VCS),例如 Git。有很多选择。

  • 每个 Python 代码文件(.py)都是一个模块

  • 将模块组织到中。每个包必须包含一个特殊__init__.py文件。

  • 您的项目通常应包含一个顶级包,该包通常包含子包。该顶级包通常与您的项目同名,并以目录的形式存在于项目仓库的根目录中。

  • 永远不要在 import 语句中使用*。在考虑可能的异常之前,Python 之禅指出“特殊情况不足以打破规则”。

  • 使用绝对或相对导入来引用项目中的其他模块。

  • 可执行项目应该__main__.py在顶级包中有一个 。然后,你可以直接使用 执行该包python -m myproject

当然,在构建 Python 项目时,我们还可以运用很多更高级的概念和技巧,但我们不会在这里讨论。我强烈建议您阅读以下文档:


感谢(Freenode IRC )、grym@ cbrintnall@rhymes (Dev) 提出的修改建议。deniska#python

文章来源:https://dev.to/codemouse92/dead-simple-python-project-struct-and-imports-38c6
PREV
极简 Python:虚拟环境与 pip 为何值得关注?获取工具 创建虚拟环境 激活虚拟环境 pip 介绍 退出虚拟环境 虚拟环境与 Git 的全过程 一些额外的技巧 总结
NEXT
极简 Python:类 类在会话中 声明方法 类与静态方法 初始化器和构造函数 变量作用域:私有和公共属性 继承 保留类!复习