Python 编程学习指南第四版(六)
对于使用 src 布局的项目,Setuptools 通常可以自动确定要包含在分发中的 Python 模块。对于平面布局,如果项目文件夹中只有一个可导入的包,则自动发现才会工作。如果存在任何其他 Python 文件,您将需要通过文件中的表配置要包含或排除的包和模块。如果您使用 src 布局,只有当您为包含代码的文件夹使用除src之外的其他名称,或者当src文件夹中有一些模块需要从分发中排除时,您才需
原文:
zh.annas-archive.org/md5/9c28dd95f85acb0e4f19ad9c6a2a5fb1
译者:飞龙
第十六章:打包 Python 应用程序
“你总共有任何奶酪吗?”“没有。”
– 蒙提·派森,"奶酪店"草图
在本章中,我们将学习如何为 Python 项目创建一个可安装的包,并将其发布供他人使用。
有许多原因说明你应该发布你的代码。在第一章,Python 的温和介绍中,我们提到 Python 的一个好处是拥有庞大的第三方包生态系统,你可以使用pip
免费安装这些包。其中大部分都是由像你一样的开发者创建的,通过贡献你自己的项目,你将帮助确保社区持续繁荣。从长远来看,这也有助于改进你的代码,因为将其暴露给更多用户意味着错误可能会更快被发现。最后,如果你试图成为一名软件开发人员,能够指向你参与过的项目将非常有帮助。
了解打包的最佳方式是通过创建包并发布的过程。这正是本章我们将要做的。我们将要工作的项目是在第十五章,CLI 应用程序中开发的铁路命令行界面(CLI)应用程序。
在本章中,我们将学习以下内容:
-
如何为你的项目创建一个发行版包
-
如何发布你的包
-
打包的不同工具
在我们开始打包和发布我们的项目之前,我们将简要介绍Python 包索引(PyPI)以及 Python 打包的一些重要术语。
Python 包索引
PyPI是一个在线的 Python 包仓库,托管在pypi.org
。它有一个网页界面,可以用来浏览或搜索包,并查看它们的详细信息。它还提供了 API,如pip
,用于查找和下载安装包。PyPI 对任何人开放,可以免费注册并免费分发他们的项目。任何人也可以免费从 PyPI 安装任何包。
仓库被组织成项目、发布和发行包。一个项目是一个带有其相关数据或资源的库、脚本或应用程序。例如,FastAPI、Requests、pandas、SQLAlchemy以及我们的 Railway CLI 应用程序都是项目。pip
本身也是一个项目。发布是项目的特定版本(或时间点的快照)。发布通过版本号来标识。例如,pip
的24.2版本是pip
项目的发布。发布以发行包的形式分发。这些是带有发布版本的归档文件,包含构成发布的 Python 模块、数据文件等。发行包还包含有关项目和发布的项目元数据,例如项目名称、作者、发布版本以及需要安装的依赖项。发行包也被称为distributions或简称为packages。
在 Python 中,单词package也用来指代一个可导入的模块,它可以包含其他模块,通常是以包含一个__init__.py
文件的文件夹形式存在。重要的是不要将这种可导入的包与发行包混淆。在本章中,我们将主要使用术语package来指代发行包。在存在歧义的情况下,我们将使用术语importable package或distribution package。
发行包可以是源发行包(也称为sdists),在安装之前需要构建步骤,或者构建发行包,在安装过程中只需将归档内容移动到正确的位置。源发行包的当前格式最初由 PEP 517 定义。正式规范可以在packaging.python.org/specifications/source-distribution-format/
找到。标准的构建发行包格式称为wheel,最初在 PEP 427 中定义。当前版本的 wheel 规范可以在packaging.python.org/specifications/binary-distribution-format/
找到。wheel 格式取代了(现在已弃用的)egg构建发行包格式。
PyPI 最初被昵称为 Cheese Shop,这个名字来源于我们本章开头引用的著名蒙提·派森素描。因此,轮分布格式并不是以汽车轮子命名,而是以奶酪轮子命名。
为了帮助理解所有这些,让我们快速通过一个例子来看看运行pip install
时会发生什么。我们将使用pip
安装requests
项目的发布版2.32.3。为了让我们看到pip
正在做什么,我们将使用三次-v
命令行选项,以使输出尽可能详细。我们还将添加--no-cache
命令行选项,强制pip
从 PyPI 下载包,而不是使用任何可能本地缓存的包。输出看起来像这样(请注意,我们已经将输出裁剪以适应页面,并省略了多行;你可以在本章源代码文件pip_install.txt
中找到完整的输出):
$ pip install -vvv --no-cache requests==2.32.3
Using pip 24.2 from ... (python 3.12)
...
1 location(s) to search for versions of requests:
* https://pypi.org/simple/requests/
Pip
告诉我们,它已经在pypi.org/simple/requests/
找到了关于requests
项目的信息。输出接着列出了requests
项目的所有可用发行版:
Found link https://.../requests-0.2.0.tar.gz..., version: 0.2.0
...
Found link https://.../requests-2.32.3-py3-none-any.whl...
(requires-python:>=3.8), version: 2.32.3
Found link https://.../requests-2.32.3.tar.gz...
(requires-python:>=3.8), version: 2.32.3
现在,pip
收集我们请求的发布版的发行版。它下载 wheel 发行版的元数据:
Collecting requests==2.32.3
...
Downloading requests-2.32.3-py3-none-any.whl.metadata (4.6 kB)
接下来,pip
从包元数据中提取依赖项列表,并继续以相同的方式查找和收集它们的元数据。一旦找到所有必需的包,就可以下载并安装:
Downloading requests-2.32.3-py3-none-any.whl (64 kB)
...
Installing collected packages: urllib3, ..., certifi, requests
...
Successfully installed ... requests-2.32.3 ...
如果pip
为任何包下载了源发行版(如果没有合适的 wheel 可用,可能会发生这种情况),在安装之前需要构建该包。
现在我们知道了项目、发布版和包之间的区别,我们可以开始准备发布版,并为铁路 CLI 应用程序构建发行版包。
使用 Setuptools 进行打包
我们将使用Setuptools库来打包我们的项目。Setuptools 是 Python 最古老的活跃开发打包工具,并且仍然是最受欢迎的。它是原始标准库distutils
打包系统的扩展。distutils
模块在 Python 3.10 中被弃用,并在 Python 3.12 中从标准库中移除。
在本节中,我们将探讨如何设置我们的项目以使用 Setuptools 构建包。
项目布局
在 Python 项目中布局文件有两种流行的约定:
-
在src 布局中,需要分发的可导入包放置在主项目文件夹内的
src
文件夹中。 -
在扁平布局中,可导入的包直接放置在顶层项目文件夹中。
src
布局的优势在于明确指出哪些文件将被包含在发行版中。这减少了意外包含其他文件的可能性,例如仅用于开发期间使用的脚本。然而,src
布局在开发期间可能不太方便,因为在不首先在虚拟环境中安装发行版的情况下,无法从顶层项目文件夹中的脚本或 Python 控制台导入包。
支持 src
布局的倡导者认为,在开发过程中被迫安装发行版包实际上是一种好处,因为它增加了在开发过程中发现创建发行版包错误的可能性。
对于这个项目,我们选择使用 src
布局:
$ tree -a railway-project
railway-project
├── .flake8
├── CHANGELOG.md
├── LICENSE
├── README.md
├── pyproject.toml
├── src
│ └── railway_cli
│ ├── __init__.py
│ └── ...
└── test
├── __init__.py
└── test_get_station.py
src/railway_cli
文件夹包含了 railway_cli
可导入包的代码。我们还在 test
文件夹中添加了一些测试,作为您扩展的示例。.flake8
文件包含了 flake8
的配置,flake8
是一个 Python 代码风格检查器,可以帮助指出我们代码中的 PEP 8 违规。
我们将在下面更详细地描述剩余的每个文件。在此之前,让我们看看如何在开发过程中在虚拟环境中安装项目。
开发安装
与 src
布局一起工作的最常见方法是使用 开发安装,也称为 开发模式,或 可编辑安装。这将构建并安装项目的 wheel。然而,它不会将代码复制到虚拟环境中,而是在虚拟环境中添加一个指向项目文件夹中源代码的链接。这允许您像安装一样导入和运行代码,但您对代码所做的任何更改都将生效,而无需重新构建和重新安装。
让我们现在尝试一下。打开一个控制台,转到本章的源代码文件夹,创建一个新的虚拟环境,激活它,并运行以下命令:
$ pip install -e railway-project
Obtaining file:///.../ch16/railway-project
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable metadata (pyproject.toml) ... done
...
Building wheels for collected packages: railway-cli
Building editable for railway-cli (pyproject.toml) ... done
Created wheel for railway-cli: filename=...
...
Successfully built railway-cli
Installing collected packages: ... railway-cli
Successfully installed ... railway-cli-0.0.1 ...
如输出所示,当我们使用 -e
(或 --editable
)选项运行 pip install
并给它项目文件夹的路径时,它会构建一个可编辑的 wheel 并将其安装在虚拟环境中。
现在,如果您运行:
$ python -m railway_cli
它应该像上一章中那样工作。您可以通过在代码中更改某些内容(例如,添加一个 print()
语句)并再次运行来验证我们确实有一个可编辑的安装。
现在我们已经有一个可工作的可编辑项目安装,让我们更详细地讨论项目文件夹中的每个文件。
更新日志
虽然这不是必需的,但包含一个变更日志文件与您的项目一起被认为是良好的实践。此文件总结了项目每个版本中进行的更改。变更日志对于通知您的用户新功能或他们需要了解的软件行为变化非常有用。
我们的更新日志文件名为 CHANGELOG.md
,并使用 Markdown 格式编写。
许可证
您应该包含一个定义您的代码分发条款的许可证。有许多软件许可证可供选择。如果您不确定选择哪个,choosealicense.com/
网站是一个有用的资源,可以帮助您。然而,如果您对法律后果有任何疑问,或需要建议,您应该咨询法律专业人士。
我们在 MIT 许可证下分发我们的铁路 CLI 项目。这是一个简单的许可证,允许任何人使用、分发或修改代码,只要他们包含我们的原始版权声明和许可证。
按照惯例,许可证包含在一个名为LICENSE
或LICENSE.txt
的文本文件中,尽管一些项目也使用其他名称,例如COPYING
。
README
您的项目还应包含一个README文件,描述项目内容、项目存在的原因以及一些基本的使用说明。该文件可以是纯文本格式,也可以使用如reStructuredText或Markdown这样的标记语法。如果是一个纯文本文件,通常命名为README
或README.txt
,如果是 reStructuredText,则命名为README.rst
,如果是 Markdown,则命名为README.md
。
我们的README.md
文件包含一个简短的段落,描述了项目的目的和一些简单的使用说明。
Markdown 和 reStructuredText 是广泛使用的标记语言,旨在易于以原始形式阅读或编写,但也可以轻松转换为 HTML 以创建简单的网页。您可以在daringfireball.net/projects/markdown/
和docutils.sourceforge.io/rst.html
上了解更多关于它们的信息。
pyproject.toml
该文件由 PEP 518(peps.python.org/pep-0518/
)引入并由 PEP 517(peps.python.org/pep-0517/
)扩展。这些 PEP 的目标是定义标准,以便项目可以指定它们的构建依赖项以及用于构建它们的包的构建工具。对于使用 Setuptools 的项目,这看起来是这样的:
# railway-project/pyproject.toml
[build-system]
requires = ["setuptools>=66.1.0", "wheel"]
build-backend = "setuptools.build_meta"
在这里,我们指定了至少需要 Setuptools 的 66.1.0 版本(这是与 Python 3.12 兼容的最旧版本)以及wheel
项目的任何版本,wheel
项目是轮分布格式的参考实现。请注意,这里的requires
字段不列出运行我们的代码的依赖项,只列出构建分发包的依赖项。我们将在本章后面讨论如何指定运行我们的项目的依赖项。
build-backend
字段指定了将用于构建包的 Python 对象。对于 Setuptools,这是setuptools
(可导入)包中的build_meta
模块。
PEP 518 还允许您在pyproject.toml
文件中放置其他开发工具的配置。当然,相关的工具也需要支持从该文件中读取它们的配置:
# railway-project/pyproject.toml
[tool.black]
line-length = 66
[tool.isort]
profile = 'black'
line_length = 66
我们在我们的pyproject.toml
文件中添加了black(一个流行的代码格式化工具)和isort(一个按字母顺序排序导入的工具)的配置。我们已将这两个工具配置为使用 66 个字符的行长度,以确保我们的代码可以适应书页。我们还配置了isort
以与black
保持兼容。
你可以在它们的网站上了解更多关于 black
和 isort
的信息:black.readthedocs.io/
和 pycqa.github.io/isort
。
PEP 621 (peps.python.org/pep-0621/
) 引入了在 pyproject.toml
文件中指定所有项目元数据的能力。这自 Setuptools 61.0.0 版本以来一直得到支持。我们将在下一节中详细探讨这一点。
包元数据
项目元数据定义在 pyproject.toml
文件中的 project
表格中。让我们一次查看几个条目:
# railway-project/pyproject.toml
[project]
name = "railway-cli"
authors = [
{name="Heinrich Kruger", email="heinrich@example.com"},
{name="Fabrizio Romano", email="fabrizio@example.com"},
]
表格从 [项目]
标题开始。我们的前两个元数据条目包括我们项目的 名称
和作者名单,包括姓名和电子邮件地址。在这个示例项目中,我们使用了假电子邮件地址,但在实际项目中,你应该使用你的真实电子邮件地址。
PyPI 要求所有项目都必须有唯一的名称。在你开始项目时检查这一点是个好主意,以确保没有其他项目已经使用了你想要的名称。还建议确保你的项目名称不会轻易与其他项目混淆;这将减少任何人意外安装错误包的可能性。
项目
表格中的下一个条目是我们项目的描述:
# railway-project/pyproject.toml
[project]
...
description = "A CLI client for the railway API"
readme = "README.md"
description
字段应该是项目的简短、单句总结,而 readme
应该指向包含更详细描述的 README 文件。
readme
也可以指定为 TOML 表格,在这种情况下,它应该包含一个 content-type
键,以及一个带有 README 文件路径的 file
键或一个带有完整 README 文本的 text
键。
项目许可证也应指定在元数据中:
# railway-project/pyproject.toml
[project]
...
license = {file = "LICENSE"}
license
字段是一个 TOML 表格,包含一个带有项目许可证文件路径的 file
键,或者一个带有许可证全文的 text
键。
下几个元数据条目旨在帮助潜在用户在 PyPI 上找到我们的项目:
# railway-project/pyproject.toml
[project]
...
classifiers = [
"Environment :: Console",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
keywords = ["packaging example", "CLI"]
classifiers
字段可以用来指定一个 trove 分类器 列表,这些分类器用于在 PyPI 上对项目进行分类。PyPI 网站允许用户在搜索项目时通过 trove 分类器进行筛选。你的项目分类器必须从 pypi.org/classifiers/
上的官方分类器列表中选择。
我们使用了分类器来表明我们的项目旨在在控制台环境中使用,它是在 MIT 许可证下发布的,它可以在 macOS、Windows 和 Linux 上运行,并且与 Python 3 兼容(具体为 3.10、3.11 和 3.12 版本)。请注意,分类器纯粹是为了向用户提供信息并帮助他们找到 PyPI 网站上的项目。它们对您的软件包可以安装的操作系统或 Python 版本没有影响。
keywords
字段可以用来提供额外的关键词,以帮助用户找到您的项目。与分类器不同,对您可以使用的关键词没有限制。
版本控制和动态元数据
项目元数据必须包含一个版本号。我们选择将版本作为一个动态字段来指定,而不是直接指定版本(通过version
键)。pyproject.toml
规范允许指定除name
之外的所有项目元数据,由其他工具动态指定。动态字段的名称通过dynamic
键指定:
# railway-project/pyproject.toml
[project]
...
dynamic = ["version"]
要使用与 Setuptools 兼容的动态元数据,我们需要使用tool.setuptools.dynamic
表来指定如何计算值。version
可以从文件中读取(使用带有file
键的表指定)或从 Python 模块中的属性中读取(使用带有attr
键的表指定)。对于本项目,我们从railway_cli
可导入包的__version__
属性中获取版本:
# railway-project/pyproject.toml
[tool.setuptools.dynamic]
version = {"attr" = "railway_cli.__version__"}
__version__
属性在railway_cli/__init__.py
文件中定义:
# railway-project/src/railway_cli/__init__.py
__version__ = "0.0.1"
使用动态字段意味着我们可以在代码和项目元数据中使用相同的版本号,而无需定义两次。
您可以选择适合您项目的任何版本控制方案,但它必须遵守 PEP 440(peps.python.org/pep-0440/
)中定义的规则。一个 PEP 440 兼容的版本由一系列由点分隔的数字组成,后面可以跟有可选的预发布、发布后或开发版本指示符。预发布指示符可以由字母a
(表示alpha)、b
(表示beta)或rc
(表示release-candidate)后跟一个数字组成。发布后指示符由单词post
后跟一个数字组成。开发版本指示符由单词dev
后跟一个数字组成。没有发布指示符的版本号被称为final发布。例如:
-
1.0.0.dev1
是我们项目 1.0.0 版本的第一个开发版本。 -
1.0.0.a1
是第一个 alpha 版本。 -
1.0.0.b1
是第一个 beta 版本。 -
1.0.0.rc1
是第一个发布候选版本。 -
1.0.0
是 1.0.0 版本的最终发布版本。 -
1.0.0.post1
是第一个发布后版本。
开发版本、预发布版本、最终发布版本和发布后版本,如果主版本号相同,它们的顺序如上列表所示。
流行的版本控制方案包括语义版本控制,该方案旨在通过版本控制方案传达关于发布之间兼容性的信息,以及基于日期的版本控制,通常使用发布的年份和月份来表示版本。
语义版本控制使用由三个数字组成的版本号,称为主版本、次版本和修订版本,由点分隔。这导致了一个看起来像major.minor.patch
的版本。如果一个新版本与其前一个版本完全兼容,则只增加修订号;通常,这样的版本只包含小的错误修复。对于添加新功能而不破坏与先前版本兼容性的版本,应增加次版本号。如果发布版本与旧版本不兼容,则应增加主版本号。您可以在semver.org/
上了解有关语义版本控制的全部内容。
指定依赖项
正如我们在本章开头所看到的,一个发行版包可以提供一个它所依赖的项目列表,并且pip
在安装包时会确保安装这些项目的版本。这些依赖项应在project
表的dependencies
键中指定:
# railway-project/pyproject.toml
[project]
...
dependencies = [
"pydantic[email]>=2.8.2,<3.0.0",
"pydantic-settings~=2.4.0",
"requests~=2.0",
]
我们的项目依赖于pydantic
、pydantic-settings
和requests
项目。方括号中的[email]
单词表示pydantic
依赖项还要求一些与处理电子邮件地址相关的pydantic
项目的可选依赖项。我们将在稍后更详细地讨论可选依赖项。
我们可以使用版本指定符来指示我们需要的依赖项的版本。除了正常的 Python 比较运算符之外,版本指定符还可以使用~=
来指示一个兼容版本。兼容版本指定符是一种表示在语义版本控制方案下可能兼容的版本的方式。例如,requests~=2.0
表示我们需要requests
项目的任何 2.x 版本,从 2.0 到 3.0(不包括 3.0)。版本指定符还可以接受一个逗号分隔的版本子句列表,必须满足所有这些子句。例如,pydantic>=2.8.2,<3.0.0
表示我们想要至少pydantic
版本 2.8.2,但不能是版本 3.0.0 或更高版本。请注意,这与pydantic~=2.8.2
不同,后者意味着至少版本 2.8.2,但不能是版本 2.9.0 或更高版本。有关依赖项语法和版本匹配的完整细节,请参阅 PEP 508(peps.python.org/pep-0508/
)。
您应该小心不要使您的依赖项版本指定过于严格。请记住,您的包可能将与同一虚拟环境中的各种其他包一起安装。这对于库或开发者工具尤其如此。在您的依赖项所需版本上尽可能提供最大自由度意味着,依赖于您的项目不太可能遇到您的包与其他项目依赖项之间的依赖项冲突。使您的版本指定过于严格还意味着,除非您也发布新版本来更新您的版本指定,否则您的用户将无法从您的依赖项中的错误修复或安全补丁中受益。
除了对其他项目的依赖项之外,您还可以指定您的项目需要哪些版本的 Python。在我们的项目中,我们使用了 Python 3.10 中添加的功能,因此我们指定至少需要 Python 3.10:
# railway-project/pyproject.toml
[project]
...
requires-python = ">=3.10"
与 dependencies
一样,最好避免过多限制您支持的 Python 版本。只有当您知道您的代码在所有活跃支持的 Python 3 版本上都无法工作的情况下,您才应该限制 Python 版本。
您可以在官方 Python 下载页面上找到 Active Python Releases 列表:www.python.org/downloads/
。
您应该确保您的代码确实可以在您在设置配置中支持的 Python 和依赖项的所有版本上运行。完成此操作的一种方法是为不同的 Python 版本和不同版本的依赖项创建几个虚拟环境。然后,您可以在所有这些环境中运行您的测试套件。手动执行此操作将非常耗时。幸运的是,有一些工具可以为您自动化此过程。最受欢迎的这些工具之一被称为 tox。您可以在 tox.wiki/
上了解更多信息。
您还可以为您的包指定可选依赖项。pip
只会在用户明确请求时安装这些依赖项。如果某些依赖项仅适用于许多用户不太可能需要的特性,这很有用。想要额外特性的用户可以安装可选依赖项,而其他人则可以节省磁盘空间和网络带宽。
例如,我们在 第九章,密码学和令牌 中使用的 PyJWT 项目,依赖于密码学项目使用非对称密钥签名 JWT。许多 PyJWT 用户不使用此功能,因此开发者将密码学作为可选依赖项。
可选(或额外)依赖项在 pyproject.toml
文件中的 project.optional-dependencies
表中指定。本节可以包含任意数量的命名可选依赖项列表。这些列表被称为 extras。在我们的项目中,我们有一个名为 dev
的额外依赖项:
# railway-project/pyproject.toml
dev = [
"black",
"isort",
"flake8",
"mypy",
"types-requests",
"pytest",
"pytest-mock",
"requests-mock",
]
这是在项目开发期间列出有用的工具作为可选依赖项的常见约定。许多项目还有一个额外的 test
依赖项,用于安装仅用于运行项目测试套件的包。
当安装包时包含可选依赖项,你必须在运行 pip install
时在方括号中添加你想要的额外依赖项的名称。例如,为了包含 dev
依赖项进行我们的项目的可编辑安装,你可以运行:
$ pip install -e './railway-project[dev]'
注意,在这个 pip install
命令中我们需要使用引号来防止 shell 将方括号解释为文件名模式。
项目 URL
你还可以在 project
元数据表的 urls
子表中包含与你的项目相关的网站 URL 列表:
# railway-project/pyproject.toml
[project.urls]
Homepage = "https://github.com/PacktPublishing/Learn-Python-..."
"Learn Python Programming Book" = "https://www.packtpub.com/..."
URLs 表的键可以是描述 URL 的任意字符串。在项目中包含指向源代码托管服务(如 GitHub 或 GitLab)的链接是很常见的。许多项目也链接到在线文档或错误跟踪器。我们已使用此字段添加了指向本书在 GitHub 上的源代码仓库的链接以及有关本书在出版社网站上的信息。
脚本和入口点
到目前为止,我们通过键入以下内容来运行我们的应用程序:
$ python -m railway_cli
这并不是特别用户友好。如果我们能只通过键入以下内容来运行我们的应用程序会更好:
$ railway-cli
我们可以通过为我们的发行版配置脚本 入口点 来实现这一点。脚本入口点是我们希望能够作为命令行或 GUI 脚本执行的功能。当我们的包被安装时,pip
将自动生成导入指定函数并运行它们的脚本。
我们在 pyproject.toml
文件中的 project.scripts
表中配置脚本入口点:
# railway-project/pyproject.toml
[project.scripts]
railway-cli = "railway_cli.cli:main"
在此表中,每个键定义了在安装包时应生成的脚本的名称。相应的值是一个对象引用,指向在执行脚本时应调用的函数。如果我们正在打包一个 GUI 应用程序,我们需要使用 project.gui-scripts
表。
Windows 操作系统以不同的方式处理控制台和 GUI 应用程序。控制台应用程序在控制台窗口中启动,可以通过控制台打印到屏幕并读取键盘输入。GUI 应用程序在没有控制台窗口的情况下启动。在其他操作系统上,scripts
和 gui-scripts
之间没有区别。
现在,当我们在一个虚拟环境中安装项目时,pip
将在虚拟环境 bin
文件夹(或在 Windows 上的 Scripts
文件夹)中生成一个 railway-cli
脚本。它看起来像这样:
#!/.../ch16/railway-project/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from railway_cli.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(
r'(-script\.pyw|\.exe)?$', '', sys.argv[0]
)
sys.exit(main())
文件顶部的#!/.../ch16/railway-project/venv/bin/python
注释被称为shebang(来自 hash + bang – bang 是感叹号的另一个名称)。它指定了将用于运行脚本的 Python 可执行文件的路径。脚本从railway_cli.main
模块导入main()
函数,对sys.argv[0]
中的程序名称进行一些操作,然后调用main()
并将返回值传递给sys.exit()
。
除了脚本入口点之外,还可以创建任意的入口点组。这些组由project.entry-points
表下的子表定义。pip
不会为其他组中的入口点生成脚本,但它们可以用于其他目的。具体来说,许多支持通过插件扩展其功能的项目使用特定的入口点组名称进行插件发现。这是一个更高级的主题,我们在这里不会详细讨论,但如果您感兴趣,您可以在入口点规范中了解更多信息:packaging.python.org/specifications/entry-points/
。
定义包内容
对于使用 src 布局的项目,Setuptools 通常可以自动确定要包含在分发中的 Python 模块。对于平面布局,如果项目文件夹中只有一个可导入的包,则自动发现才会工作。如果存在任何其他 Python 文件,您将需要通过pyproject.toml
文件中的tools.setuptools
表配置要包含或排除的包和模块。
如果您使用 src 布局,只有当您为包含代码的文件夹使用除src
之外的其他名称,或者当src
文件夹中有一些模块需要从分发中排除时,您才需要额外的配置。在我们的 railway-cli 项目中,我们依赖于自动发现,因此我们pyproject.toml
文件中没有任何包发现配置。您可以在 Setuptools 用户指南中了解更多关于 Setuptools 的自动发现以及如何自定义它的信息:setuptools.pypa.io/en/latest/userguide/
。
我们的包元数据配置现在已经完成。在我们继续构建和发布包之前,我们将简要看看如何在我们的代码中访问元数据。
在您的代码中访问元数据
我们已经看到如何使用动态元数据在分发配置和代码之间共享版本号。对于其他动态元数据,Setuptools 仅支持从文件中加载,而不是从代码中的属性加载。除非我们为此添加显式配置,否则这些文件将不会包含在 wheel 分发中。然而,有一个更方便的方法可以从代码中访问分发元数据。
importlib.metadata
标准库模块提供了访问任何已安装包的发行版元数据的接口。为了演示这一点,我们已向 Railway CLI 应用程序添加了一个用于显示项目许可证的命令行选项:
from importlib.metadata import metadata, packages_distributions
def get_arg_parser() -> argparse.ArgumentParser:
...
parser.add_argument(
"-L",
"--license",
action="version",
version=get_license(),
)
...
def get_license() -> str:
default = "License not found"
all_distributions = packages_distributions()
try:
distribution = all_distributions[__package__][0]
except KeyError:
return default
meta = metadata(distribution)
return meta["License"] or default
我们使用"version"
参数解析器操作来打印许可证字符串,并在命令行上存在-L
或--license
选项时退出。要获取许可证文本,我们首先需要找到与我们的可导入包对应的发行版。packages_distributions()
函数返回一个字典,其键是虚拟环境中可导入包的名称。值是提供相应可导入包的发行版包的列表。我们假设没有其他已安装的发行版提供与我们的包同名的一个包,所以我们只取all_distributions[__package__]
列表的第一个元素。
metadata
函数返回一个类似dict
的对象,包含元数据。键与用于定义元数据的pyproject.toml
条目名称相似,但并不完全相同。所有键及其含义的详细信息可以在packaging.python.org/specifications/core-metadata/
的元数据规范中找到。
注意,如果我们的包未安装,当我们尝试在packages_distributions()
返回的字典中查找它时,将会得到一个KeyError
。在这种情况下,或者在没有在项目元数据中指定"License"
的情况下,我们返回一个默认值来表示无法找到许可证。
让我们看看输出是什么样子:
$ railway-cli -L
Copyright (c) 2024 Heinrich Kruger, Fabrizio Romano
Permission is hereby granted, free of charge, ...
我们在这里已经裁剪了输出以节省空间,但如果你亲自运行它,你应该能看到打印出的完整许可证文本。
现在,我们已经准备好进行构建和发布发行版包。
构建和发布包
我们将使用由build项目提供的包构建器(pypi.org/project/build/
)来构建我们的发行版包。我们还需要twine(pypi.org/project/twine/
)工具来上传我们的包到 PyPI。您可以从本章源代码提供的requirements/build.txt
文件中安装这些工具。我们建议在新的虚拟环境中安装这些工具。
由于 PyPI 上的项目名称必须是唯一的,因此您在更改名称之前无法上传railway-cli
项目。在构建发行版包之前,您应该将pyptoject.toml
文件中的name
更改为一个唯一的名称。请注意,这意味着您的发行版包的文件名也将与我们不同。
构建
build
项目提供了一个简单的脚本,用于根据 PEP 517 规范构建包。它将为我们处理构建分发包的所有细节。当我们运行它时,build
将执行以下操作:
-
创建一个虚拟环境。
-
在虚拟环境中安装
pyproject.toml
文件中列出的构建需求。 -
导入
pyproject.toml
文件中指定的构建后端并运行它以构建源分发。 -
创建另一个虚拟环境并安装构建需求。
-
导入构建后端并使用它从步骤 3中构建的源分发构建一个轮子。
让我们看看它是如何工作的。进入章节源代码中的railway-project
文件夹,并运行以下命令:
$ python -m build
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- setuptools>=66.1.0
- wheel
* Getting build dependencies for sdist...
...
* Building sdist...
...
* Building wheel from sdist
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- setuptools>=66.1.0
- wheel
* Getting build dependencies for wheel...
...
* Building wheel...
...
Successfully built railway_cli-0.0.1.tar.gz and
railway_cli-0.0.1-py3-none-any.whl
我们已经从输出中删除了很多行,以便更容易地看到它如何遵循我们上面列出的步骤。如果您查看railway-project
文件夹的内容,您会注意到有一个名为dist
的新文件夹,其中包含两个文件:railway_cli-0.0.1.tar.gz
是我们的源分发,而railway_cli-0.0.1-py3-none-any.whl
是轮子。
在上传您的包之前,建议您进行一些检查以确保它正确构建。首先,我们可以使用twine
来验证readme
是否会在 PyPI 网站上正确渲染:
$ twine check dist/*
Checking dist/railway_cli-0.0.1-py3-none-any.whl: PASSED
Checking dist/railway_cli-0.0.1.tar.gz: PASSED
如果twine
报告了任何问题,您应该修复它们并重新构建包。在我们的例子中,检查通过了,因此让我们安装我们的轮子并确保它工作。在一个单独的虚拟环境中运行:
$ pip install dist./railway_cli-0.0.1-py3-none-any.whl
在您的虚拟环境中安装了轮子后,尝试运行应用程序,最好从项目目录外部运行。如果在安装或运行代码时遇到任何错误,请仔细检查您的配置是否有误。
我们的包似乎已经成功构建,因此让我们继续发布它。
发布
由于这是一个示例项目,我们将将其上传到 TestPyPI 而不是真正的 PyPI。这是一个专门创建的包索引的独立实例,旨在允许开发者测试包上传并实验打包工具和流程。
在您上传包之前,您需要注册一个账户。您可以通过访问test.pypi.org
并点击注册来现在就完成此操作。完成注册过程并验证您的电子邮件地址后,您需要生成一个 API 令牌。您可以在 TestPyPI 网站的账户设置页面完成此操作。确保在关闭页面之前复制令牌并保存它。您应该将令牌保存到用户主目录中名为.pypirc
的文件中。该文件应如下所示:
[testpypi]
username = __token__
password = pypi-...
应将password
值替换为您的实际令牌。
我们强烈建议您为您的 TestPyPI 账户以及尤其是您的真实 PyPI 账户启用双因素认证。
现在,您已经准备好运行twine
来上传您的分发包:
$ twine upload --repository testpypi dist/*
Uploading distributions to https://test.pypi.org/legacy/
Uploading railway_cli-0.0.1-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━ 19.3/19.3 kB • 00:00 • 7.3 MB/s
Uploading railway_cli-0.0.1.tar.gz
100% ━━━━━━━━━━━━━━━━━━ 17.7/17.7 kB • 00:00 • 9.4 MB/s
View at:
https://test.pypi.org/project/railway-cli/0.0.1/
twine
显示进度条以显示上传进度。一旦上传完成,它将打印出一个 URL,你可以在这里看到你包的详细信息。在浏览器中打开它,你应该会看到我们的项目描述以及 README.md
文件的内容。在页面的左侧,你会看到项目 URL、作者详情、许可信息、关键词和分类器的链接。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-py-prog-4e/img/B30992_16_01.png
图 16.1:我们在 TestPyPI 网站上的项目页面
图 16.1 展示了我们的 railway-cli 项目在这个页面上的样子。你应该仔细检查页面上的所有信息,并确保它们与你期望看到的一致。如果不一致,你将不得不修复 pyproject.toml
中的元数据,重新构建并重新上传。
PyPI 不会允许你重新上传与之前上传的包具有相同文件名的分发。为了修复你的元数据,你必须增加你包的版本号。在你完全确定一切正确无误之前使用开发版本号可以帮助你避免仅仅为了修复打包错误而不必要地增加版本号。
现在,我们可以从 TestPyPI 仓库安装我们的包。在新的虚拟环境中运行以下命令:
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ railway-cli==1.0.0
--index-url
选项指示 pip
使用 https://test.pypi.org/simple/
作为主要包索引。我们使用 --extra-index-url https://pypi.org/simple/
来告诉 pip
还要查找标准 PyPI 中的包,以便可以安装 TestPyPI 中不可用的依赖项。包安装成功,这证实了我们的包已正确构建和上传。
如果这是一个真实的项目,我们现在将开始上传到真实的 PyPI。过程与 TestPyPI 相同。当你保存 PyPI API 密钥时,你应该将其添加到现有 .pypirc
文件下的 [pypi]
标题下,如下所示:
[pypi]
username = __token__
password = pypi-…
你也不需要使用 --repository
选项将你的包上传到真实的 PyPI;你只需运行以下命令:
$ twine upload dist/*
如你所见,打包和发布项目并不困难,但确实有很多细节需要注意。好消息是,大部分工作只需要做一次:当你发布第一个版本时。对于后续版本,你通常只需要更新版本号,也许调整一下依赖项。在下一节中,我们将给你一些建议,这应该会使整个过程更容易。
新项目启动建议
一次性完成所有包装准备工作的过程可能相当繁琐。如果您试图在首次发布包之前编写所有包配置,很容易犯诸如忘记列出所需依赖项之类的错误。从简单的 pyproject.toml
开始,其中只包含基本配置和元数据,会更容易一些。您可以在项目开发过程中逐步添加元数据和配置。例如,每次您开始在代码中使用新的第三方项目时,您都可以立即将其添加到您的 dependencies
列表中。这也有助于您尽早开始编写 README 文件,并在进展过程中对其进行扩展。您甚至可能会发现,写一段或两段描述您项目的段落有助于您更清晰地思考您试图实现的目标。
为了帮助您,我们为新的项目创建了一个初始骨架。您可以在本章源代码的 skeleton-project
文件夹中找到它:
$ tree skeleton-project
skeleton-project
├── README.md
├── pyproject.toml
├── src
│ └── example
│ └── __init__.py
└── tests
└── __init__.py
将此内容复制下来,按需修改,并将其用作您自己项目的起点。
cookiecutter 项目(cookiecutter.readthedocs.io
)允许您创建模板,用作项目的起点。这可以使启动新项目的流程变得更加简单。
其他文件
本章中向您展示的配置文件就是您包装和分发大多数现代 Python 项目所需的所有文件。然而,如果您查看 PyPI 上的其他项目,可能会遇到一些其他文件:
-
在 Setuptools 的早期版本中,每个项目都需要包含一个
setup.py
脚本,该脚本用于构建项目。大多数这样的脚本仅包含对setuptools.setup()
函数的调用,并将项目元数据作为参数指定。setup.py
的使用尚未被弃用,这仍然是一种配置 Setuptools 的有效方法。 -
Setuptools 也支持从
setup.cfg
文件中读取其配置。在pyproject.toml
得到广泛采用之前,这是配置 Setuptools 的首选方式。 -
如果您需要在您的分发包中包含数据文件或其他非标准文件,您将需要使用
MANIFEST.in
文件来指定要包含的文件。您可以在packaging.python.org/guides/using-manifest-in/
上了解更多关于此文件的使用信息。
替代工具
在我们结束本章之前,让我们简要讨论一些您用于包装项目的替代选项。在 PEP 517 和 PEP 518 之前,除了 Setuptools 之外,很难使用其他任何工具来构建包。项目无法指定构建它们所需的库或构建方式,因此 pip
和其他工具只是假设应该使用 Setuptools 来构建包。
多亏了pyproject.toml
文件中的构建系统信息,现在使用任何你想要的打包库都变得容易。有几种选择可供选择,包括:
-
Flit 项目(
flit.pypa.io
)在启发 PEP 517 和 PEP 518 标准(Flit 的创建者是 PEP 517 的共同作者)的发展中起到了关键作用。Flit 旨在使不需要复杂构建步骤(如编译 C 代码)的纯 Python 项目打包尽可能简单。Flit 还提供了一个 CLI 用于构建软件包并将它们上传到 PyPI(因此你不需要build
工具或twine
)。 -
诗歌(
python-poetry.org/
)也提供了用于构建和发布软件包的 CLI 以及一个轻量级的 PEP 517 构建后端。然而,诗歌真正出色的地方在于其高级依赖管理功能。诗歌甚至可以为你管理虚拟环境。 -
Hatch(
hatch.pypa.io
)是一个可扩展的 Python 项目管理工具。它包括安装和管理 Python 版本、管理虚拟环境、运行测试等功能。它还包含一个名为 hatchling 的 PEP 517 构建后端。 -
PDM(
pdm-project.org/
)是另一个包含构建后端的软件包和依赖管理器。像 Hatch 一样,它可以用来管理虚拟环境和安装 Python 版本。还有许多插件可用于扩展 PDM 的功能。 -
Enscons(
dholth.github.io/enscons/
)基于 SCons(scons.org/
)通用构建系统。这意味着,与 Flit 或 Poetry 不同,enscons 可以用来构建包含 C 语言扩展的发行版。 -
Meson(
mesonbuild.com/
)是另一个流行的通用构建系统。meson-python
(mesonbuild.com/meson-python/
)项目提供了一个基于 Meson 的 PEP 517 构建后端。这意味着它可以构建包含使用 Meson 构建的扩展模块的发行版。 -
Maturin(
www.maturin.rs/
)是另一个构建工具,可以用来构建包含使用 Rust 编程语言实现的扩展模块的发行版。
我们在本章中讨论的工具都专注于通过 PyPI 分发软件包。根据你的目标受众,这不一定总是最佳选择。PyPI 主要存在是为了分发项目,如库和 Python 开发者的开发工具。从 PyPI 安装和使用软件包还需要有一个正常工作的 Python 安装,以及至少足够的 Python 知识,知道如何使用pip
安装软件包。
如果你的项目是一个面向技术能力较弱的受众的应用程序,你可能需要考虑其他选项。Python 打包用户指南提供了关于分发应用程序的各种选项的有用概述;它可在packaging.python.org/overview/#packaging-applications
找到。
这标志着我们打包之旅的结束。
进一步阅读
我们将本章结束于一些链接,你可以通过这些链接阅读更多关于打包的资源:
-
Python 打包权威机构的打包历史页面(
www.pypa.io/en/latest/history/
)是了解 Python 打包演变的有用资源。 -
Python 打包用户指南(
packaging.python.org/
)包含有用的教程和指南,以及打包术语表、打包规范的链接和与打包相关的各种有趣项目的总结。 -
Setuptools 文档(
setuptools.readthedocs.io/
)包含大量有用信息。 -
如果你正在分发一个使用类型提示的库,你可能希望分发类型信息,以便依赖你的库的开发者可以对他们的代码运行类型检查。Python 类型文档包括一些关于分发类型信息的有用信息:
typing.readthedocs.io/en/latest/spec/distributing.html
。 -
在本章中,我们展示了如何在代码中访问分发元数据。大多数打包工具还允许你在分发包中包含数据文件。标准库
importlib.resources
模块提供了对这些包资源的访问。你可以在docs.python.org/3/library/importlib.resources.html
了解更多信息。
当你阅读这些(以及其他打包资源)时,值得记住的是,尽管 PEP 517、PEP 518 和 PEP 621 几年前就已经最终确定,但许多项目尚未完全迁移到使用 PEP 517 构建,或在 pyproject.toml
文件中配置项目元数据。大部分可用的文档也仍然引用了较旧的做法。
摘要
在本章中,我们探讨了如何通过 PyPI 打包和分发 Python 项目。我们首先介绍了一些关于打包的理论,并引入了 PyPI 上的项目、发布和分发等概念。
我们学习了 Setuptools,这是 Python 最广泛使用的打包库,并了解了使用 Setuptools 准备项目打包的过程。在这个过程中,我们看到了需要添加到项目中以进行打包的各种文件,以及每个文件的作用。
我们讨论了你应该提供哪些元数据来描述你的项目并帮助用户在 PyPI 上找到它,以及如何将代码添加到发行版中,如何指定我们的依赖项,以及如何定义入口点,以便 pip
自动为我们生成脚本。我们还探讨了 Python 提供的用于访问发行版元数据的工具。
我们继续讨论如何构建发行包以及如何使用 twine
将这些包上传到 PyPI。我们还给你提供了一些关于启动新项目的建议。在简要介绍了一些 Setuptools 的替代方案后,我们指出了你可以学习更多关于打包知识的一些资源。
我们真的非常鼓励你开始在 PyPI 上分发你的代码。无论你认为它可能多么微不足道,世界上某个地方的其他人可能觉得它很有用。为社区做出贡献并回馈社区的感觉真的很好,而且,这对你的简历也有好处。
下一章将带你进入竞技编程的世界。我们将稍微偏离专业编程,学习一些关于编程挑战及其为何如此有趣、有用的知识。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
discord.com/invite/uaKmaz7FEC
第十七章:编程挑战
“注意上述代码中的错误;我只证明了它是正确的,而没有尝试运行它。”
——唐纳德·克努特(Donald Knuth)
在本章中,我们将偏离专业编程,讨论竞赛编程和编程挑战的世界。
编程挑战是可以用相对简短的程序解决的问题。有几个网站提供了大量的挑战集合。大多数网站根据难度和其他因素,如:
-
解决挑战所需的算法类型。
-
挑战涉及的数据结构类型,如树、链表、n 维向量等。
-
挑战涉及的具体数据结构类型。例如,对于 Python,它们可以是列表、元组、字典等。
-
解决挑战所需的方法类型,如动态规划、递归等。
这个列表并不全面,但它给出了我们可以期待的内容。
提供编程挑战的网站出于各种原因。有些帮助程序员准备面试,有些只是出于娱乐,还有些作为教授编程的方式。
一些网站还举办比赛,其中参赛者提供的解决方案在执行速度、正确性和内存占用等方面进行衡量,仅举几例。
竞赛编程的世界非常有趣,编程挑战是学习新语言和提高编程技能的绝佳方式。
在本章中,我们将解决来自Advent of Code(https://adventofcode.com/)的两个问题,这是我们最喜欢的挑战网站。解决挑战很有趣,而且这是我们在相对较短的时间内展示 Python 几个特性的有效方式。
Advent of Code
《Advent of Code》是由埃里克·瓦斯特尔(Eric Wastl)在 2015 年创建的。引用网站上的话:
“《Advent of Code》是一个包含各种技能水平和技能集的小型编程谜题日历,可以用你喜欢的任何编程语言解决。人们使用它们作为面试准备、公司培训、大学课程、练习问题、速度竞赛或相互挑战。”
你不需要计算机科学背景来参与——只需要一点编程知识和一些解决问题的技能,你就能走得很远。你也不需要一台高级的电脑;每个问题都有一个解决方案,在十年前的硬件上最多只需要 15 秒就能完成。”
每个问题有两个部分。根据埃里的说法,第一个部分是确保你理解任务的方式,第二个部分是实际问题。通常,第二部分比第一部分更具挑战性。每个问题都附有可以从网站上下载的输入数据。
这个网站有一些独特的特点。首先,每个问题都是故事的一部分,程序员通过解决挑战来帮助圣诞老人或他的助手,小精灵,拯救圣诞节。因此,每个问题都是一次持续 25 天的冒险的一部分,从 12 月 1 日到 12 月 25 日。展示中充满了幽默,每个问题都是独特的,需要一定程度的逻辑和创造性思维。这在许多其他类似网站上并不常见,那里的挑战通常非常枯燥,它们的解决方案通常围绕应用某些算法或使用适当的数据结构或模式。
在决定添加这一章节之前,我们与埃里克进行了交谈,他唯一的要求是我们不要逐字逐句地复制全部挑战。根据这一点,我们已缩减了问题陈述,只呈现问题指令。
我们即将呈现的解决方案只是解决这些挑战的几种方法之一。它们旨在让我们与您讨论一些最终概念。我们的建议是:在阅读完这一章后,尝试提出自己对所提问题的解决方案。
让我们开始吧。
骆驼牌
第一个问题,骆驼牌,来自 2023 年 7 月 7 日。在这个挑战中,你可以在adventofcode.com/2023/day/7
找到,我们必须编写一个程序来解决扑克游戏的变体。
第一部分 – 问题陈述
这是对原文的缩减版本:
“在骆驼牌中,你将得到一串手牌,你的目标是根据每手牌的强度来排序。一双手牌由五张标记为A
、K
、Q
、J
、T
、9
、8
、7
、6
、5
、4
、3
或2
的牌组成。每张牌的相对强度遵循以下顺序,其中A
为最高,2
为最低。
每一手牌都是一种类型。从最强到最弱,它们是:
-
五张同花,其中所有五张牌都有相同的标记:
AAAAA
。 -
四张同花,其中四张牌有相同的标记,另一张牌有不同的标记:
AA8AA
。 -
满贯,其中三张牌有相同的标记,剩下的两张牌共享不同的标记:
23332
。 -
三张同花,其中三张牌有相同的标记,剩下的两张牌与手中的任何其他牌都不同:
TTT98
。 -
两对,其中两张牌共享一个标记,另外两张牌共享第二个标记,剩下的牌有第三个标记:
23432
。 -
一对,其中两张牌共享一个标记,其他三张牌与这对牌和彼此都有不同的标记:
A23A4
。 -
高牌,其中所有牌的标记都是不同的:
23456
。
手牌主要按类型排序;例如,每个满贯都比任何三张同花更强。
如果有两手牌类型相同,则生效第二个排序规则。首先比较每手牌中的第一张牌。如果这些牌不同,则拥有更强第一张牌的手牌被认为是更强的。如果每手牌中的第一张牌标签相同,则继续考虑每手牌中的第二张牌。如果它们不同,则拥有更高第二张牌的手牌获胜;否则,继续考虑每手牌中的第三张牌,然后是第四张,然后是第五张。
因此,33332
和2AAAA
都是四条牌的手牌,但33332
更强,因为它的第一张牌更强。同样,77888
和77788
都是满贯牌,但77888
更强,因为它的第三张牌更强(而且这两手牌都有相同的第一张和第二张牌)。
要玩骆驼牌,你会得到一串手牌及其相应的叫牌(你的谜题输入)。例如:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
这个例子展示了五手牌;每手牌后面跟着它的叫牌金额。每手牌赢得的金额等于其叫牌金额乘以其排名,其中最弱的手牌排名为 1,第二弱的手牌排名为 2,依此类推,直到最强的手牌。因为在这个例子中有五手牌,所以最强的手牌将拥有排名 5,其叫牌金额将乘以 5。
因此,第一步是将手牌按照力量顺序排列:
-
32T3K
是唯一的一对牌,而其他手牌都是更强类型,所以它获得排名 1。 -
KK677
和KTJJT
都是两对牌。它们的第一张牌标签都相同,但KK677
的第二张牌更强(K
对T
),所以KTJJT
获得排名 2,KK677
获得排名 3。 -
T55J5
和QQQJA
都是三张牌。QQQJA
的第一张牌更强,所以它获得排名 5,而T55J5
获得排名 4。 -
现在,你可以通过将每手牌的叫牌金额乘以其排名的结果相加来确定这组手牌的总共赢利(
765 * 1 + 220 * 2 + 28 * 3 + 684 * 4 + 483 * 5
)。因此,在这个例子中的总共赢利是6440
。
找出你手中每手牌的排名。总共赢利是多少?”
我们的任务是按照给定的说明找到总共赢利。
第一部分 – 解决方案
解决方案的实施很简单。因为问题的第二部分与第一部分相当相似,所以我们已经将公共逻辑封装到一个基类Solver
中。这个类提供了我们解决问题所需的所有方法,除了一个,即type()
方法,它在每个子类PartOne
和PartTwo
中实现。这是防止代码重复的许多可能方法之一。让我们看看代码的第一部分:
# day7.py
from collections import Counter
from functools import cmp_to_key
from util import get_input
class Solver:
strengths: str = ""
def __init__(self) -> None:
self.ins: list[str] = get_input("input7.txt")
def solve(self) -> int:
hands = dict(self.parse_line(line) for line in self.ins)
sorted_hands = sorted(
hands.keys(), key=cmp_to_key(self.cmp)
)
return sum(
rank * hands[hand]
for rank, hand in enumerate(sorted_hands, start=1)
)
def parse_line(self, line: str) -> tuple[str, int]:
hand, bid = line.split()
return hand, int(bid)
我们从标准库中导入Counter
和cmp_to_key()
,从util.py
模块中导入get_input()
。后者是一个辅助函数,它读取输入文件并返回一个字符串列表,例如["9A35J 469", "75T32 237", ...]
。
我们定义了一个Solver
类,这里只部分展示了该类,它会在初始化时读取输入,并包含一个solve()
方法,该方法运行算法。步骤很简单:我们使用parse_line()
方法将字符串列表转换为字典(hands
),其中键是手牌,值是它们各自的出价,这些出价已经被转换为整数。
在转换输入数据后,我们创建了一个手牌列表,按照其等级排序,等级是根据问题陈述中给出的规则计算的。我们将在稍后分析自定义的cmp()
方法。现在,只需注意它是如何被使用的,用于执行排序。由于比较器需要两个对象进行比较,所以它不适合作为sorted()
函数的key
参数的参数。因此,Python 提供了一个cmp_to_key()
函数,它接受一个比较器函数作为输入,并产生一个可以作为排序键的对象。
在创建好排序好的手牌列表后,我们返回每对手牌的等级和其出价之间的所有乘积的总和。
让我们现在来检查这个类的更有趣的部分:
# day7.py
…
class Solver:
…
def type(self, hand: str) -> list[int]:
raise NotImplementedError
def cmp(self, hand1: str, hand2: str) -> int:
"""-1 if hand1 < hand2 else 1, or 0 if hand1 == hand2"""
type1 = self.type(hand1)
type2 = self.type(hand2)
if type1 == type2:
for card1, card2 in zip(hand1, hand2):
strength1 = self.strengths.index(card1)
strength2 = self.strengths.index(card2)
if strength1 == strength2:
continue
return -1 if strength1 < strength2 else 1
return 0
return -1 if type1 < type2 else 1
在类的其余代码中,我们定义了一个自定义比较器cmp()
。它接受两个手牌并按照问题的规则进行比较。首先,我们计算每个手牌的类型。如果类型不同,如果hand2
比hand1
强,则返回-1
,反之则返回1
。如果两个手牌的类型相同,我们需要检查每张牌的强度。根据检查结果,我们使用之前相同的标准返回-1
或1
。为了完整性,如果两个手牌的类型和强度都相同,则返回0
。我们知道,根据问题陈述,这种情况永远不会发生,否则最终的排名可能会依赖于输入顺序。
比较器正在使用type()
方法,其逻辑没有在这个类中实现。由于牌的强度和手牌类型的计算在第一部分和第二部分之间是不同的,所以我们把它们实现在了两个专门的类中。
让我们看看PartOne
的实现:
# day7.py
…
class PartOne(Solver):
strengths: str = "23456789TJQKA"
def type(self, hand: str) -> list[int]:
return [count for _, count in Counter(hand).most_common()]
在PartOne
中,我们设置了strengths
类属性并实现了type()
方法。当我们用几个示例手牌调用它时,这些是结果:
type("KK444") = [3, 2]
type("QQQ6Q") = [4, 1]
type("5JA22") = [2, 1, 1, 1]
type("7A264") = [1, 1, 1, 1, 1]
type("TTKTT") = [4, 1]
这是type()
的工作方式:首先将手牌输入到Counter
中。这个对象将计算手牌中每个字符出现的次数。在第一个例子中,Counter("KK444")
的结果是{'K': 2, '4': 3}
,这是一个类似字典的对象。正如预期的那样,我们有两个K
和三个4
。使用most_common()
方法,我们可以获取该对象的值的列表,按从高到低的顺序排序。这将产生上面的结果:[3, 2]
。这些列表,如[1, 1, 1, 1, 1]
、[3, 2]
、[4, 1]
等,可以相互比较以计算等级。我们在solve()
方法中计算sorted_hands
时这样做。
现在,是时候创建一个PartOne
的实例并运行它的solve()
方法了:
# day7.py
part_one = PartOne()
print(part_one.solve())
注意,我们将PartOne.strengths
设置为字符串"
23456789TJQKA"
。这是根据问题陈述评估强度的顺序,其中2
是最弱的,A
是最强的。使用它们的位置来表示它们的相对权重,使我们能够在cmp()
中使用它们的索引进行比较。索引越高,牌越强。
第一部分现在已经结束,让我们继续到第二部分。
第二部分 – 问题陈述
当我们在网站上输入第一部分的正确解决方案时,我们可以访问第二部分。现在规则有所改变,因为游戏中引入了王牌牌。
“现在,J
牌是王牌——可以像任何牌一样行动的百搭牌。为了平衡这一点,J
牌现在是单个牌中最弱的,甚至比2
还弱。其他牌保持相同的顺序:A
、K
、Q
、T
、9
、8
、7
、6
、5
、4
、3
、2
、J
。
J
牌可以假装成任何最适合确定手牌类型的牌;例如,QJJQ2
现在被认为是四条。然而,为了在相同类型的两张手牌之间打破平局,J
始终被当作J
来处理,而不是它假装成的牌:JKKK2
比QQQQ2
弱,因为J
比Q
弱。
现在,上面的例子会有很大的不同:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
-
32T3K
仍然是唯一的单对牌;它不包含任何王牌,所以它的强度没有增加。 -
KK677
现在是唯一的两对牌,使其成为第二最弱的牌。 -
T55J5
、KTJJT
和QQQJA
现在都是四条!T55J5
得到第 3 名,QQQJA
得到第 4 名,KTJJT
得到第 5 名。
使用新的王牌规则,本例中的总奖金为5905
。使用新的王牌规则,找出你手中每张牌的等级。新的总奖金是多少?”
让我们深入到解决方案中。
第二部分 – 解决方案
对于这部分,我们只需要提供正确的新的一组强度和一个不同的type()
方法实现。
让我们看看代码,这是从同一个模块继续的:
# day7.py
…
class PartTwo(Solver):
strengths: str = "J23456789TQKA"
def type(self, hand: str) -> list[int]:
card_counts = Counter(hand.replace("J", "")).most_common()
h = [count for _, count in card_counts]
return [h[0] + hand.count("J"), *h[1:]] if h else [5]
part_two = PartTwo()
print(part_two.solve())
上述内容是我们解决问题第二部分所需的所有内容。type()
方法的新的版本是这样工作的:我们仍然将 hand
字符串喂给一个 Counter
,但这次我们从手牌中移除了任何王牌。我们再次使用 most_common()
方法按逆序排序值。在这个时候,我们最终会遇到以下这些情况之一:
-
对于完全由王牌组成的手牌
"JJJJJ"
,h
将为空,因此type()
返回[5]
。这是正确的,因为这个手牌的最高等级是 五带一。 -
对于没有王牌的手牌,逻辑行为类似于
PartOne.type()
。 -
对于至少有一张王牌但不到五张的手牌,我们首先忽略王牌计算其类型。之后,我们返回一个修改过的结果版本,其中第一个元素增加手牌中王牌的数量。例如,手牌
"KKJJ4"
将产生(忽略王牌)列表[2, 1]
(两个K
和一个4
)。为了最大化其等级,最聪明的事情是将两张王牌当作K
使用。这意味着拥有等效的手牌"KKKK4"
。为此,我们将 2(王牌的数量)加到列表[2, 1]
的第一个元素上,因此我们返回[4, 1]
,这是"KKKK4"
手牌的正确类型。
PartTwo
中唯一另一个重要的细节是新的大牌顺序,现在是 "J23456789TQKA"
。对于这部分,大牌顺序也考虑了王牌牌,它是最弱的,因此放在字符串中的最低索引位置。
这就结束了问题的第二部分。我们选择这个问题是因为它允许我们向你展示如何使用面向对象编程(OOP)来防止代码重复,以及使用 Counter
和 cmp_to_key()
。
你可以在书籍源代码的 ch17
文件夹中找到这个问题的输入,以及 util.py
模块中 get_input()
函数的实现。
现在,让我们继续进行第二个挑战。
宇宙膨胀
第二个问题,宇宙膨胀,来自 2023 年 11 月 11 日。在这个挑战中,你可以在 adventofcode.com/2023/day/11
找到,我们需要扩展一个宇宙并计算所有星系对之间最短路径的长度。
第一部分 – 问题陈述
这里是原始文本的简略版:
“研究员收集了一堆数据,并将数据编译成一张巨大的图像(你的谜题输入)。图像包括空白空间(.
)和星系(#
)。例如:
...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....
研究员正在试图找出每对星系之间最短路径长度的总和。然而,有一个陷阱:在那些星系的光到达观测站之前,宇宙已经膨胀了。
只有部分空间会膨胀。实际上,任何包含没有星系的行或列都应该扩大一倍。在上面的例子中,有三列和两行没有星系。这些行和列需要扩大一倍;因此,宇宙膨胀的结果看起来像这样:
....#........
.........#...
#............
.............
.............
........#....
.#...........
............#
.............
.............
.........#...
#....#.......
配备了这个扩展宇宙,可以找到每对星系之间的最短路径。这有助于为每个星系分配一个唯一的数字。在这 9 个星系中,有 36 对。只计算每对一次;对内的顺序不重要。对于每一对,使用只向上、向下、向左或向右移动一步的步骤(每次移动正好一步或 #)来找到两个星系之间的任何最短路径。(两个星系之间的最短路径允许穿过另一个星系。)
分配唯一的数字,并用 x 突出显示从星系 5 到星系 9 的其中一条最短路径,我们得到这个:
....1........
.........2...
3............
.............
.............
........4....
.5...........
.xx.........6
..xx.........
...xx........
....xx...7...
8....9.......
从星系 5 到星系 9(8 个 x 加上进入星系 9 的步骤)至少需要 9 步。这里有一些其他最短路径长度的例子:
在星系 1 和星系 7 之间:15
在星系 8 和星系 9 之间:5
在这个例子中,在扩展宇宙之后,所有 36 对星系之间最短路径的总和是 374。扩展宇宙,然后找到每对星系之间最短路径的长度。这些长度的总和是多少?”
让我们现在看看这一部分的解答。
第一部分 – 解答
让我们先定义基本块:
# day11.py
from itertools import combinations
from typing import NamedTuple, Self
from util import get_input
universe = get_input("input11.txt")
class Galaxy(NamedTuple):
x: int
y: int
@classmethod
def expand(
cls, galaxy: Self, xfactor: int, yfactor: int
) -> Self:
return cls(galaxy.x + xfactor, galaxy.y + yfactor)
@classmethod
def manhattan(cls, a: Self, b: Self) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
class ExpansionCoeff(NamedTuple):
x: int = 0
y: int = 0
代码从一些导入开始。我们需要计算所有星系对,这可以通过使用 itertools.combinations()
来实现。我们还需要从 typing
模块中的 NamedTuple
和 Self
,以及当然,我们的自定义 get_input()
函数来读取输入文件,我们将它命名为 universe
。
在这个问题中,我们选择创建一个 Galaxy
类,它代表空间中的一个点。它从 NamedTuple
继承,这提供了一些开箱即用的有用功能。其他适合这种数据的选择是 dataclasses.dataclass
, complex
,或者甚至只是一个裸露的自定义类。
Galaxy
有两个坐标, x
和 y
;它定义了一个 expand()
方法,该方法返回具有偏移坐标的新 Galaxy
,以及一个 manhattan()
方法,该方法使用 出租车几何 来计算两个星系之间的距离。要了解更多信息,请访问 en.wikipedia.org/wiki/Taxicab_geometry
。简单来说,这是在整数平面上垂直和水平方向上受到运动限制时计算距离的方式。
我们还定义了一个 ExpansionCoeff
类来表示膨胀系数。
这里是求解器逻辑的主要部分:
# day11.py
…
def coords_to_expand(universe: list[str]) -> list[int]:
return [
coord
for coord, row in enumerate(universe)
if set(row) == {"."}
]
def expand_universe(universe: list[str], coeff: int) -> set:
galaxies = parse(universe)
rows_to_expand = coords_to_expand(universe)
galaxies = expand_dimension(
galaxies, rows_to_expand, ExpansionCoeff(y=coeff - 1)
)
**transposed_universe = [****""****.join(col)** **for** **col** **in****zip****(*universe)]**
cols_to_expand = coords_to_expand(transposed_universe)
return expand_dimension(
galaxies, cols_to_expand, ExpansionCoeff(x=coeff - 1)
)
def parse(ins: list[str]) -> set:
return {
Galaxy(x, y)
for y, row in enumerate(ins)
for x, val in enumerate(row)
if val == "#"
}
def solve(universe: list[str], coeff) -> int:
expanded_universe = expand_universe(universe, coeff)
return sum(
Galaxy.manhattan(g1, g2)
for g1, g2 in combinations(expanded_universe, 2)
)
solve()
函数计算扩展宇宙并返回每对之间最短路径的总和。主要任务在 expand_universe()
方法中执行。
在这里,我们首先解析宇宙,提取一组Galaxy
实例。扩展分为两个步骤:首先,我们沿着垂直方向扩展,然后是水平方向。因为宇宙是一个字符串列表,它可以被视为一个二维矩阵。为了沿两个正交方向扩展,我们有两种选择:一种是为每个方向编写一个单独的函数并传递宇宙作为参数。第二种选择,即我们实现的,是只为垂直方向编写扩展代码,一次使用原始宇宙调用它,然后再次传递宇宙的转置版本。这相当于沿水平方向扩展。
如果你不太熟悉转置矩阵的概念,你可以在en.wikipedia.org/wiki/Transpose
上了解它。简单来说,矩阵的转置版本就是将矩阵沿对角线翻转的结果。结果是原始的行变成了列,反之亦然。
在高亮行中,你可以看到如何计算宇宙的转置版本。我们本可以使用zip(*universe)
,但这将需要对类型注解进行一些调整,因为这样不会返回一个字符串列表。我们选择遵守更简单的类型注解,并将宇宙的转置版本作为一个字符串列表来保持与原始宇宙版本的一致性。
值得注意的是,在专业代码中,更好的选择是将注解适应到代码,而不是相反,但在这个案例中,我们希望尽可能保持代码的简洁性,以便于阅读。
关于coords_to_expand()
方法,我们实现宇宙扩展方向的算法依赖于坐标是排序的事实。你可以看到,通过从上到下遍历宇宙并返回不包含星系的行的坐标列表,我们仍然在不需要显式调用sorted()
的情况下产生了一个排序列表。
我们需要的最后一段代码是执行一维扩展的部分:
# day11.py
…
def expand_dimension(
galaxies: set,
coords_to_expand: list[int],
expansion_coeff: ExpansionCoeff,
) -> set:
dimension = "x" if expansion_coeff.y == 0 else "y"
for coord in reversed(coords_to_expand):
new_galaxies = set()
for galaxy in galaxies:
if getattr(galaxy, dimension) >= coord:
galaxy = Galaxy.expand(galaxy, *expansion_coeff)
new_galaxies.add(galaxy)
galaxies = new_galaxies
return new_galaxies
expand_dimension()
函数可能不是那么直接,让我们逐行分析它。我们首先通过检查expansion_coeff
对象来获取我们应该扩展的维度。如果它的y
属性是0
,我们就在x
维度上扩展,反之,如果x
属性是0
,我们就在y
维度上扩展。
然后我们进入一个嵌套循环。它的外层遍历所有需要扩展的坐标。我们以相反的顺序遍历它们,这样我们就不会将星系移动到我们尚未考虑的坐标上,这可能会导致星系移动超过应有的距离。
在进入内部循环之前,我们创建一个集合,new_galaxies
,它将包含当前坐标被使用后的所有星系。其中一些新星系可能会移动,而另一些则可能不会。
内部循环结束后,我们将galaxies
赋值为new_galaxies
,然后移动到下一个坐标进行扩展。在这个过程中,所有星系都将被适当地移动。
就这样。我们现在只需要调用带有正确系数的求解器:
# day11.py
print(solve(universe, coeff=2))
这将给出第一部分的解决方案。
第二部分 – 问题陈述
与第一个问题一样,第二部分只是第一部分的微小变化。让我们看看问题陈述:
“现在,不再使用你之前所做的扩展,将每个空行或列扩大一百万倍。也就是说,每个空行应替换为 100 万个空行,每个空列应替换为 100 万个空列。
(在上面的例子中,如果每个空行或列只是 100 倍更大,每对星系之间最短路径的总和将是 8,410。然而,你的宇宙需要扩展到远超过这些值。)
从相同的初始图像开始,根据这些新规则扩展宇宙,然后找到每对星系之间最短路径的长度。这些长度的总和是多少?”
在 Advent of Code 中,通常在第二部分我们会意识到第一部分的实现是否足够好。在这种情况下,因为我们选择使用一组Galaxy
对象来表示星系,而不是坚持使用字符串列表(或列表的列表),我们不需要在我们的代码中做任何改变,除了传递给solve()
函数的扩展系数。
第二部分 – 解答
让我们看看我们是如何调用solve()
来解决第二部分的:
# day11.py
print(solve(universe, coeff=int(1e6)))
就这样。现在,我们传递 1,000,000,而不是 2,我们就能得到第二部分的正确结果。
关键的收获是,通过在第一部分选择合适的数据结构,我们可以通过任何系数扩展宇宙,而不会产生任何惩罚。
我们使用的技术是称为稀疏矩阵或稀疏数组的概念的一个版本。你可以在en.wikipedia.org/wiki/Sparse_matrix
上了解更多信息。
当处理矩阵形式的数据时,我们通常使用的数据结构是列表的列表(或者,在这个问题的情况下,是字符串的列表)。然而,有时矩阵大部分是空的,而重要的数据只是整个数据的一小部分。
在问题的宇宙中,例如,星系只是整个宇宙的一小部分,其余部分是空的。因此,用列表的列表(或字符串的列表)来表示它们是不合适的,我们选择不同的数据结构。在这里,我们选择了一组坐标,因为它足以保留我们所需的所有信息。
在其他情况下,可能需要一个字典。比如说,每个星系都有一个与之相关的亮度因子。使用字典,我们可以通过设置坐标作为键,亮度因子作为值来表示每个星系。主要观点仍然是相同的:我们只会存储星系数据,而不会存储如果我们使用列表的列表必须存储的空空间。
正如我们在前面的章节中提到的,选择合适的数据结构至关重要。在这个问题中,如果我们选择通过将宇宙存储在另一个字符串列表(或列表的列表)中创建一个扩展版本的宇宙,那么对于第一部分来说是可以的,但对于第二部分来说就不行了。
最终考虑
在结束本章之前,这里有一些最后的考虑。
首先,正如本章开头所提到的,我们提出的两个问题的解决方案并不声称是最优雅的,也不是最有效的。我们本可以编写更快的算法,在处理更大规模的数据输入时表现会更好。
此外,我们结构代码的方式,使用面向对象编程来处理骆驼卡和使用函数式方法来处理宇宙扩张,只是为了确保我们可以向您展示解决给定问题的不同代码结构方式。
此外,我们还可以选择其他方法将解决方案拆分为类和函数,我们还可以使用不同的数据结构来表示数据。
我们尽量优先考虑可读性和简洁性,同时仍然向您展示我们在本书其他部分未能探索的概念,例如使用自定义比较函数或稀疏矩阵。
我们希望您喜欢这次从专业 Python的短暂偏离,我们也希望激发您的一些好奇心。
我们的建议是注册 Advent of Code,并至少尝试为本章中的问题提出自己的解决方案。尝试在我们没有使用面向对象编程的地方使用它,反之亦然。尝试使用不同的算法和其他方式来组织数据。最重要的是,享受乐趣。解决编程挑战可以非常有趣,如果你像我们一样,可能会上瘾!
我们以一个编程挑战网站的列表结束本章,我们希望您会发现这些网站很有趣。
其他编程挑战网站
这里有一些我们最喜欢的挑战网站列表。其中一些是免费的,一些不是。有些是数学导向的,而有些则更专注于纯编程。有些有助于面试准备,而有些只是为了娱乐。
面试准备:
-
LeetCode:
leetcode.com/
-
HackerRank:
www.hackerrank.com/
-
CodeSignal:
codesignal.com/
-
Coderbyte:
coderbyte.com/
-
HackerEarth:
www.hackerearth.com/
竞赛编程:
-
Codeforces:
codeforces.com/
-
Topcoder:
www.topcoder.com/
-
AtCoder:
atcoder.jp/
-
Sphere Online Judge (SPOJ):
www.spoj.com/
技能建设和学习:
-
Project Euler:
projecteuler.net/
(Fabrizio 曾是 Project Euler 开发团队的成员。他创建了一些问题,并与其他人合作) -
Exercism:
exercism.org/
-
Codewars:
www.codewars.com/
乐趣与社区:
-
编程挑战赛:
adventofcode.com/
-
CodinGame:
www.codingame.com/
机器学习:
- Kaggle:
www.kaggle.com/
我们希望您能花些时间探索其中的一些内容;它们将帮助您保持头脑清醒,复习或学习算法、数据结构和新的编程语言。
摘要
在本章中,我们探索了编程挑战的世界。我们解决了 Advent of Code 网站上的两个问题,并了解了一个不同的宇宙,在那里编程被用于学习、娱乐、准备面试和比赛。
我们还学习了自定义比较函数和稀疏矩阵,并看到了我们之前章节中学到的某些概念如何应用于解决问题。
现在,我们的旅程即将结束。保持势头,充分利用这些页面上学到的知识,这取决于您。我们试图为您提供坚实的基础,这将足以支持您在知识和方法方面的下一步行动。
我们希望我们已经成功地传达了我们的热情和经验,并相信它将伴随您走向任何地方。
希望您喜欢阅读这本书,祝您好运!
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
discord.com/invite/uaKmaz7FEC
订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及行业领先的工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
为什么订阅?
-
使用来自 4,000 多位行业专业人士的实用电子书和视频,减少学习时间,增加编码时间
-
通过为您量身定制的技能计划提高您的学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于快速获取关键信息
-
复制粘贴、打印和收藏内容
在www.packt.com ,你还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
你可能还会喜欢的其他书籍
如果你喜欢这本书,你可能对 Packt 的其他书籍也感兴趣:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-py-prog-4e/img/9781835466384.jpg[(https://www.packtpub.com/en-in/product/modern-python-cookbook-9781835460757)]
现代 Python 食谱
Steven F. Lott
ISBN: 9781835466384
-
掌握核心 Python 数据结构、算法和设计模式
-
实现面向对象的设计和函数式编程特性
-
使用类型匹配和注解来编写更具表达性的程序
-
使用 Matplotlib 和 Pyplot 创建有用的数据可视化
-
有效管理项目依赖和虚拟环境
-
遵循代码风格和测试的最佳实践
-
为你的项目创建清晰和可信的文档
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/lrn-py-prog-4e/img/9781800207721.jpg[(https://www.packtpub.com/en-in/product/mastering-python-2e-9781800202108)]
精通 Python 2E
Rick Hattem
ISBN: 9781800207721
-
编写优美的 Python 代码并避免常见的 Python 编码错误
-
应用装饰器、生成器、协程和元类的力量
-
使用不同的测试系统,如 pytest、unittest 和 doctest
-
跟踪和优化应用程序的性能,包括内存和 CPU 使用
-
使用 PDB、Werkzeug 和 faulthandler 调试你的应用程序
-
通过 asyncio、多进程和分布式计算提高你的性能
-
探索流行的库,如 Dask、NumPy、SciPy、pandas、TensorFlow 和 scikit-learn
-
使用 C/C++库和系统调用扩展 Python 的功能
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并申请。我们已经与成千上万的开发者和科技专业人士合作,就像你一样,帮助他们将见解分享给全球科技社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。
分享你的想法
现在你已经完成了 Python 编程学习 第四版,我们非常想听听你的想法!如果你在亚马逊购买了这本书,请点击此处直接进入该书的亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。
你的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
更多推荐
所有评论(0)