本章重点介绍编写和发布 Python 包的可重复过程。其意图是:
- 在开始实际工作之前,缩短设置所有内容所需的时间
- 提供编写包的标准化方法
- 简化测试驱动开发方法的使用
- 以促进释放过程
它分为以下四个部分:
- 所有包的通用****模式,描述了所有 Python 包之间的相似性,以及
distutils和setuptools如何发挥核心作用 - 什么是命名空间包以及它们为什么有用
- 如何在Python 包索引(PyPI中注册和上传包,重点关注安全性和常见陷阱
- 独立可执行文件作为打包和分发 Python 应用程序的替代方式
Python 打包一开始可能有点势不可挡。主要原因是对创建 Python 包的适当工具的混淆。无论如何,一旦您创建了第一个包,您将看到这并不像看上去那么困难。此外,了解适当的、最先进的包装工具也会有很大帮助。
您应该知道如何创建包,即使您对将代码作为开放源代码分发不感兴趣。了解如何创建自己的产品将使您能够更深入地了解打包生态系统,并将帮助您使用 PyPI 上可能使用的第三方代码。
此外,将封闭源代码项目或其组件作为源代码分发包提供可以帮助您在不同的环境中部署代码。下一章将更详细地描述在代码部署中利用 Python 打包生态系统的优势。在这里,我们将重点介绍创建此类发行版的适当工具和技术。
Python 打包的状态在很长一段时间内非常混乱,并且花了很多年的时间才将组织带入这个主题。一切从 1998 年推出的distutils套餐开始,之后在 2003 年通过setuptools进行了增强。这两个项目开始了一个漫长而复杂的故事,包括 forks、替代项目和试图一劳永逸地修复 Python 打包生态系统的完整重写。不幸的是,这些尝试大多从未成功。结果恰恰相反。每一个旨在取代setuptools或distutils的新项目都只会加剧包装工具已经存在的巨大混乱。一些这样的叉子被合并回他们的祖先(如distribute是setuptools的叉子),但一些被遗弃(如distutils2。
幸运的是,这种状态正在逐渐改变。成立了一个名为Python 包装机构(PyPA的组织,将秩序和组织带回包装生态系统。Python 打包用户指南(https://packaging.python.org 由 PyPA 维护的,是关于最新包装工具和最佳实践的权威信息源。将其视为有关包装的最佳信息来源和本章的补充阅读。该指南还包含与打包相关的更改和新项目的详细历史记录,因此,如果您已经知道一点,但希望确保仍然使用适当的工具,则该指南将非常有用。
远离其他热门互联网资源,如搭便车者包装指南。它很旧,没有维护,而且大部分都过时了。它可能只是出于历史原因才有意思,《Python 打包用户指南》实际上就是这个老资源的一部分。
PyPA 除了为包装提供权威指南外,还维护包装项目和包装新官方方面的标准化流程。PyPA 的所有项目都可以在 GitHub 上的一个组织下找到:https://github.com/pypa 。
书中已经提到了其中一些。最值得注意的是:
pipvirtualenvtwinewarehouse
请注意,它们中的大多数都是在该组织之外创建的,并且仅在 PyPA 的赞助下作为成熟和广泛的解决方案移动。
多亏了 PyPA 的参与,逐渐放弃了 eggs 格式,取而代之的是用于构建发行版的轮子。未来可能会给我们带来更清新的气息。PyPA 正在积极开发warehouse,旨在完全取代当前的 PyPI 实现。这将是包装史上的一大步,因为pypi是一个如此古老而被忽视的项目,只有少数人可以想象不需要完全重写就可以逐步改进它。
PythonPackaging 用户指南提供了一些关于使用包的推荐工具的建议。它们通常可以分为两组:用于安装包的工具和用于创建和分发包的工具。
PyPA 推荐的第一个组中的实用程序已经在第 1 章、Python 的当前状态中提到,但为了一致性,让我们在这里重复它们:
- 使用
pip安装 PyPI 的软件包 - 使用
virtualenv或venv对 Python 环境进行应用程序级隔离
Python 打包用户指南对包创建和分发工具的建议如下:
- 使用
setuptools定义项目并创建源分布 - 使用轮支持蛋创建内置分发
- 使用
twine将包分发上传到 PyPI
显然,组织大型应用程序代码的最简单方法是将其拆分为几个包。这使得代码更简单,更易于理解、维护和更改。它还最大限度地提高了每个包的可重用性。它们的作用类似于组件。
必须分发的包的根目录包含一个setup.py脚本。它定义了distutils模块中描述的所有元数据,在对标准setup()函数的调用中组合为参数。尽管distutils是一个标准库模块,但建议您使用setuptools软件包,该软件包为标准distutils提供了多项增强功能。
因此,此文件的最小内容为:
from setuptools import setup
setup(
name='mypackage',
)name给出了包的全名。从那里,脚本提供了几个命令,这些命令可以通过–-help-commands选项列出:
$ python3 setup.py --help-commands
Standard commands:
build build everything needed to install
clean clean up temporary files from 'build' command
install install everything from build directory
sdist create a source distribution (tarball, zip file)
register register the distribution with the PyP
bdist create a built (binary) distribution
check perform some checks on the package
upload upload binary package to PyPI
Extra commands:
develop install package in 'development mode'
alias define a shortcut to invoke one or more commands
test run unit tests after in-place build
bdist_wheel create a wheel distribution
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: setup.py --help [cmd1 cmd2 ...]
or: setup.py --help-commands
or: setup.py cmd --help实际的命令列表较长,并且根据可用的setuptools扩展名而有所不同。它被截断,只显示那些最重要和与本章相关的内容。标准****命令是distutils提供的内置命令,而额外的****命令是由setuptools等第三方包或任何定义和注册新命令的其他包创建的命令。由另一个包注册的一个这样的额外命令是由wheel包提供的bdist_wheel。
setup.cfg文件包含setup.py脚本命令的默认选项。如果构建和分发包的过程更复杂,并且需要将许多可选参数传递给setup.py命令,那么这非常有用。这允许您以每个项目为基础在代码中存储此类默认参数。这将使您的分发流独立于项目,并且还提供了关于如何构建包以及如何将包分发给用户和其他团队成员的透明度。
setup.cfg文件的语法与内置configparser模块提供的相同,因此与流行的 Microsoft Windows INI 文件类似。下面是一个设置配置文件的示例,它提供了一些global、sdist和bdist_wheel命令默认值:
[global]
quiet=1
[sdist]
formats=zip,tar
[bdist_wheel]
universal=1此示例配置将确保始终使用两种格式(ZIP 和 TAR)创建源发行版,并且构建的控制盘发行版将创建为通用控制盘(独立于 Python 版本)。此外,全局quiet开关会在每个命令上抑制大部分输出。请注意,这仅用于演示目的,默认情况下禁止每个命令的输出可能不是一个合理的选择。
当使用sdist命令构建分发时,distutils浏览包目录,查找要包含在存档中的文件。distutils将包括:
py_modules、packages和scripts选项所暗示的所有 Python 源文件ext_modules选项中列出的所有 C 源文件
与 glob 模式test/test*.py匹配的文件有:README、README.txt、setup.py和setup.cfg。
此外,如果您的软件包处于 subversion 或 CVS 下,sdist将浏览.svn等文件夹以查找要包含的文件。通过扩展,还可以与其他版本控制系统集成。sdist构建一个MANIFEST文件,列出所有文件并将其包含到存档中。
假设您没有使用这些版本控制系统,并且需要包含更多文件。现在您可以在MANIFEST文件的setup.py目录下定义一个名为MANIFEST.in的模板,在该目录下您可以向sdist指示要包含哪些文件。
此模板每行定义一条包含或排除规则,例如:
include HISTORY.txt
include README.txt
include CHANGES.txt
include CONTRIBUTORS.txt
include LICENSE
recursive-include *.txt *.pyMANIFEST.in命令的完整列表可在官方distutils文档中找到。
除了分发的包的名称和版本外,setup可以接收的最重要的参数有:
description:这包括几个句子来描述包装long_description:这包括一个完整的描述,可以在 StructuredText 中keywords:这是定义包的关键字列表author:作者姓名或单位author_email:这是联系人的电子邮件地址url:这是项目的 URLlicense:这是许可证(GPL、LGPL 等)packages:这是包装中所有名称的列表;setuptools提供了一个名为find_packages的小函数,用于计算该值namespace_packages:这是名称空间包的列表
PyPI 和distutils提供了一个解决方案,用于使用名为Tve 分类器的分类器集对应用程序进行分类。所有分类器形成一个树状结构。每个分类器都是一种字符串形式,其中每个名称空间由::子字符串分隔。它们的列表作为setup()函数的classifiers参数提供给包定义。以下是 PyPI 上可用的某些项目的分类器示例列表(此处为solrq:
from setuptools import setup
setup(
name="solrq",
# (...)
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Internet :: WWW/HTTP :: Indexing/Search',
],
)它们在包定义中是完全可选的,但为setup()接口中可用的基本元数据提供了有用的扩展。除其他外,trove 分类器可以提供有关受支持的 Python 版本或系统、项目开发阶段或代码发布许可证的信息。许多 PyPI 用户按类别搜索和浏览可用的包,因此适当的分类有助于包达到其目标。
Trove 分类器在整个包装生态系统中发挥着重要作用,不容忽视。没有组织验证软件包分类,因此您有责任为您的软件包提供适当的分类器,并且不会给整个软件包索引带来混乱。
在撰写本书时,PyPI 上有 608 个分类器,分为九大类:
- 发展状况
- 环境
- 框架
- 目标受众
- 许可证
- 自然语言
- 操作系统
- 程序设计语言
- 话题
新的分类符会不时添加,因此在您阅读时,这些数字可能会有所不同。当前可用的 trove 分类器的完整列表可通过setup.py register --list-classifiers命令获得。
为发行版创建包对于经验不足的开发人员来说可能是一项乏味的任务。setuptools或distuitls在其setup()函数调用中接受的大部分元数据都可以手动提供,忽略了在项目的其他部分可能提供的事实:
from setuptools import setup
setup(
name="myproject",
version="0.0.1",
description="mypackage project short description",
long_description="""
Longer description of mypackage project
possibly with some documentation and/or
usage examples
""",
install_requires=[
'dependency1',
'dependency2',
'etc',
]
)虽然这肯定会起作用,但很难长期维持,并为未来的错误和不一致留下了余地。setuptools和distutils都不能自动从项目源中选取各种元数据信息,需要您自己提供。Python 社区中有一些常见的模式用于解决最流行的问题,如依赖关系管理、版本/自述文件包含等。至少了解其中一些是值得的,因为它们非常受欢迎,可以被视为包装习惯用语。
**PEP 440(版本标识和依赖性规范)**文件规定了版本和依赖性规范的标准。这是一篇很长的文档,涵盖了公认的版本规范方案,以及 Python 打包工具中的版本匹配和比较应该如何工作。如果您正在使用或计划使用复杂的项目版本编号方案,则必须阅读本文档。如果您使用的是由一个、两个、三个或更多由点分隔的数字组成的简单方案,那么您可以忽略 PEP440 的读数。如果您不知道如何选择合适的版本控制方案,我强烈建议您遵循第 1 章、Python 当前状态中已经提到的语义版本控制。
另一个问题是在何处包含包或模块的版本说明符。PEP 396(模块版本号)正好解决了这个问题。请注意,它只是信息性的,并且具有延迟状态,因此它不是标准轨道的一部分。无论如何,它描述了现在似乎是一个事实上的标准。根据 PEP 396,如果包或模块具有指定的版本,则应将其作为包根(__init__.py或模块文件的__version__属性包含。另一个事实上的标准是还包括包含版本部分元组的VERSION属性。这有助于用户编写兼容代码,因为如果版本控制方案足够简单,则可以轻松比较此类版本元组。
PyPI 上提供的许多包都遵循这两个标准。他们的 __init__.py文件包含如下版本属性:
# version as tuple for simple comparisons
VERSION = (0, 1, 1)
# string created from tuple to avoid inconsistency
__version__ = ".".join([str(x) for x in VERSION])延期 PEP 396 的另一个建议是,distutils 的setup()函数中提供的版本应派生自__version__,反之亦然。PythonPackagingUserGuide 为单个外包项目版本提供了多种模式,每种模式都有其自身的优势和局限性。我个人的最爱是相当长的,不包括在 PyPA 的指南中,但其优点是将复杂性限制在setup.py脚本中。此锅炉板假定版本说明符由包的__init__模块的VERSION属性提供,并提取此数据以包含在setup()调用中。以下是某个虚构软件包的setup.py脚本的摘录,该脚本介绍了这种方法:
from setuptools import setup
import os
def get_version(version_tuple):
# additional handling of a,b,rc tags, this can
# be simpler depending on your versioning scheme
if not isinstance(version_tuple[-1], int):
return '.'.join(
map(str, version_tuple[:-1])
) + version_tuple[-1]
return '.'.join(map(str, version_tuple))
# path to the packages __init__ module in project
# source tree
init = os.path.join(
os.path.dirname(__file__), 'src', 'some_package', '__init__.py'
)
version_line = list(
filter(lambda l: l.startswith('VERSION'), open(init))
)[0]
# VERSION is a tuple so we need to eval its line of code.
# We could simply import it from the package but we
# cannot be sure that this package is importable before
# finishing its installation
VERSION = get_version(eval(version_line.split('=')[-1]))
setup(
name='some-package',
version=VERSION,
# ...
)Python 打包索引可以在 PyPI 门户的包页面上显示项目的自述文件或long_description的值。您可以使用 StructuredText(来编写此描述 http://docutils.sourceforge.net/rst.html )标记,因此上传时将格式化为 HTML。不幸的是,目前只有 StructuredText 可作为 PyPI 上的文档标记使用。这在不久的将来不太可能改变。更可能的是,当我们看到warehouse项目完全取代当前的 PyPI 实现时,将支持其他标记语言。不幸的是,warehouse的最终版本仍然未知。
尽管如此,许多开发人员出于各种原因希望使用不同的标记语言。最流行的选择是 Markdown,这是 GitHub 上的默认标记语言,目前大多数开源 Python 开发都在 GitHub 上进行。因此,通常,GitHub 和 Markdown 爱好者要么忽略这个问题,要么提供两个独立的文档文本。提供给 PyPI 的描述要么是项目 GitHub 页面上可用内容的简短版本,要么是 PyPI 上没有很好呈现的纯无格式标记。
如果您想在项目的自述中使用与 StructuredText 标记语言不同的东西,您仍然可以在 PyPI 页面上以可读的形式提供它作为项目描述。诀窍在于使用pypandoc包将其他标记语言转换为 StructuredText,同时将包上载到 Python 包索引。重要的是要回退到自述文件的普通内容,这样如果用户没有安装pypandoc,安装就不会失败:
try:
from pypandoc import convert
def read_md(f):
return convert(f, 'rst')
except ImportError:
convert = None
print(
"warning: pypandoc module not found, could not convert Markdown to RST"
)
def read_md(f):
return open(f, 'r').read() # noqa
README = os.path.join(os.path.dirname(__file__), 'README.md')
setup(
name='some-package',
long_description=read_md(README),
# ...
)许多项目需要安装和/或使用一些外部软件包。当依赖项列表很长时,就会出现如何管理它的问题。在大多数情况下,答案很简单。不要过度设计这个问题。保持简单,并在setup.py脚本中明确提供依赖项列表:
from setuptools import setup
setup(
name='some-package',
install_requires=['falcon', 'requests', 'delorean']
# ...
)一些 Python 开发人员喜欢使用requirements.txt文件来跟踪其包的依赖项列表。在某些情况下,您可能会找到这样做的原因,但在大多数情况下,这是该项目的代码没有正确打包的时代遗留下来的。无论如何,即使像芹菜这样著名的项目仍然坚持这一惯例。所以,如果你不愿意改变你的习惯,或者你被迫使用需求文件,那么至少要正确地去做。下面是从requirements.txt文件中读取依赖项列表的常用习惯用法之一:
from setuptools import setup
import os
def strip_comments(l):
return l.split('#', 1)[0].strip()
def reqs(*f):
return list(filter(None, [strip_comments(l) for l in open(
os.path.join(os.getcwd(), *f)).readlines()]))
setup(
name='some-package',
install_requires=reqs('requirements.txt')
# ...
)distutils允许您创建新命令。一个新的命令可以注册到一个入口点,这是由setuptools引入的一种将包定义为插件的简单方法。
入口点是指向类或函数的命名链接,可通过setuptools中的某些 API 使用。任何应用程序都可以扫描所有已注册的包,并将链接的代码用作插件。
要链接新命令,entry_points元数据可用于设置调用:
setup(
name="my.command",
entry_points="""
[distutils.commands]
my_command = my.command.module.Class
"""
)所有命名链接都收集在命名部分中。加载distutils时,它会扫描在distutils.commands下注册的链接。
许多提供可扩展性的 Python 应用程序都使用这种机制。
与setuptools合作主要是构建和分发包。但是,您仍然需要知道如何使用它们直接从项目源安装软件包。原因很简单。在将包提交给 PyPI 之前,最好测试您的打包代码是否正常工作。最简单的测试方法是安装它。如果您将向存储库发送一个损坏的包,那么为了重新上载它,您需要增加版本号。
在最终发行版之前测试代码是否正确打包,可以避免不必要的版本号膨胀和明显的时间浪费。此外,在同时处理多个相关软件包时,使用setuptools直接从您自己的来源进行安装可能是必不可少的。
install命令将包安装到 Python 环境中。如果之前没有构建,它将尝试构建包,然后将结果注入 Python 树。提供源发行版后,可以将其解压缩到临时文件夹中,然后使用此命令安装。install命令还将安装install_requires元数据中定义的依赖项。这是通过查看 Python 包索引中的包来完成的。
在安装软件包时,除了裸的setup.py脚本之外,还有一种替代方法是使用pip。因为它是 PyPA 推荐的工具,所以即使在本地环境中安装包进行开发,也应该使用它。要从本地源安装软件包,请运行以下命令:
pip install <project-path>令人惊讶的是,setuptools和distutils缺少uninstall命令。幸运的是,可以使用pip卸载任何 Python 包:
pip uninstall <package-name>在系统范围的软件包上尝试卸载时,可能是一个危险的操作。这也是为什么在任何开发中使用虚拟环境都如此重要的另一个原因。
使用setup.py install安装的软件包将复制到您当前环境的站点软件包目录中。这意味着无论何时更改该软件包的源,都需要重新安装它。在密集型开发期间,这通常是一个问题,因为很容易忘记再次执行安装的需要。这就是为什么setuptools提供了一个额外的develop命令,允许我们以开发模式安装软件包。此命令在部署目录(站点包)中创建指向项目源的特殊链接,而不是在那里复制整个包。软件包源无需重新安装即可编辑,并可在sys.path中正常安装。
pip也允许以这种方式安装软件包。此安装选项称为可编辑模式,可通过install命令中的-e参数启用:
pip install -e <project-path>Python 的禅宗,您可以通过在解释器会话中写入import this来阅读,它对名称空间有如下说明:
名称空间是一个非常好的主意,让我们多做一些吧!
这至少可以从两个方面理解。第一个是语言上下文中的名称空间。我们都使用名称空间,甚至不知道:
- 模块的全局命名空间
- 函数或方法调用的本地命名空间
- 内置名称的命名空间
另一种名称空间可以在打包级别提供。这些是命名空间包。这是一个经常被忽略的特性,在组织或大型项目中构建包生态系统时非常有用。
命名空间包可以理解为一种将相关包或模块分组到元包级别以上的方式,其中每个包都可以独立安装。
命名空间如果您独立开发、打包和版本控制应用程序组件,但仍希望从同一命名空间访问它们,那么命名空间包尤其有用。这有助于明确每个包属于哪个组织或项目。例如,对于某个虚构的 Acme 公司,公共名称空间可以是acme。结果可能导致创建通用的acme名称空间包,该名称空间包将用作此组织中其他包的容器。例如,如果来自 Acme 的人希望通过一个 SQL 相关库为这个名称空间做出贡献,那么他可以创建一个新的acme.sql包,在acme中注册自己。
了解普通包和命名空间包之间的区别以及它们解决的问题很重要。通常(没有名称空间包),您将创建一个包acme,其中包含具有以下文件结构的sql子包/子模块:
$ tree acme/
acme/
├── acme
│ ├── __init__.py
│ └── sql
│ └── __init__.py
└── setup.py
2 directories, 3 files每当您想要添加新的子包时,比如说templating,您必须将其包含在acme的源树中:
$ tree acme/
acme/
├── acme
│ ├── __init__.py
│ ├── sql
│ │ └── __init__.py
│ └── templating
│ └── __init__.py
└── setup.py
3 directories, 4 files这种方法使得acme.sql和acme.templating的独立开发几乎不可能。setup.py脚本还必须为每个子包指定所有依赖项,因此不可能(或至少非常困难)选择性地安装一些acme组件。此外,如果某些子包的要求相互冲突,则这是一个无法解决的问题。
使用命名空间包,您可以独立地存储每个子包的源树:
$ tree acme.sql/
acme.sql/
├── acme
│ └── sql
│ └── __init__.py
└── setup.py
2 directories, 2 files
$ tree acme.templating/
acme.templating/
├── acme
│ └── templating
│ └── __init__.py
└── setup.py
2 directories, 2 files您还可以在 PyPI 或您使用的任何包索引中独立注册它们。用户可以从acme命名空间中选择要安装的子包,但他们从不安装通用acme包(它不存在):
$ pip install acme.sql acme.templating请注意,独立的源代码树不足以在 Python 中创建名称空间包。如果不想让包相互覆盖,则需要做一些额外的工作。此外,根据目标的 Python 语言版本,正确的处理可能会有所不同。下面两节将详细介绍这方面的内容。
如果您只使用和针对 Python3,那么对您来说是个好消息。**PEP 420(隐式命名空间包)**引入了一种定义命名空间包的新方法。它是标准轨道的一部分,自 3.3 版本起成为该语言的官方部分。简而言之,如果包含 Python 包或模块(也包括命名空间包)的每个目录不包含__init__.py文件,则将其视为命名空间包。因此,以下是上一节中介绍的文件结构示例:
$ tree acme.sql/
acme.sql/
├── acme
│ └── sql
│ └── __init__.py
└── setup.py
2 directories, 2 files
$ tree acme.templating/
acme.templating/
├── acme
│ └── templating
│ └── __init__.py
└── setup.py
2 directories, 2 files它们足以定义acme是 Python 3.3 及更高版本中的名称空间包。使用设置工具的最低setup.py脚本如下所示:
from setuptools import setup
setup(
name='acme.templating',
packages=['acme.templating'],
)不幸的是,setuptools.find_packages()在撰写本书时不支持 PEP 420。无论如何,这在未来可能会改变。此外,显式定义包列表的要求对于命名空间包的轻松集成来说似乎是一个非常小的代价。
无法使 PEP 420 布局中的名称空间包在早于 3.3 的 Python 版本中工作。尽管如此,这个概念非常古老,并且在像 Zope 这样的成熟项目中普遍使用,因此使用它们当然是可能的,但没有隐含的定义。在较早版本的 Python 中,有几种方法可以定义包应被视为名称空间。
最简单的方法是为每个组件创建一个文件结构,类似于没有名称空间包的普通包布局,并将所有内容留给setuptools。因此,acme.sql和acme.templating的示例布局可以如下所示:
$ tree acme.sql/
acme.sql/
├── acme
│ ├── __init__.py
│ └── sql
│ └── __init__.py
└── setup.py
2 directories, 3 files
$ tree acme.templating/
acme.templating/
├── acme
│ ├── __init__.py
│ └── templating
│ └── __init__.py
└── setup.py
2 directories, 3 files请注意,对于acme.sql和acme.templating,都有一个额外的源文件acme/__init__.py。这个必须留空。如果我们将此名称作为setuptools.setup()函数的namespace_packages关键字参数的值提供,则将创建acme命名空间包:
from setuptools import setup
setup(
name='acme.templating',
packages=['acme.templating'],
namespace_packages=['acme'],
)最简单并不意味着最好。setuptools为了注册一个新的名称空间,将在您的__init__.py文件中调用pkg_resources.declare_namespace()函数。即使__init__.py文件为空,也会发生这种情况。无论如何,正如官方文档所说,在__init__.py文件中声明名称空间是您自己的责任,并且setuptools的这种隐含行为可能会在将来被删除。为了安全和“经得起未来考验”,您需要在文件acme/__init__.py中添加以下行:
__import__('pkg_resources').declare_namespace(__name__)如果没有一种有组织的存储、上传和下载方式,软件包将毫无用处。Python 打包索引是 Python 社区中开源包的主要来源。任何人都可以自由上传新的软件包,唯一的要求是在 PyPI 网站上注册 https://pypi.python.org/pypi 。
当然,您不限于,只限于此索引,所有打包工具都支持使用替代包存储库。这对于在内部组织之间分发封闭源代码或用于部署目的特别有用。下一章将解释此类包装使用的详细信息以及如何创建自己的包装索引的说明。在这里,我们只关注到 PyPI 的开源上传,而很少提到如何指定替代存储库。
正如已经提到的,Python 包索引是开源包发行版的官方来源。从它下载不需要任何帐户或许可。您唯一需要的是一个包管理器,它可以从 PyPI 下载新的发行版。您的首选应该是pip。
只要注册了账户,任何人都可以注册并将软件包上传到 PyPI。软件包绑定到用户,因此,默认情况下,只有注册软件包名称的用户才是其管理员,并且可以上载新发行版。对于更大的项目来说,这可能是一个问题,因此可以选择将其他用户设计为包维护者,以便他们能够上传新的发行版。
上传包的最简单方法是使用setup.py脚本的upload命令:
$ python setup.py <dist-commands> upload这里,<dist-commands>是创建要上载的分发的命令列表。只有在同一setup.py执行期间创建的分发版本才会上载到存储库。因此,如果要同时上载源发行版、内置发行版和 wheel 软件包,则需要发出以下命令:
$ python setup.py sdist bdist bdist_wheel upload当使用setup.py上传时,您不能重用已经构建的发行版,并且在每次上传时都必须重新构建发行版。这可能有点道理,但对于大型或复杂的项目来说可能不太方便,因为在这些项目中创建分发实际上可能需要相当长的时间。setup.py upload的另一个问题是,它可以在某些 Python 版本上使用明文 HTTP 或未经验证的 HTTPS 连接。这就是为什么建议将twine作为setup.py upload的安全替代品。
Twine 是用于与 PyPI 交互的实用程序,PyPI 目前仅用于将包安全地上载到存储库的一个目的。它支持任何打包格式,并始终确保连接安全。它还允许您上传已经创建的文件,因此您可以在发布之前测试发行版。twine的一个示例用法仍然需要调用setup.py进行构建分发:
$ python setup.py sdist bdist_wheel
$ twine upload dist/*如果您尚未注册此软件包,则上载将失败,因为您需要先注册它。这也可以使用twine完成:
$ twine register dist/*.pypirc是一个配置文件,用于存储有关 Python 软件包存储库的信息。它应该位于您的主目录中。此文件的格式如下所示:
[distutils]
index-servers =
pypi
other
[pypi]
repository: <repository-url>
username: <username>
password: <password>
[other]
repository: https://example.com/pypi
username: <username>
password: <password>distutils部分应该有index-servers变量,该变量列出了描述所有可用存储库和凭证的所有部分。每个存储库部分只能修改三个变量:
repository:包存储库的 URL(默认为https://www.python.org/pypiusername:这是给定存储库中授权的用户名password:明文授权的用户密码
请注意,以明文形式存储存储库密码可能不是最明智的安全选择。您可以始终将其留空,并在必要时提示您输入。
为 Python 构建的每个打包工具都应该遵守.pypirc文件。虽然这可能不适用于所有与包装相关的实用程序,但最重要的实用程序如pip、twine、distutils和setuptools都支持这一点。
Python 包通常有两种类型的发行版:
- 源分布
- 内置(二进制)发行版
源分布是最简单、最独立于平台的。对于纯 Python 软件包来说,这是一个很简单的问题。这样的发行版只包含 Python 源代码,这些源代码应该已经具有很高的可移植性。
更复杂的情况是,当您的软件包引入一些用 C 编写的扩展时。如果软件包用户在其环境中拥有适当的开发工具链,那么源发行版仍然可以工作。这主要由编译器和适当的 C 头文件组成。对于这种情况,构建的分发格式可能更适合,因为它可能为特定平台提供已构建的扩展。
sdist命令是可用的最简单的命令。它创建了一个发布树,其中复制了运行包所需的所有内容。然后将该树归档到一个或多个归档文件中(通常,它只创建一个 tarball)。归档文件基本上是源代码树的副本。
此命令是从目标系统独立分发包的最简单方法。它创建了一个dist文件夹,其中包含可以分发的档案。为了能够使用它,必须向setup传递一个额外的参数以提供版本号。如果您不给它一个version值,它将使用version = 0.0.0:
from setuptools import setup
setup(name='acme.sql', version='0.1.1')此号码对于升级安装非常有用。每次发布包时,都会增加该数字,以便目标系统知道它已更改。
让我们用这个额外的参数运行sdist命令:
$ python setup.py sdist
running sdist
...
creating dist
tar -cf dist/acme.sql-0.1.1.tar acme.sql-0.1.1
gzip -f9 dist/acme.sql-0.1.1.tar
removing 'acme.sql-0.1.1' (and everything under it)
$ ls dist/
acme.sql-0.1.1.tar.gz在 Windows 下,存档将是一个 ZIP 文件。
该版本用于标记存档文件的名称,该文件可以分发并安装在任何具有 Python 的系统上。在sdist发行版中,如果包包含 C 库或扩展,则目标系统负责编译它们。这在基于 Linux 的系统或 Mac OS 中非常常见,因为它们通常提供编译器,但在 Windows 下使用编译器则不太常见。这就是为什么当一个包打算在多个平台下运行时,它也应该使用预构建的发行版进行发行。
为了能够分发预构建的分发版,distutils提供了build命令,该命令分四步编译包:
build_py:通过字节编译纯 Python 模块并将其复制到构建文件夹中,从而构建纯 Python 模块。build_clib:当包中包含任何库时,使用 C 编译器并在 build 文件夹中创建一个静态库来构建 C 库。build_ext:构建 C 扩展并将结果放入构建文件夹中,如build_clib。build_scripts:构建标记为脚本的模块。当设置第一行(!#时,它还会更改解释器路径,并修复文件模式,使其可执行。
每个步骤都是一个可以独立调用的命令。编译过程的结果是生成文件夹,其中包含安装包所需的所有内容。distutils包中还没有交叉编译器选项。这意味着命令的结果总是特定于它所构建的系统。
当必须创建一些 C 扩展时,构建过程使用系统编译器和 Python 头文件(Python.h。这个include文件是从 Python 从源代码构建时就可用的。对于打包发行版,您的系统发行版可能需要一个额外的包。至少在流行的 Linux 发行版中,它通常被命名为python-dev。它包含构建 Python 扩展所需的所有头文件。
使用的 C 编译器是系统编译器。对于基于 Linux 的系统或 Mac OS X,这将分别是gcc或clang。对于 Windows,微软 Visual C++可以使用(有免费的命令行版本可用),开源项目 MINW 也可以使用。可在distutils中配置。
build命令由bdist命令用于构建二进制分发。它调用build和所有相关命令,然后以与sdist相同的方式创建存档。
让我们在 Mac OS X 下为acme.sql创建一个二进制发行版:
$ python setup.py bdist
running bdist
running bdist_dumb
running build
...
running install_scripts
tar -cf dist/acme.sql-0.1.1.macosx-10.3-fat.tar .
gzip -f9 acme.sql-0.1.1.macosx-10.3-fat.tar
removing 'build/bdist.macosx-10.3-fat/dumb' (and everything under it)
$ ls dist/
acme.sql-0.1.1.macosx-10.3-fat.tar.gz acme.sql-0.1.1.tar.gz请注意,新创建的归档文件的名称包含系统的名称及其在(Mac OS X 10.3下构建的发行版)。
在 Windows 下调用的同一命令将创建特定的分发存档:
C:\acme.sql> python.exe setup.py bdist
...
C:\acme.sql> dir dist
25/02/2008 08:18 <DIR> .
25/02/2008 08:18 <DIR> ..
25/02/2008 08:24 16 055 acme.sql-0.1.win32.zip
1 File(s) 16 055 bytes
2 Dir(s) 22 239 752 192 bytes free如果一个包包含 C 代码,除了源代码发行版之外,发布尽可能多的不同二进制发行版是很重要的。至少,Windows 二进制发行版对于那些没有安装 C 编译器的人来说很重要。
二进制版本包含一个可以直接复制到 Python 树中的树。它主要包含一个复制到 Python 的site-packages文件夹中的文件夹。它还可能包含缓存的字节码文件(Python 2 上的*.pyc文件和 Python 3 上的__pycache__/*.pyc文件)。
另一种内置的发行版是wheel包提供的“轮子”。安装时(例如,使用pip,wheel向distutils添加新的bdist_wheel命令。它允许创建特定于平台的发行版(目前仅适用于 Windows 和 Mac OS X),提供正常bdist发行版的替代品。它的设计目的是取代先前由setuptools引入的另一个发行版——鸡蛋。鸡蛋现在已经过时了,所以这里不会介绍鸡蛋。使用轮子的优点清单很长。以下是在 PythonWheels 页面(中提到的内容 http://pythonwheels.com/ :
- 更快地安装纯 python 和本机 C 扩展包
- 避免安装时执行任意代码。(避免
setup.py) - 在 Windows 或 OS X 上安装 C 扩展不需要编译器
- 为测试和持续集成提供更好的缓存
- 创建
.pyc文件作为安装的一部分,以确保它们与使用的 Python 解释器匹配 - 跨平台和机器进行更一致的安装
根据 PyPA 建议,wheels 应该是您的默认分发格式。不幸的是,Linux 平台特定的控制盘还不可用,因此如果您必须分发带有 C 扩展的包,那么您需要为 Linux 用户创建sdist分发版。
在涉及 Python 代码打包的资料中,创建独立的可执行文件是一个经常被忽略的主题。这主要是因为 Python 的标准库中缺少适当的工具,这些工具允许程序员创建简单的可执行文件,用户无需安装 Python 解释器即可运行这些文件。
编译语言与 Python 相比有很大的优势,因为它们允许为给定的系统体系结构创建可执行的应用程序,该应用程序可以由用户以一种不需要他们了解任何底层技术的方式运行。Python 代码作为包分发时,需要 Python 解释器才能运行。这给技术熟练程度不够的用户带来了很大的不便。
Mac OS X 或大多数 Linux 发行版等开发人员友好的操作系统都预装了 Python。因此,对于他们的用户来说,基于 Python 的应用程序仍然可以作为源程序包分发,该源程序包依赖于主脚本文件中特定的解释器指令,通常称为shebang。对于大多数 Python 应用程序,其形式如下:
#!/usr/bin/env python当将此指令用作脚本的第一行时,它将标记为默认情况下由给定环境的 Python 版本对其进行解释。当然,这可以采用更详细的形式,需要特定的 Python 版本,如python3.4、python3或python2。请注意,这将在大多数流行的 POSIX 系统中工作,但根据定义,它根本不是可移植的。此解决方案依赖于特定 Python 版本的存在,以及在/usr/bin/env位置提供env可执行文件。这两种假设在某些操作系统上都可能失败。此外,shebangs 在 Windows 上根本不起作用。此外,即使对于有经验的开发人员来说,在 Windows 上引导 Python 环境也是一项挑战,因此您不能期望非技术用户能够自己完成这项工作。
另一个需要考虑的是桌面环境中的简单用户体验。用户通常希望只需单击应用程序就可以从桌面上运行它们。并不是每个桌面环境都支持将 Python 应用程序作为源代码分发。
因此,最好是我们能够创建一个二进制发行版,它可以像任何其他编译的可执行文件一样工作。幸运的是,可以创建一个同时嵌入 Python 解释器和项目的可执行文件。这允许用户打开我们的应用程序,而不必关心 Python 或任何其他依赖关系。
在用户体验的简单性比用户干扰应用程序代码的能力更重要的情况下,独立可执行文件非常有用。请注意,将应用程序作为可执行文件分发只会使代码读取或修改变得更困难,而不是不可能。它不是保护应用程序代码的一种方法,而应该仅用作简化与应用程序交互的一种方法。
独立可执行文件应该是为非技术最终用户分发应用程序的首选方式,而且似乎也是为 Windows 分发 Python 应用程序的唯一合理方式。
对于以下情况,独立可执行文件通常是一个不错的选择:
- 依赖特定 Python 版本的应用程序,这些版本在目标操作系统上可能不容易获得
- 依赖修改的预编译 CPython 源的应用程序
- 具有图形界面的应用程序
- 具有许多用不同语言编写的二进制扩展的项目
- 游戏
Python 没有任何内置的对构建独立可执行文件的支持。幸运的是,有一些社区项目解决了这个问题,取得了不同程度的成功。最值得注意的四个方面是:
- PyInstaller
- 冷冻
- py2exe
- py2app
它们在使用上都略有不同,而且它们的局限性也略有不同。在选择工具之前,您需要决定要针对哪个平台,因为每个打包工具只能支持一组特定的操作系统。
最好的情况是,如果您在项目生命的一开始就做出这样的决定。当然,这些工具都不需要在代码中进行深入的交互,但是如果您尽早开始构建独立的包,您可以自动化整个过程,并节省未来的集成时间和成本。如果您稍后再讨论这个问题,您可能会发现自己处于这样一种情况:项目是以一种非常复杂的方式构建的,所有可用的工具都无法工作。为这样一个项目提供一个独立的可执行文件将是有问题的,并且会占用您大量的时间。
PyInstaller(http://www.pyinstaller.org/ 是迄今为止将 Python 包冻结为独立可执行文件的最高级程序。它提供了目前所有可用解决方案中最广泛的多平台兼容性,因此是最推荐的解决方案。PyInstaller 支持的平台包括:
- Windows(32 位和 64 位)
- Linux(32 位和 64 位)
- Mac OS X(32 位和 64 位)
- FreeBSD、Solaris 和 AIX
支持的 Python 版本有 Python 2.7 和 Python 3.3、3.4 和 3.5。它在 PyPI 上可用,因此可以使用pip将其安装在您的工作环境中。如果以这种方式安装时遇到问题,可以始终从项目页面下载安装程序。
不幸的是,不支持跨平台构建(交叉编译),因此如果要为特定平台构建独立的可执行文件,则需要在该平台上执行构建。如今,随着许多虚拟化工具的出现,这并不是什么大问题。如果您的计算机上没有安装特定的系统,您可以始终使用 Vagrant 作为虚拟机为您提供所需的操作系统。
简单应用程序的使用非常简单。假设我们的应用程序包含在名为myscript.py的脚本中。这是一个简单的“Hello world!”应用程序。我们想为 Windows 用户创建一个独立的可执行文件,我们的源代码位于文件系统的D://dev/app下。我们的应用程序可以与以下短命令捆绑在一起:
$ pyinstaller myscript.py
2121 INFO: PyInstaller: 3.1
2121 INFO: Python: 2.7.10
2121 INFO: Platform: Windows-7-6.1.7601-SP1
2121 INFO: wrote D:\dev\app\myscript.spec
2137 INFO: UPX is not available.
2138 INFO: Extending PYTHONPATH with paths
['D:\\dev\\app', 'D:\\dev\\app']
2138 INFO: checking Analysis
2138 INFO: Building Analysis because out00-Analysis.toc is non existent
2138 INFO: Initializing module dependency graph...
2154 INFO: Initializing module graph hooks...
2325 INFO: running Analysis out00-Analysis.toc
(...)
25884 INFO: Updating resource type 24 name 2 language 1033PyInstaller 的标准输出非常长,即使对于简单的应用程序也是如此,因此为了简洁起见,在前面的示例中对其进行了截断。如果在 Windows 上运行,则生成的目录和文件结构如下:
$ tree /0066
│ myscript.py
│ myscript.spec
│
├───build
│ └───myscript
│ myscript.exe
│ myscript.exe.manifest
│ out00-Analysis.toc
│ out00-COLLECT.toc
│ out00-EXE.toc
│ out00-PKG.pkg
│ out00-PKG.toc
│ out00-PYZ.pyz
│ out00-PYZ.toc
│ warnmyscript.txt
│
└───dist
└───myscript
bz2.pyd
Microsoft.VC90.CRT.manifest
msvcm90.dll
msvcp90.dll
msvcr90.dll
myscript.exe
myscript.exe.manifest
python27.dll
select.pyd
unicodedata.pyd
_hashlib.pyddist/myscript目录包含构建的应用程序,现在可以分发给用户。请注意,必须分发整个目录。它包含运行应用程序所需的所有附加文件(DLL、编译的扩展库等)。通过pyinstaller命令的--onefile开关可以获得更紧凑的分布:
$ pyinstaller --onefile myscript.py
(...)
$ tree /f
├───build
│ └───myscript
│ myscript.exe.manifest
│ out00-Analysis.toc
│ out00-EXE.toc
│ out00-PKG.pkg
│ out00-PKG.toc
│ out00-PYZ.pyz
│ out00-PYZ.toc
│ warnmyscript.txt
│
└───dist
myscript.exe当使用--onefile选项构建时,您需要分发给其他用户的唯一文件是dist目录中的单个可执行文件(此处为myscript.exe。对于小型应用程序,这可能是首选选项。
运行pyinstaller命令的一个副作用是创建*.spec文件。这是一个自动生成的 Python 模块,包含如何从源代码创建可执行文件的规范。例如,我们已经在以下代码中使用了此选项:
# -*- mode: python -*-
block_cipher = None
a = Analysis(['myscript.py'],
pathex=['D:\\dev\\app'],
binaries=None,
datas=None,
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='myscript',
debug=False,
strip=False,
upx=True,
console=True )此.spec文件包含前面指定的所有pyinstaller参数。如果您已经对构建执行了大量自定义,这将非常有用,因为这可以用来代替构建必须存储配置的脚本。创建后,您可以将其用作pyinstaller命令的参数,而不是 Python 脚本:
$ pyinstaller.exe myscript.spec请注意,这是一个真正的 Python 模块,因此您可以使用您已经知道的语言对其进行扩展,并对构建过程执行更复杂的自定义。当您针对许多不同的平台时,定制.spec文件尤其有用。此外,并非所有的pyinstaller选项都可以通过命令行参数使用,并且只能在修改.spec文件时使用。
PyInstaller 是一个广泛的工具,它的使用对于大多数程序来说都非常简单。无论如何,如果您有兴趣将其用作分发应用程序的工具,建议您彻底阅读其文档。
cx_ 冻结(http://cx-freeze.sourceforge.net/ 是另一个用于创建独立可执行文件的工具。它是一个比 PyInstaller 更简单的解决方案,但也支持三大平台:
- 窗户
- Linux
- MacOSX
与 PyInstaller 相同,它不允许我们执行跨平台构建,因此您需要在分发到的同一操作系统上创建可执行文件。cx_ 冻结的主要缺点是它不允许我们创建真正的单文件可执行文件。使用它构建的应用程序需要与相关的 DLL 文件和库一起分发。假设我们的应用程序与PyInstaller部分中的应用程序相同,那么示例用法也非常简单:
$ cxfreeze myscript.py
copying C:\Python27\lib\site-packages\cx_Freeze\bases\Console.exe -> D:\dev\app\dist\myscript.exe
copying C:\Windows\system32\python27.dll -> D:\dev\app\dist\python27.dll
writing zip file D:\dev\app\dist\myscript.exe
(...)
copying C:\Python27\DLLs\bz2.pyd -> D:\dev\app\dist\bz2.pyd
copying C:\Python27\DLLs\unicodedata.pyd -> D:\dev\app\dist\unicodedata.pyd生成的文件结构如下所示:
$ tree /f
│ myscript.py
│
└───dist
bz2.pyd
myscript.exe
python27.dll
unicodedata.pydcx_Freeze 扩展了distutils包,而不是像 PyInstaller 那样为构建规范提供自己的格式。这意味着您可以配置如何使用熟悉的setup.py脚本构建独立可执行文件。如果您已经使用setuptools或distutils分发了包,那么 cx_Freeze 将非常方便,因为额外的集成只需要对setup.py脚本进行小的更改。下面是使用cx_Freeze.setup()在 Windows 上创建独立可执行文件的setup.py脚本示例:
import sys
from cx_Freeze import setup, Executable
# Dependencies are automatically detected, but it might need fine tuning.
build_exe_options = {"packages": ["os"], "excludes": ["tkinter"]}
setup(
name="myscript",
version="0.0.1",
description="My Hello World application!",
options={
"build_exe": build_exe_options
},
executables=[Executable("myscript.py")]
)有了这样一个文件,可以使用添加到setup.py脚本中的新build_exe命令创建新的可执行文件:
$ python setup.py build_execx_Freeze 的使用似乎比 PyInstaller 的要简单一些,而且distutils集成是一个非常有用的特性。不幸的是,该项目可能会给缺乏经验的开发人员带来一些麻烦:
- 在 Windows 下使用
pip进行安装可能有问题 - 官方文件非常简短,有些地方缺乏
py2exe(http://www.py2exe.org/ 和 py2app(https://pythonhosted.org/py2app/ 是另外两个通过distutils或setuptools与 Python 打包集成以创建独立可执行文件的程序。这里将它们放在一起,因为它们在用法和局限性上非常相似。py2exe 和 py2app 的主要缺点是它们只针对单个平台:
- py2exe 允许构建 Windows 可执行文件
- py2app 允许构建 Mac OS X 应用程序
因为用法非常相似,只需要修改setup.py脚本,所以这些包似乎是相辅相成的。py2app 的文档展示了setup.py脚本的以下示例,该脚本允许使用正确的工具(py2exe 或 py2app)构建独立的可执行文件,具体取决于所使用的平台:
import sys
from setuptools import setup
mainscript = 'MyApplication.py'
if sys.platform == 'darwin':
extra_options = dict(
setup_requires=['py2app'],
app=[mainscript],
# Cross-platform applications generally expect sys.argv to
# be used for opening files.
options=dict(py2app=dict(argv_emulation=True)),
)
elif sys.platform == 'win32':
extra_options = dict(
setup_requires=['py2exe'],
app=[mainscript],
)
else:
extra_options = dict(
# Normally unix-like platforms will use "setup.py install"
# and install the main script as such
scripts=[mainscript],
)
setup(
name="MyApplication",
**extra_options
)有了这样一个脚本,可以使用python setup.py py2exe命令构建 Windows 可执行文件,使用python setup.py py2app构建 Mac OS X 应用程序。交叉编译当然是不可能的。
尽管存在一些限制,并且比 PyInstaller 或 cx_Freeze 更缺乏弹性,但知道总是有 py2exe 和 py2app 项目是件好事。在某些情况下,PyInstaller 或 cx_Freeze 可能无法为项目正确构建可执行文件。在这种情况下,总是值得检查其他解决方案是否可以处理我们的代码。
了解独立可执行文件不会以任何方式使应用程序代码安全,这一点很重要。从这样的可执行文件中反编译嵌入式代码不是一件容易的事情,但它确实是可行的。更重要的是,这种反编译(如果使用适当的工具完成)的结果可能与原始源代码惊人地相似。
这一事实使得独立 Python 可执行文件对于封闭源代码项目来说不是一个可行的解决方案,因为应用程序代码的泄漏可能会损害组织。因此,如果只需复制应用程序的源代码就可以复制整个业务,那么您应该考虑其他方法来分发应用程序。也许将软件作为服务提供对您来说是一个更好的选择。
如前所述,目前还没有可靠的方法可以使用可用的工具保护应用程序不被反编译。尽管如此,还是有一些方法使这一过程更加困难。但更难并不意味着可能性更小。对我们中的一些人来说,最诱人的挑战是最艰难的挑战。我们都知道,这个挑战的最终目标是非常高的:您试图保护的代码。
反编译的过程通常包括几个步骤:
- 从独立可执行文件中提取项目字节码的二进制表示形式。
- 将二进制表示映射到特定 Python 版本的字节码。
- 字节码到 AST 的翻译。
- 直接从 AST 复制资源。
出于显而易见的原因,提供阻止开发人员对独立可执行文件进行反向工程的确切解决方案是毫无意义的。因此,以下只是一些阻碍反编译过程或降低其结果的想法:
- 删除运行时可用的任何代码元数据(docstrings),因此最终结果的可读性会降低一点
- 修改 CPython 解释器使用的字节码值,以便从二进制转换为字节码,然后再转换为 AST 需要更多的工作
- 使用以如此复杂的方式修改的 CPython 源的版本,即使应用程序的反编译源可用,如果不反编译修改后的 CPython 二进制文件,它们也是无用的
- 在将源代码绑定到可执行文件之前,对源代码使用模糊处理脚本,这将使源代码在反编译后的价值降低
这样的解决方案使得开发过程更加困难。上面的一些想法需要对 Python 运行时有非常深刻的理解,但它们中的每一个都充满了许多陷阱和缺点。大多数情况下,他们只是推迟了不可避免的事情。一旦你的把戏被打破,你所有的额外努力都会浪费时间和资源。
不允许封闭代码泄漏到应用程序之外的唯一可靠方法是不以任何形式直接将其发送给用户。只有在组织安全的其他方面保持严密的情况下,这才是正确的。
本章详细介绍了 Python 的打包生态系统。现在,在阅读之后,您应该知道哪些工具适合您的打包需求,以及您的项目需要哪些类型的发行版。您还应该了解常见问题的常用技术,以及如何为项目提供有用的元数据。
我们还讨论了非常有用的独立可执行文件的主题,特别是在分布式桌面应用程序中。
下一章将广泛地依赖于我们在这里学到的知识来展示如何以可靠和自动化的方式有效地处理代码部署。