超简单的Python教程系列——第3篇:项目结构和导入
Posted 飞天程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第3篇:项目结构和导入相关的知识,希望对你有一定的参考价值。
教程最糟糕的部分总是它们的简单性,不是吗?你很少会找到一个包含多个文件的文件,很少会找到包含多个目录的文件。
我发现构建 Python 项目是语言教学中最常被忽视的组成部分之一。更糟糕的是,许多开发人员都弄错了,在一堆常见的错误中跌跌撞撞,直到他们找到至少可以工作的东西。
好消息是:你不必成为其中之一!
在 超简单的Python教程 系列的这一部分中,我们将探索import
语句、模块、包,以及如何将所有内容组合在一起而不费力气。我们甚至会涉及 VCS、PEP 和 the Zen of Python。系好安全带!
设置存储库
在我们深入研究实际的项目结构之前,让我们先来了解一下它是如何融入我们的版本控制系统 [VCS] 的……从你需要VCS 的事实开始!有几个原因...
- 跟踪你所做的每一个更改,
- 弄清楚你什么时候改错了代码,
- 能够查看旧版本的代码,
- 备份你的代码,
- 与他人合作。
你有很多选择。Git是最明显的,特别是如果你不知道还可以使用什么。你可以在 GitHub、GitLab、Bitbucket 或 Gitote 等平台上免费托管你的 Git 存储库。如果你想要 Git 以外的东西,还有很多其他选项,包括 Mercurial、Bazaar、Subversion(尽管如果你使用最后一个,你可能会被同行视为原始人。)
我会悄悄地假设你在本指南的其余部分使用 Git,因为这是我专门使用的。
创建存储库并将本地副本克隆到计算机后,你就可以开始设置项目了。至少,你需要创建以下内容:
-
README.md
:对你的项目及其目标的描述。 -
LICENSE.md
:你的项目的许可证,如果它是开源的。 -
.gitignore
: 一个特殊的文件,告诉 Git 要忽略哪些文件和目录。(如果你使用的是另一个 VCS,则此文件具有不同的名称。) - 带有项目名称的目录。
没错……我们的 Python 代码文件实际上属于一个单独的子目录!这非常重要,因为我们的存储库的根目录将被构建文件、打包脚本、虚拟环境以及所有其他实际上不是源代码一部分的东西弄得乱七八糟。
只是为了举例,我们称我们的虚构项目为awesomething
。
PEP 8 和命名
Python 风格主要由一组称为Python Enhancement Proposals的文档管理,缩写为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
虽然命名确实很冗长,但这是PEP的规范,能够让你的代码更规范。
包和模块
这会让人觉得虎头蛇尾,但这里是那些承诺的定义:
任何 Python( .py
) 文件都是一个模块,一个目录中的一堆模块就是一个包。
你必须对目录执行另一件事以使其成为包,那就是将调用的文件粘贴__init__.py
到其中。你实际上不必将任何内容放入该文件中,但它必须在那里。
你还可以使用其他很酷的东西__init__.py
,但这超出了本指南的范围。
如果你确实忘记__init__.py
了你的包,它会做一些比失败更奇怪的事情,因为这使它成为一个隐式命名空间包。你可以用这种特殊类型的包做一些漂亮的事情,但我不在这里讨论。像往常一样,你可以通过阅读官方文档了解更多信息。
所以,如果我们看一下我们的项目结构,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
你会看到我有一个名为 的顶级包omission
,其中包含四个子包:common
、data
、game
和tests
。我也有目录resources
,但只包含游戏音频、图像等(为简洁起见,此处省略)。resources
不是包,因为它不包含__init__.py
.
我的顶级包中还有另一个特殊文件:__main__.py
. 这是当我们直接通过执行我们的顶级包时运行的文件python -m omission
。我们稍后会讨论其中__main__.py
的内容。
导入的工作原理
如果你以前编写过任何有意义的 Python 代码,那么你几乎可以肯定熟悉该import
语句。例如...
import re
知道当我们导入一个模块时,我们实际上是在运行它是很有帮助的。这意味着import
模块中的任何语句也在运行。
例如,re.py它有几个自己的 import 语句,当我们说import re
. 这并不意味着它们可用于我们从中导入的文件re
,但这确实意味着这些文件必须存在。如果(由于某种不太可能的原因)enum.py
在你的环境中被删除,并且你运行import re
,它将失败并出现错误......
Traceback (most recent call last):
File "weird.py", line 1, in
import re
File "re.py", line 122, in
import enum
ModuleNotFoundError: No module named enum
看到这里,你可能会有些困惑。有人问我为什么外部模块找不到(在本例中为re
)。有些人又想知道为什么要导入内部模块(这儿是enum
),因为他们没有直接在代码中要求它。答案很简单:我们导入re
了 ,而re
导入了enum
。
当然,上面的场景是虚构的:在正常情况import enum
下import re
永远不会失败,因为这两个模块都是 Python 核心库的一部分。
导入注意事项
实际上有多种导入方式,但大多数都应该很少使用,如果曾经使用过的话。
对于下面的所有示例,我们将假设我们有一个名为 的文件smart_door.py
:
# smart_door.py
def close():
print("Ahhhhhhhhhhhh.")
def open():
print("Thank you for making a simple door very happy.")
例如,我们将在 Python 交互式 shell 中运行本节中的其余代码,与smart_door.py
.
如果我们想运行这个函数open()
,我们必须先导入模块smart_door
。最简单的方法是...
import smart_door
smart_door.open()
smart_door.close()
我们实际上会说这smart_door
是and的命名空间。Python 开发人员真的很喜欢命名空间,因为它们让函数和诸如此类的来源变得一目了然。open()
close()
(顺便说一句,不要将命名空间与隐式命名空间包混淆。它们是两个不同的东西。)
Zen of Python,也称为PEP 20,定义了 Python 语言的原理。最后一行有一个声明解决了这个问题:
Namespaces are one honking great idea -- lets do more of those!
然而,在某些时候,命名空间可能会变得很痛苦,尤其是对于嵌套包。foo.bar.baz.whatever.doThing()
只是丑陋。值得庆幸的是,我们有办法避免每次调用函数时都必须使用命名空间。
如果我们希望能够使用该open()
函数而不必经常在其前面加上其模块名称,我们可以这样做...
from smart_door import open
open()
但是请注意,在最后一种情况下,两者close()
都smart_door.close()
不会起作用,因为我们没有直接导入函数。要使用它,我们必须将代码更改为...
from smart_door import open, close
open()
close()
在之前那个可怕的嵌套包噩梦中,我们现在可以说from foo.bar.baz.whatever import doThing
,然后直接使用doThing()
。或者,如果我们想要一点命名空间,我们可以说from foo.bar.baz import whatever
,然后说whatever.doThing()
。
这样的import
系统非常灵活。
不过,不久之后,你可能会发现说“但我的模块中有数百个函数,我想全部使用它们!” 这是许多开发人员偏离轨道的地方,通过这样做......
from smart_door import *
这是非常非常糟糕的!简单来说就是直接导入模块中的所有东西,这是个问题。想象一下下面的代码......
from smart_door import *
from gzip import *
open()
你认为会发生什么?答案是,gzip.open()
将是被调用的函数,因为这open()
是在我们的代码中导入并定义的最后一个版本。smart_door.open()
已被覆盖- 我们不能将其称为open()
,这意味着我们实际上根本无法调用它。
当然,由于我们通常不知道,或者至少不记得,每个被导入的模块中的每个函数、类和变量,我们很容易陷入一大堆混乱。
Zen of Python解决了这种情况......
显式优于隐式。
你永远不必猜测函数或变量的来源。文件中的某处应该是明确告诉我们它来自哪里的代码。前两个场景证明了这一点。
我还应该提到,早期的foo.bar.baz.whatever.doThing()
场景是 Python 开发人员不喜欢看到的。同样来自Zen of 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
在我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
这称为绝对导入。它从顶级包开始omission
,然后向下进入common
包,在其中查找game_enums.py
.
一些开发人员带着类似的 import 语句来找我from common.game_enums import GameMode
,想知道为什么它不起作用。简单地说,data
包(所在的game_round_settings.py
地方)不知道它的兄弟包。
然而,它确实知道它的父级。正因为如此,Python 有一种叫做相对导入的东西,它可以让我们做同样的事情......
from ..common.game_enums import GameMode
意思是“这个..
包的直接父包”,在这种情况下是omission
. 因此,导入后退一级,走进common
,并找到game_enums.py
。
关于是使用绝对导入还是相对导入存在很多争论。就个人而言,我更喜欢尽可能使用绝对导入,因为它使代码更具可读性。但是,你可以自己决定。唯一重要的部分是结果是显而易见的——任何东西的来源都不应该是神秘的。
这里还有另一个潜伏的陷阱!在omission/data/settings.py
,我有这一行:
from omission.data.game_round_settings import GameRoundSettings
当然,由于这两个模块都在同一个包中,我们应该可以说from game_round_settings import GameRoundSettings
,对吧?
这是错误的!它实际上将无法找到game_round_settings.py
. 这是因为我们正在运行顶级包omission
,这意味着搜索路径(Python 查找模块的位置,以及以什么顺序)的工作方式不同。
但是,我们可以使用相对导入:
from .game_round_settings import GameRoundSettings
在这种情况下,single 的.
意思是“这个包”。
如果你熟悉经典的 Linux 文件系统,那么这应该开始有意义了。..
意思是“后一级”,.
意思是“当前位置”。当然,Python 更进一步:...
意思是“后两级”,....
是“后三级”,等等。
但是,请记住,这些“级别”不仅仅是简单的目录,在这里。它们是包裹。如果n你在一个不是包的普通目录中有两个不同的包,则不能使用相对导入从一个跳转到另一个。为此,你必须使用 Python 搜索路径,这超出了本指南的范围。
__main__.py
还记得我提到__main__.py
在我们的顶级包中创建一个吗?那是一个特殊的文件,当我们直接用 Python 运行包时会执行它。我的omission
包可以从我的存储库的根目录运行python -m omission
。
这是该文件的内容:
from omission import app
if __name__ == __main__:
app.run()
是的,实际上就是这样!我正在app
从顶级 package导入我的模块omission
。
请记住,我也可以说from . import app
。或者,如果我只想说run()
而不是app.run()
,我可以做from omission.app import run
or from .app import run
。最后,只要代码可读,我如何进行导入并没有太大的技术差异。
(PS:我们可以讨论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()
然后我可以直接通过 执行该模块python -m omission.app
,结果与python -m omission
一样. 现在__main__.py
被完全忽略,而omission/app.py
的__name__
是"__main__.py"
。
同样的,如果我只是运行python -m omission
,app.py
则忽略其中的特殊代码,因为它的__name__
现在是omission.app
。
总结
让我们回顾一下:
- 每个项目都应该使用 VCS,例如 Git。有很多选择可供选择。
- 每个 Python 代码文件 (
.py
) 都是一个模块。 - 将你的模块组成包。每个包必须包含一个特殊
__init__.py
文件。 - 你的项目通常应该由一个顶级包组成,通常包含子包。该顶级包通常与你的项目共享名称,并作为目录存在于项目存储库的根目录中。
- 永远不要
*
在导入语句中使用。在你接受一个可能的例外之前,Zen of Python指出“其他情况不足以违反这个规则”。 - 使用绝对或相对导入来引用项目中的其他模块。
- 可执行项目应该
__main__.py
在顶级包中有一个。然后,你可以直接使用python -m myproject
.
当然,我们可以在构建 Python 项目时使用更多高级概念和技巧,但我们不会在这里讨论。
以上是关于超简单的Python教程系列——第3篇:项目结构和导入的主要内容,如果未能解决你的问题,请参考以下文章