在本书中,到目前为止,我们主要关注的是编写一段工作代码。我们的项目都是单个脚本,最多包含少量支持数据文件。然而,生成一个完成的项目并不是以编写代码结束;我们还需要我们的项目易于分发,以便我们可以与其他人共享(或出售给其他人)。
在本章中,我们将研究如何准备代码以供共享和分发。
我们将介绍以下主题:
- 构建项目
- 与
setuptools
一起分发 - 使用 PyInstaller 编译
在本章中,您将需要我们在本书中使用的基本 Python 和 PyQt 设置。您还需要使用以下命令从 PyPI 获得的setuptools
、wheel
和pyinstaller
库:
$ pip install --user setuptools wheel pyinstaller
Windows 用户将希望从安装 7-Zip 程序 https://www.7-zip.org/ 以便他们可以使用tar.gz
文件,所有平台上的用户都应该从安装 UPX 实用程序 https://upx.github.io/ 。
查看以下视频以查看代码的运行:http://bit.ly/2M5xH4J
到目前为止,在本书中,我们已经将每个示例项目中的所有 Python 代码放在一个文件中。然而,现实世界中的 Python 项目受益于更好的组织。虽然没有关于如何构造 Python 项目的官方标准,但是我们可以将一些约定和一般概念应用到我们的项目结构中,这些约定和概念不仅可以使事情有条理,还可以鼓励其他人对我们的代码做出贡献。
为了了解这是如何工作的,我们将在 PyQt 中创建一个简单的 tic-tac-toe 游戏,然后花本章剩余的时间准备发布。
我们的井字游戏分为三类:
- 管理游戏逻辑的引擎类
- 提供游戏状态视图和进行游戏的方法的棋盘类
- 将其他两个窗口合并到 GUI 中的主窗口类
打开第 4 章中使用 QMainWindow 构建应用中的应用模板的新副本,并将其命名为ttt-qt.py
。现在让我们创建这些类。
我们的游戏引擎对象的主要职责是跟踪游戏并检查是否有赢家或游戏是否平局。玩家将简单地用'X'
和'O'
字符串表示,棋盘将被建模为九个项目的列表,这些项目要么是玩家,要么是None
。
它是这样开始的:
class TicTacToeEngine(qtc.QObject):
winning_sets = [
{0, 1, 2}, {3, 4, 5}, {6, 7, 8},
{0, 3, 6}, {1, 4, 7}, {2, 5, 8},
{0, 4, 8}, {2, 4, 6}
]
players = ('X', 'O')
game_won = qtc.pyqtSignal(str)
game_draw = qtc.pyqtSignal()
def __init__(self):
super().__init__()
self.board = [None] * 9
self.current_player = self.players[0]
winning_sets
列表包含set
对象,每个组合的董事会索引构成一个胜利。我们将使用该列表检查是否有玩家获胜。我们还定义了当比赛获胜或平局时(即所有方格都被填满,没有人获胜)要发出的信号。构造器填充棋盘列表并将当前玩家设置为X
。
我们需要在每次回合后更新当前玩家的方法,如下所示:
def next_player(self):
self.current_player = self.players[
not self.players.index(self.current_player)]
接下来,我们将添加一种标记正方形的方法:
def mark_square(self, square):
if any([
not isinstance(square, int),
not (0 <= square < len(self.board)),
self.board[square] is not None
]):
return False
self.board[square] = self.current_player
self.next_player()
return True
此方法首先检查给定的正方形是否因任何原因不应标记,如果有原因,则返回False
;否则,我们标记方块,交换到下一个玩家,然后返回True
。
本课程的最后一个方法将检查棋盘的状态,以确定是否有赢家或平局:
def check_board(self):
for player in self.players:
plays = {
index for index, value in enumerate(self.board)
if value == player
}
for win in self.winning_sets:
if not win - plays: # player has a winning combo
self.game_won.emit(player)
return
if None not in self.board:
self.game_draw.emit()
该方法使用一些集合操作来对照获胜组合列表检查每个玩家当前标记的方块。如果发现任何匹配项,则发出game_won
信号,方法返回。如果还没有人赢,我们也会检查是否有未标记的方块;如果没有,比赛就是平局。如果这两种情况都不是真的,我们什么也不做。
对于 board GUI,我们将使用QGraphicsScene
对象,就像我们在第 12 章中的坦克游戏一样,使用 QPaint创建 2D 图形。
我们将从一些类变量开始:
class TTTBoard(qtw.QGraphicsScene):
square_rects = (
qtc.QRectF(5, 5, 190, 190),
qtc.QRectF(205, 5, 190, 190),
qtc.QRectF(405, 5, 190, 190),
qtc.QRectF(5, 205, 190, 190),
qtc.QRectF(205, 205, 190, 190),
qtc.QRectF(405, 205, 190, 190),
qtc.QRectF(5, 405, 190, 190),
qtc.QRectF(205, 405, 190, 190),
qtc.QRectF(405, 405, 190, 190)
)
square_clicked = qtc.pyqtSignal(int)
square_rects
元组为板上的九个方块中的每一个定义了一个QRectF
对象,每当点击方块时就会发出square_clicked
信号;伴随的整数将指示单击了哪个正方形(0-8)。
以下是=__init__()
方法:
def __init__(self):
super().__init__()
self.setSceneRect(0, 0, 600, 600)
self.setBackgroundBrush(qtg.QBrush(qtc.Qt.cyan))
for square in self.square_rects:
self.addRect(square, brush=qtg.QBrush(qtc.Qt.white))
self.mark_pngs = {
'X': qtg.QPixmap('X.png'),
'O': qtg.QPixmap('O.png')
}
self.marks = []
此方法设置场景大小并绘制青色背景,然后在square_rects
中绘制每个正方形。然后,我们加载用于标记正方形的'X'
和'O'
图像的QPixmap
对象,并创建一个空列表来跟踪QGraphicsSceneItem
对象以进行标记。
接下来,我们将添加一个方法来绘制电路板的当前状态:
def set_board(self, marks):
for i, square in enumerate(marks):
if square in self.mark_pngs:
mark = self.addPixmap(self.mark_pngs[square])
mark.setPos(self.square_rects[i].topLeft())
self.marks.append(mark)
此方法将获取我们板上的标记列表,并在每个正方形中绘制适当的像素图,跟踪创建的QGraphicsSceneItems
对象。
现在,我们需要一种方法来清除电路板:
def clear_board(self):
for mark in self.marks:
self.removeItem(mark)
此方法只需迭代保存的 pixmap 项并将其全部删除。
我们需要做的最后一件事是处理鼠标点击:
def mousePressEvent(self, mouse_event):
position = mouse_event.buttonDownScenePos(qtc.Qt.LeftButton)
for square, qrect in enumerate(self.square_rects):
if qrect.contains(position):
self.square_clicked.emit(square)
break
每当用户点击鼠标时,QGraphicsScene
就会调用mousePressEvent())
方法。它包括一个QMouseEvent
对象,其中包含有关事件的详细信息,包括鼠标单击的位置。我们可以检查此点击是否在我们的square_rects
对象中,如果是,我们将发出square_clicked
信号并退出该方法。
在MainWindow.__init__()
中,我们将首先创建一块板和一个QGraphicsView
对象来显示它:
self.board = TTTBoard()
self.board_view = qtw.QGraphicsView()
self.board_view.setScene(self.board)
self.setCentralWidget(self.board_view)
现在我们需要创建一个游戏引擎实例并连接其信号。为了让我们能够一次又一次地开始游戏,我们将为此创建一个单独的方法:
def start_game(self):
self.board.clear_board()
self.game = TicTacToeEngine()
self.game.game_won.connect(self.game_won)
self.game.game_draw.connect(self.game_draw)
此方法清除棋盘,然后创建游戏引擎对象的实例,将引擎的信号连接到MainWindow
方法以处理两个游戏结束场景。
回到__init__()
,我们将继续调用此方法自动设置第一场游戏:
self.start_game()
接下来,我们需要启用播放器输入。我们需要一种方法,尝试在引擎中标记正方形,然后检查棋盘,如果标记成功,则为赢或平局:
def try_mark(self, square):
if self.game.mark_square(square):
self.board.set_board(self.game.board)
self.game.check_board()
该方法可以连接到板的square_clicked
信号;回到__init__()
中,添加以下代码:
self.board.square_clicked.connect(self.try_mark)
最后,我们需要处理两种游戏场景:
def game_won(self, player):
"""Display the winner and start a new game"""
qtw.QMessageBox.information(
None, 'Game Won', f'Player {player} Won!')
self.start_game()
def game_draw(self):
"""Display the lack of a winner and start a new game"""
qtw.QMessageBox.information(
None, 'Game Over', 'Game Over. Nobody Won...')
self.start_game()
在这两种情况下,我们只需在QMessageBox
中显示适当的消息,然后重新启动游戏。
这就完成了我们的游戏。花点时间运行游戏,确保你理解它在正常工作时的反应(可能让朋友和你一起玩几轮;如果你的朋友很年轻或不是特别聪明,这会很有帮助)。
现在我们有了一个有效的游戏,是时候准备分发了。我们要做的第一件事是以一种更易于维护和扩展的方式构建我们的项目,并使其他 Python 程序员能够协作。
作为程序员,我们倾向于将应用和库视为两种截然不同的东西,但事实上,结构良好的应用与库并没有太大区别。库只是现成类和函数的集合。我们的应用大部分也只是类定义;它的结尾恰好有几行代码,允许它作为应用执行。从这个角度来看,将应用结构化为 Python 库模块非常有意义。为此,我们将把一个 Python 文件转换为一个目录,其中包含所有文件,每个文件都包含一个单独的代码单元。
第一步是考虑我们的项目名称;现在,这个名字是ttt-qt.py
。当你第一次开始对一个项目进行黑客攻击时,你会想出一个简短的名字,这并不少见,但你不需要坚持使用这个名字。在本例中,我们的名称相当隐晦,并且由于连字符,不能作为 Python 模块名称使用。取而代之的是,我们称之为qtictactoe
,这是一个更明确的名称,避免了连字符。
首先,创建一个名为QTicTacToe
的新目录;这将是我们的项目根。项目根目录是所有项目文件的目录。
在该目录中,我们将创建第二个名为qtictactoe
的目录;这将是模块目录,其中包含我们的大部分源代码。
为了开始我们的模块,我们将从添加三个类的代码开始。我们将把每一个放在一个单独的文件中;这并不是绝对必要的,但它将帮助我们保持代码的解耦,并使我们更容易找到要编辑的类。
因此,在qtictactoe
下,创建三个文件:
engine.py
将举办我们的游戏引擎课程。复制TicTacToeEngine
定义以及所用类的必要PyQt5
导入语句。在这种情况下,您只需要QtCore
。board.py
将举办TTTBoard
课程。复制该代码以及全套PyQt5
导入语句。- 最后,
mainwindow.py
将举办MainWindow
课程。复制该类的代码以及PyQt5
导入。
mainwindow.py
还需要从其他文件访问TicTacToeEngine
和TTTBoard
类。为了提供这种访问,我们需要使用相对进口。相对导入是从同一模块导入子模块的一种方式。
在mainwindow.py
顶部添加以下内容:
from .engine import TicTacToeEngine
from .board import TTTBoard
导入中的点表示这是一个相对导入,具体指的是当前容器模块(在本例中为qtictactoe
。通过使用这样的相对导入,我们可以确保这些模块是从我们自己的项目导入的,而不是从最终用户系统上的其他 Python 库导入的。
我们需要添加到模块的下一个代码是使其实际运行的代码。这是我们通常放在if __name__ == '__main__'
块下的代码。
在模块中,我们将把它放在一个名为__main__.py
的文件中:
import sys
from PyQt5.QtWidgets import QApplication
from .mainwindow import MainWindow
def main():
app = QApplication(sys.argv)
mainwindow = MainWindow()
sys.exit(app.exec())
if __name__ == '__main__':
main()
__main__.py
文件在 Python 模块中有特殊用途。每当我们的模块使用-m
开关运行时,它就会执行,如下所示:
$ python3 -m qtictactoe
本质上,__main__.py
是 Python 脚本中if __name__ == '__main__':
块的模块等价物。
请注意,我们将三行主要代码放在一个名为main()
的函数中。当我们讨论setuptools
的使用时,其原因将变得显而易见。
我们需要在模块内部创建的最后一个文件是名为__init__.py
的空文件。Python 模块的__init__.py
文件类似于 Python 类的__init__()
方法。它在导入模块时执行,其命名空间中的任何内容都被视为在模块的根命名空间中。不过,在这种情况下,我们将把它留空。这似乎毫无意义,但如果没有这个文件,我们将要使用的许多工具将无法将这个包含 Python 文件的文件夹识别为实际的模块。
此时,目录结构应如下所示:
QTicTacToe/
├── qtictactoe
├── board.py
├── engine.py
├── __init__.py
├── __main__.py
└── mainwindow.py
现在我们可以使用python3 -m qtictactoe
来执行我们的程序,但对于大多数用户来说,这并不十分直观。让我们通过创建一个用于执行应用的明显文件来提供一些帮助。
直接在项目根目录下(模块外部),创建一个名为run.py
的文件:
from qtictactoe.__main__ import main
main()
这个文件的唯一目的是从我们的模块加载main()
函数并执行它。现在你可以执行python run.py
了,你会发现它启动的很好。然而,当你点击一个正方形时,会出现问题,什么也不会发生。那是因为我们的图像文件丢失了。下一步我们需要处理这些问题。
在 PyQt 程序中,处理我们的X
和O
图像等文件的最佳方法是使用pyrcc5
工具生成一个资源文件,然后可以像其他 Python 文件一样添加到您的模块中(我们在第 6 章、设计 Qt 应用中了解了这一点)。然而,在本例中,我们将把图像保留为 PNG 文件,以便我们可以探索处理非 Python 文件的选项。
对于这些类型的文件应该放在项目目录中的什么位置,几乎没有共识,但是由于这些图像是TTTBoard
类的必需组件,所以将它们放在我们的模块中是有意义的。为了便于组织,将它们放在名为images
的目录中。
您的目录结构现在应该如下所示:
QTicTacToe/
├── qtictactoe
│ ├── board.py
│ ├── engine.py
│ ├── images
│ │ ├── O.png
│ │ └── X.png
│ ├── __init__.py
│ ├── __main__.py
│ └── mainwindow.py
└── run.py
按照我们编写TTTBoard
的方式,您可以看到每个图像都是使用相对文件路径加载的。在 Python 中,相对路径总是相对于当前工作目录,即用户启动脚本的目录。不幸的是,这是一个相当脆弱的设计,因为我们无法控制这个目录。我们也不能硬编码绝对文件路径,因为我们不知道我们的应用可能存储在用户的系统中的什么位置(请参阅我们在第 6 章、设计 Qt 应用、使用 Qt 资源文件部分中对这个问题的讨论)。
The ideal way to solve this problem in a PyQt application is to use Qt Resource files; however, we're going to try a different approach just to illustrate how to solve this problem in cases where that isn't an option.
为了解决这个问题,我们需要修改TTTBoard
加载图像的方式,使其相对于模块的位置,而不是用户当前的工作目录。这将要求我们使用 Python 标准库中的os.path
模块,因此在board.py
顶部添加此模块:
from os import path
现在,在__init__()
中,我们将修改图像中加载的行:
directory = path.dirname(__file__)
self.mark_pngs = {
'X': qtg.QPixmap(path.join(directory, 'images', 'X.png')),
'O': qtg.QPixmap(path.join(directory, 'images', 'O.png'))
}
__file__
变量是一个内置变量,始终包含当前文件的绝对路径(本例中为board.py
)。使用path.dirname
,我们可以找到包含此文件的目录。然后,我们可以使用path.join
组合一个路径,在同一目录中的名为images
的文件夹下查找文件。
如果你现在运行这个程序,你会发现它和以前一样工作得很好。不过,我们还没做完。
工作和组织良好的代码是我们项目的良好开端;但是,如果您希望其他人使用您的项目或为您的项目做出贡献,您需要解决他们可能遇到的一些问题。例如,他们需要知道如何安装该程序,它的先决条件是什么,或者使用或分发的法律条款是什么。
为了回答这些问题,我们将包括一系列标准文件和目录:LICENSE
文件、README
文件、docs
目录和requirements.txt
文件。
当您共享代码时,明确说明其他人可以或不能使用该代码做什么是很重要的。在大多数国家,一个人创造了一件作品,如一个程序,他就自动成为该作品的版权持有人;这意味着你可以控制你的作品的复制。如果您希望其他人对您所创建的内容进行贡献或使用,您需要向他们授予许可证才能这样做。
管理项目的许可证通常在名为LICENSE
的项目根目录中的纯文本文件中提供。在我们的示例代码中,我们包含了这样一个文件,其中包含了MIT 许可证的副本。麻省理工学院许可证是一个许可的开源许可证,基本上允许任何人对代码做任何事情,只要他们保留我们的版权声明。它还声明,由于有人使用我们的代码而发生的任何可怕的事情,我们都不负责。
This file is sometimes called COPYING
, and may have a file extension such as txt
as well.
您当然可以在许可证中自由设定任何条件;但是,对于 PyQt 应用,您需要确保您的许可证与 PyQt 的通用公共许可证(GPL)GNU 和 Qt 的次要通用公共许可证(LGPLGNU 的条款兼容。如果您打算发布商业或受限许可的 PyQt 软件,请记住第 1 章PyQt 入门中的内容,您需要从 Qt 公司和 Riverbank Computing 购买商业许可证。
对于开源项目,Python 社区强烈建议您坚持使用众所周知的许可证,如 MIT、BSD、GPL 或 LGPL。公认的开放源代码许可证列表可在开放源代码倡议网站上找到 https://opensource.org/licenses 。您也可以咨询https://choosealicense.com ,一个为您选择最符合您意图的许可证提供指导的网站。
README
文件是软件发行中最古老的传统之一。可以追溯到 20 世纪 70 年代中期,这个纯文本文件通常用于在用户安装或运行软件之前向用户传达最基本的指令和信息。
虽然README
文件应该包含什么并没有标准,但用户希望找到某些东西;其中包括:
- 软件的名称和主页
- 软件的作者(带联系方式)
- 软件的简短描述
- 基本使用说明,包括任何命令行开关或参数
- 报告错误或对项目有贡献的说明
- 已知 bug 的列表
- 注意事项,如平台特定问题或说明
无论你在文件中包含什么,你都应该力求使其简洁有序。为了方便一些组织,许多现代软件项目在编写README
文件时使用标记语言;这允许我们使用诸如标题、项目符号列表甚至表格之类的元素。
在 Python 项目中,首选的标记语言是重组文本(RST)。这种语言是docutils
项目的一部分,该项目为 Python 提供文档实用程序。
在为qtictactoe
创建README.rst
文件的过程中,我们将简要介绍一下 RST。从标题开始:
============
QTicTacToe
============
顶行周围的等号表示它是一个标题;在本例中,我们只使用了项目的名称。
下一步,我们将创建两个部分来了解项目的基本信息;我们通过简单地在一行文本下面加上符号来表示章节标题,如下所示:
Authors
=======
By Alan D Moore - https://www.alandmoore.com
About
=====
This is the classic game of **tic-tac-toe**, also known as noughts and crosses. Battle your opponent in a desperate race to get three in a line.
用于在节标题下划线的符号必须是以下符号之一:
= - ` : ' " ~ ^ _ * + # < >
我们使用它们的顺序并不重要,因为 RST 解释器将假定第一个符号用作表示顶级头的下划线,下一种符号类型是第二级头,依此类推。在本例中,我们首先使用等号,因此在本文档中使用它时,它将指示一级标题。
注意单词tic-tac-toe
周围的双星号;这表示粗体文本。RST 还可以表示下划线、斜体和类似的印刷样式。
例如,我们可以通过使用反勾号指示单间距代码文本:
Usage
=====
Simply run `python qtictactoe.py` from within the project folder.
- Players take turns clicking the mouse on the playing field to mark squares.
- When one player gets 3 in a row, they win.
- If the board is filled with nobody getting in a row, the game is a draw.
本例还显示了一个项目符号列表:每行都以破折号和空格作为前缀。我们也可以交替使用+
或*
符号,并通过缩进创建子点。
让我们用一些关于贡献的信息和一些注释来完成我们的README.rst
文件:
Contributing
============
Submit bugs and patches to the
`public git repository <http://git.example.com/qtictactoe>`_.
Notes
=====
A strange game. The only winning move is not to play.
*—Joshua the AI, WarGames*
Contributing
部分介绍如何创建超链接:将超链接文本放在反勾号内,URL 放在尖括号内,并在结束反勾号后添加下划线。Notes
部分演示了一个块引号,它通过简单地将行缩进四个空格来完成。
尽管我们的文件完全可以作为文本阅读,但许多流行的代码共享站点都会将 RST 和其他标记语言转换为 HTML。例如,在 GitHub 上,此文件将显示在浏览器中,如下所示:
这个简单的README.rst
文件足以满足我们的小型应用;随着应用的增长,它将保证进一步扩展,以记录添加的功能、贡献者、社区策略等。这就是为什么我们喜欢使用诸如 RST 这样的纯文本格式,以及为什么我们将其作为项目存储库的一部分;它应该与代码一起更新。
A quick reference for RST syntax can be found at docutils.sourceforge.net/docs/user/rst/quickref.html.
虽然此README
文件足以为QTicTacToe
提供文档,但更复杂的程序或库可能需要更健壮的文档。放置此类文件的标准位置为docs
目录。此目录应位于我们的项目根目录下,并且可以包含任何类型的附加文档,包括以下内容:
- 示例配置文件
- 用户手册
- API 文档
- 数据库图
因为我们的程序不需要这些东西,所以我们不需要向这个项目添加一个docs
目录。
Python 程序通常需要标准库之外的包才能运行,用户需要知道安装什么才能运行项目。您可以(也可能应该)将此信息放在README
文件中,但也应该放在requirements.txt
中。
requirements.txt
的格式是每行一个库,如下所示:
PyQt5
PyQt5-sip
此文件中的库名称应与 PyPI 中使用的库名称匹配,因为pip
可以使用此文件安装项目所需的所有库,如下所示:
$ pip install --user -r requirements.txt
We don't actually have to specify PyQt5-sip
since it's a dependency of PyQt5
and will be installed automatically. We added it here to show how multiple libraries are specified.
如果需要特定版本的库,也可以使用版本说明符进行说明:
PyQt5 >= 5.12
PyQt5-sip == 4.19.4
在这种情况下,我们指定的是PyQt5
版本5.12
或更高版本,并且只指定PyQt5-sip
的4.19.4
版本。
有关requirements.txt
文件的更多信息,请访问https://pip.readthedocs.io/en/1.1/requirements.html 。
这些是项目文档和元数据的基本要素,但您可能会发现一些附加文件在某些情况下很有用:
TODO.txt
:需要改进的缺陷或缺失功能的短名单CHANGELOG.txt
:重大项目变更和发布的历史记录tests
:包含模块单元测试的目录scripts
:包含 Python 或 shell 脚本的目录,这些脚本对模块有用,但不是模块的一部分Makefile
:一些项目受益于脚本化的构建过程,对于这一点,像make
这样的实用程序可能会有所帮助;替代品包括 CMake、SCons 或 Waf
此时,您的项目已经准备好上传到您最喜欢的源代码共享站点。在下一节中,我们将研究如何为 PyPI 做好准备。
在本书中,您多次使用pip
安装 Python 软件包。您可能知道pip
从 PyPI 下载这些包,并将它们安装到您的系统、Python 虚拟环境或用户环境中。您可能不知道的是,用于创建和安装这些软件包的工具称为setuptools
,如果我们想为 PyPI 或个人使用制作自己的软件包,我们可以随时使用该工具。
尽管setuptools
是官方推荐的用于创建 Python 包的工具,但它不是标准库的一部分。但是,如果您选择在安装过程中包含pip
,则大多数操作系统(操作系统es)的默认发行版中都包含了*。如果出于某种原因,您没有安装setuptools
,请参阅上的文档 https://setuptools.readthedocs.io/en/latest/ 查看如何将其安装到您的平台上。*
使用setuptools
的主要任务是编写setup.py
脚本。在本节中,我们将学习如何编写和使用setup.py
脚本来生成可分发的包。
setup.py
的主要目的是使用关键字参数调用setuptools.setup()
函数,该函数将定义项目的元数据以及项目的打包和安装方式。
因此,我们要做的第一件事是导入该函数:
from setuptools import setup
setup(
# Arguments here
)
setup.py
中的剩余代码将是setup()
的关键字参数。让我们看看这些论点的不同类别。
最简单的参数涉及项目的基本元数据:
name='QTicTacToe',
version='1.0',
author='Alan D Moore',
author_email='[email protected]',
description='The classic game of noughts and crosses',
url="http://qtictactoe.example.com",
license='MIT',
在这里,我们描述了包名、版本、简短描述、项目 URL 和许可证,以及作者的姓名和电子邮件。此信息将写入包元数据,并由 PyPI 等站点用于为项目构建配置文件页面。
例如,查看 PyQt5 的 PyPI 页面:
沿着页面左侧,您将看到指向项目主页、作者(带有超链接电子邮件地址)和许可证的链接。在顶部,您可以看到项目名称和版本,以及项目的简短描述。所有此类数据都可以从项目的setup.py
脚本中提取。
If you plan to submit a package to PyPI, please see PEP 440 at https://www.python.org/dev/peps/pep-0440/ for how your version number should be specified.
您在本页正文中看到的长文本来自long_description
参数。我们可以直接在这个参数中输入一个长字符串,但是既然我们已经有了这么好的README.rst
文件,为什么不在这里使用它呢?由于setup.py
是一个 Python 脚本,我们可以直接读入文件的内容,如下所示:
long_description=open('README.rst', 'r').read(),
在这里使用 RST 的一个优点是 PyPI(和许多其他代码共享站点)将自动将标记呈现为格式良好的 HTML。
如果我们希望让我们的项目更容易搜索,我们可以包括一个空格分隔的关键字字符串:
keywords='game multiplayer example pyqt5',
在这种情况下,在 PyPI 中搜索“多人 pyqt5”的人应该可以找到我们的项目。
最后,您可以包括项目相关 URL 的字典:
project_urls={
'Author Website': 'https://www.alandmoore.com',
'Publisher Website': 'https://packtpub.com',
'Source Code': 'https://git.example.com/qtictactoe'
},
格式为{'label': 'URL'}
;这里可能包括项目的 bug 跟踪器、文档站点、Wiki 页面或源代码库,特别是如果其中任何一个与主 URL 不同的话。
除了建立基本元数据外,setup()
还需要关于需要包含的实际代码的信息,或者需要在系统上存在的环境的信息,以便执行此包。
这里需要处理的第一个关键字是packages
,它定义了我们项目中需要包含的模块:
packages=['qtictactoe', 'qtictactoe.images'],
注意我们需要明确地包括qtictactoe
模块和qtictactoe.images
模块;即使images
目录在qtictactoe
下,也不会自动包含。
如果我们有很多子模块,并且不想显式列出它们,setuptools
还提供了一个自动解决方案:
from setuptools import setup, find_package
setup(
#...
packages=find_packages(),
)
If you want to use find_packages
, make sure each submodule has an __init__.py
file in it so that setuputils
can identify it as a module. In this case, you'd need to add an __init__.py
file to the images
folder or it will be ignored.
两种方法各有优缺点;手动方法需要更多的工作,但在某些情况下find_packages
有时可能无法识别库。
我们还需要指定在本例中运行此项目所需的外部库PyQt5
。这可以通过install_requires
关键字完成:
install_requires=['PyQt5'],
此关键字获取要安装的程序必须安装的程序包的名称列表。当您的程序使用pip
安装时,它将使用此列表自动安装所有依赖项软件包。您应该在此列表中包括任何不属于标准库的内容。
就像requirements.txt
文件一样,我们甚至可以明确每个依赖项所需的版本号:
install_requires=['PyQt5 >= 5.12'],
在这种情况下,pip
将确保安装了大于或等于 5.12 的 PyQt5 版本。如果未指定版本,pip
将安装 PyPI 提供的最新版本。
在某些情况下,我们可能还需要某种版本的 Python;例如,我们的项目使用 f-strings,这是仅在 Python3.6 或更高版本中才能找到的特性。我们可以用python_requires
关键字指定:
python_requires='>=3.6',
我们还可以为可选特性指定依赖项;例如,如果我们在qtictactoe
中添加了一个可选的网络播放功能,它需要requests
库,我们会这样指定:
extras_require={
"NetworkPlay": ["requests"]
}
extras_require
关键字接受功能名称(可以是任何您想要的)到包名称列表的映射。在安装软件包时,这些模块不会自动安装,但其他模块可以依赖于这些子功能。例如,另一个模块可以指定对我们项目的NetworkPlay
额外关键字的依赖性,如下所示:
install_requires=['QTicTacToe[NetworkPlay]'],
这将触发一系列依赖项,从而安装requests
库。
默认情况下,setuptools
将打包它在项目中找到的 Python 文件,其他文件类型将被忽略。然而,在几乎所有的项目中,都会有一些非 Python 文件,我们希望将它们包含在发行包中。这些文件通常分为两类:一类是 Python 模块的一部分,如我们的 PNG 文件;另一类不是,如README
文件。
要合并不属于 Python 包的文件,我们需要创建一个名为MANIFEST.in
的文件。此文件包含项目根目录下文件路径的include
指令。例如,如果我们希望包含文档文件,我们的文档文件应如下所示:
include README.rst
include LICENSE
include requirements.txt
include docs/*
格式很简单:单词include
后跟一个文件名、路径或模式,将匹配一组文件。所有路径都是相对于项目根的。
要包含作为 Python 包一部分的文件,我们有两个选择。
一种方法是将它们包含在MANIFEST.in
文件中,然后在setup.py
中将include_package_data
设置为True
:
include_package_data=True,
包含非 Python 文件的另一种方法是在setup.py
中使用package_data
关键字参数:
package_data={
'qtictactoe.images': ['*.png'],
'': ['*.txt', '*.rst']
},
此参数接受一个dict
对象,其中每个项都是一个模块路径和一个与包含的文件匹配的模式列表。在本例中,我们希望包括在qtictactoe.images
模块中找到的所有 PNG 文件,以及包中任何位置的任何 TXT 或 RST 文件。请记住,此参数仅适用于模块目录中的文件*(即qtictactoe
下的文件)。如果我们想要包括像README.rst
或run.py
这样的文件,那么这些文件应该放在MANIFEST.in
文件中。*
You can use either approach to including files, but you cannot use both approaches in the same project; if you enable include_package_data
, the package_data
directives will be ignored.
我们倾向于将 PyPI 视为安装 Python 库的工具;事实上,它对于安装应用也很有效,并且许多 Python 应用都可以从中获得。即使您正在创建一个库,您的库也可能附带可执行的实用程序,例如 PyQt5 附带的pyrcc5
和pyuic5
实用程序。
为了适应这些需求,setuputils
为我们提供了一种将特定函数或方法指定为控制台脚本的方法;安装包时,它将创建一个简单的可执行文件,当从命令行执行时,该文件将调用该函数或方法。
这是使用entry_points
关键字指定的:
entry_points={
'console_scripts': [
'qtictactoe = qtictactoe.__main__:main'
]
}
entry_points
字典还有其他用途,但我们最关心的是'console_scripts'
键。该键指向一个字符串列表,该列表指定了我们希望设置为命令行脚本的函数。这些字符串的格式如下所示:
'command_name = module.submodule:function'
您可以添加任意数量的控制台脚本;它们只需要指向包中可以直接运行的函数或方法。注意您必须在这里指定一个实际的可调用项;不能只指向要运行的 Python 文件。这就是为什么我们将所有执行代码放在__main__.py
中的main()
函数下。
setuptools
包含更多指令,用于处理不太常见的情况;完整列表见https://setuptools.readthedocs.io/en/latest/setuptools.html 。
现在setup.py
已经准备就绪,我们可以使用它来实际创建包分发。包分发有两种基本类型:source
和built
。在本节中,我们将讨论如何使用源分布。
源代码分发版是构建项目所需的所有源代码和额外文件的捆绑包。它包括setup.py
文件,对于以跨平台方式分发项目非常有用。
要生成源发行版,请在项目根目录中打开命令提示符,然后输入以下命令:
$ python3 setup.py sdist
这将创建几个目录和许多文件:
ProjectName.egg-info
目录(在本例中为QTicTacToe.egg-info
目录)将包含几个由setup.py
参数生成的元数据文件。dist
目录将包含包含我们发行版的tar.gz
存档文件。我们的名字叫QTicTacToe-1.0.tar.gz
。
花几分钟时间探索QTicTacToe.egg-info
的内容;您将看到我们在setup()
中指定的所有信息都以某种形式存在。此目录也包含在源发行版中。
另外,花点时间打开tar.gz
文件,看看它包含什么;您将看到我们在MANIFEST.in
中指定的所有文件,以及qtictactoe
模块和QTicTacToe.egg-info
中的所有文件。本质上,这是我们项目目录的完整副本。
Linux and macOS have native support for tar.gz
archives; on Windows, you can use the free 7-Zip utility. See the Technical requirements section for information about 7-Zip.
可使用pip
安装电源分配;为了了解在干净的环境中如何工作,我们将在 Python虚拟环境中安装我们的库。虚拟环境是一种创建独立 Python 堆栈的方法,您可以在其中独立于系统 Python 安装添加或删除库。
在控制台窗口中,创建一个新目录,然后将其设置为虚拟环境:
$ mkdir test_env
$ virtualenv -p python3 test_env
virtualenv
命令将必要的文件复制到给定的目录中,以便运行 Python,以及一些用于激活和停用环境的脚本。
要开始使用新环境,请运行以下命令:
# On Linux and Mac
$ source test_env/bin/activate
# On Windows
$ test_env\Scripts\activate
根据您的平台,您的命令行提示符可能会更改,以指示您处于虚拟环境中。现在,当您运行python
或与 Python 相关的工具(如pip
)时,它们将在虚拟环境中而不是在您的 Python 系统中执行所有操作。
让我们安装源代码分发包:
$ pip install QTicTacToe/dist/QTicTacToe-1.0.tar.gz
此命令将导致pip
提取我们的源分布,并在项目根目录内执行python setup.py install
。install
指令将下载任何依赖项,构建入口点可执行文件,并将代码复制到存储 Python 库的目录中(在我们的虚拟环境中,这将是test_env/lib/python3.7/site-packages/
)。请注意,PyQt5
的新副本已下载;您的虚拟环境只安装了 Python 和标准库,因此我们在install_requires
中列出的任何依赖项都必须重新安装。
pip
完成后,您应该能够运行qtictactoe
命令并成功启动应用。此命令存储在test_env/bin
中,以防您的操作系统不会自动将虚拟环境目录附加到您的PATH
中。
要从虚拟环境中删除包,可以运行以下操作:
$ pip uninstall QTicTacToe
这将清理源代码和所有生成的文件。
源代码发行版对于开发人员来说是必不可少的,但它们通常包含许多最终用户不需要的元素,例如单元测试或示例代码。除此之外,如果项目包含已编译的代码(如用 C 编写的 Python 扩展),则该代码需要编译才能在目标上使用。为了解决这个问题,setuptools
提供了多种内置分发类型。内置分发版提供了一组现成的文件,只需将其复制到适当的目录即可使用。
在本节中,我们将讨论如何使用构建的发行版。
创建内置分发的第一步是确定我们想要的内置分发的类型。setuptools
库提供了几种不同的内置分发类型,我们可以安装其他库来添加更多选项。
内置类型如下所示:
- 二进制发行版:这是一个
tar.gz
文件,就像源代码发行版一样,但与源代码发行版不同,它包含预编译代码(例如qtictactoe
可执行文件),并省略某些类型的文件(例如测试)。需要提取构建分发的内容,并将其复制到要运行的适当位置。 - Windows installer:这与二进制发行版类似,只是它是一个可执行文件,可以在 Windows 上启动安装向导。该向导仅用于将文件复制到适当的位置以供执行或库使用。
- RPM 软件包管理器RPM安装程序:同样,这个软件包与二进制发行版类似,只是它将代码打包在 RPM 文件中。RPM 文件由几个 Linux 发行版(如 Red Hat、CentOS、Suse、Fedora 等)上的包管理实用程序使用。
虽然您可能会发现这些分布类型在某些情况下很有用,但它们都有点过时,时间是 2019 年;现在发布 Python 的标准方式是使用轮分发。这些是您可以在 PyPI 上找到的二进制分发包。
让我们看看如何创建和安装控制盘软件包。
要创建车轮分配,首先需要确保从 PyPI 安装了wheel
库(请参见技术要求部分)。之后,setuptools
将有一个额外的bdist_wheel
选项。
您可以使用它创建控制盘文件,如下所示:
$ python3 setup.py bdist_wheel
与前面一样,此命令将创建QTicTacToe.egg-info
目录,并用包含项目元数据的文件填充该目录。它还创建了一个build
目录,编译后的文件在压缩到wheel
文件之前会在该目录中暂存。
在dist
下,我们将找到已完成的wheel
文件。在我们的例子中,它被称为QTicTacToe-1.0-py3-none-any.whl
。文件名的格式如下所示:
-
项目名称(
QTicTacToe
)。 -
版本(1.0)。
-
支持的 Python 版本,无论是 2、3 还是
universal
(py3
)。 -
ABI
标记,表示 Python 的特定版本,我们的项目依赖于它的二进制接口(none
。只有在我们编译了代码的情况下,才会使用它。 -
平台(操作系统和 CPU 架构)。我们的是 any,因为我们没有包含任何特定于平台的二进制文件。
二进制分布有三种类型:
- 通用类型只有 Python,并且与 Python 2 或 3 兼容
- 纯 Python类型只有 Python,但与 Python 2 或 Python 3 兼容
- 平台类型包括仅在特定平台上运行的编译代码
正如发行版名称所反映的,我们的包是纯 Python 的,因为它不包含编译代码,只支持 Python 3。PyQt5 是平台包类型的一个示例,因为它包含为特定平台编译的 Qt 库。
Recall from Chapter 15, PyQt on the Raspberry Pi, that we could not install PyQt from PyPI on the Raspberry Pi because there was no wheel
file for the Linux ARM platform. Since PyQt5 is a platform package type, it can only be installed on platforms for which this wheel
file has been generated.
与源代码发行版一样,我们可以使用pip
安装车轮文件:
$ pip install qtictactoe/dist/QTicTacToe-1.0-py3-none-any.whl
如果您在一个全新的虚拟环境中尝试此操作,您会再次发现 PyQt5 是从 PyPI 下载并安装的,之后您就可以使用qtictactoe
命令了。对于最终用户来说,对于像QTicTacToe
这样的程序没有太大的区别,但是对于需要编译二进制文件的库(比如 PyQt5),它使得设置问题大大减少。
当然,即使是一个wheel
文件也需要目标系统安装 Python 和pip
以及访问 internet 和 PyPI。对于许多用户或计算环境来说,这仍然是一个很大的要求。在下一节中,我们将探索一个工具,该工具允许我们从 Python 项目中创建一个独立的可执行文件,它可以在没有任何先决条件的情况下运行。
在成功编写了第一个应用之后,许多 Python 程序员最常见的问题是*如何将这些代码转换为可执行文件?。*不幸的是,这个问题没有一个单一的官方答案。多年来,已经启动了许多项目来解决这一任务(例如 Py2Exe、cx_Freeze、Nuitka 和 PyInstaller 等等),它们具有不同程度的支持、简单的使用和结果的一致性。就这些品质而言,目前最好的选择是PyInstaller。
Python 是一种解释语言;代替编译成 C 或 C++的机器代码,您的 Python 代码(或它的优化版本称为 OutT0.BytCeDead To1 T1)由 Python 解释器每次运行时读取和执行。这使得 Python 具有一些特性,使其非常易于使用,但也使其难以编译为机器代码以提供传统的独立可执行文件。
PyInstaller 通过使用 Python 解释器打包脚本以及运行脚本所需的任何库或二进制文件来解决此问题。这些东西被捆绑到一个目录或单个文件中,以提供一个可分发的应用,该应用可以复制到任何系统并执行,即使该系统没有 Python。
要了解其工作原理,请确保已从 PyPI 安装 PyInstaller(请参阅技术要求部分),然后让我们为QTicTacToe
创建一个可执行文件。
Note that the application packages created by PyInstaller are platform-specific and can only be run on an OS and CPU architecture compatible with that on which it was compiled. For example, if you build your PyInstaller executable on 64-bit Linux, it will not run on 32-bit Linux or 64-bit Windows.
理论上,使用 PyInstaller 非常简单,只需打开命令提示符并键入以下内容:
$ pyinstaller my_python_script.py
事实上,让我们用第 4 章中的qt_template.py
文件来尝试这一点,使用 QMainWindow构建应用;将其复制到一个空目录,并在该目录中运行pyinstaller qt_template.py
。
您将获得大量控制台输出,并发现生成了多个目录和文件:
build
和__pycache__
目录主要包含构建过程中生成的中间文件。这些在调试期间可能会有所帮助,但它们不是最终产品的一部分。dist
目录包含我们的可分发输出。qt_template.spec
文件保存 PyInstaller 生成的配置数据。
默认情况下,PyInstaller 生成一个目录,其中包含可执行文件以及它工作所需的所有库和数据文件。如果要运行可执行文件,必须将整个目录复制到另一台计算机上。
输入此目录并查找名为qt_template
的可执行文件。如果你运行它,你会看到一个空白的QMainWindow
对象弹出。
如果您只想拥有一个文件,PyInstaller 可以将这个目录压缩成一个可执行文件,在运行时,它会将自身解压缩到一个临时位置,并运行主可执行文件。
这可以通过--onefile
参数来实现;删除dist
和build
的内容,然后运行此命令:
$ pyinstaller --onefile qt_template.py
现在,在dist
下,您将只找到一个qt_template
可执行文件。再次运行它,您将看到我们的空白QMainWindow
。请记住,虽然这种方法比较整洁,但它会增加启动时间(因为需要提取应用),并且如果应用打开本地文件,可能会造成一些复杂情况,我们将在下面看到。
If you make significant changes to your code, environment, or build specifications, it's a good idea to delete the build
and dist
directories, and possibly the .spec
file.
在我们尝试打包 AutoT0 之前,让我们深入研究一下 Ont1 文件。
.spec
文件是一个 Python 语法config
文件,其中包含关于构建的所有元数据。您可以将其视为 PyInstaller 对setup.py
文件的回答。但是,与setup.py
不同,.spec
文件是自动生成的。每当我们运行pyinstaller
时,就会发生这种情况,使用脚本中检测到的数据和通过命令行开关传入的数据的组合。我们也可以使用pyi-makespec
命令生成.spec
文件(而不是启动构建)。
生成后,可以编辑.spec
文件,然后将其传回pyinstaller
以重建分发,而无需每次指定命令行开关:
$ pyinstaller qt_template.spec
要查看我们可以在该文件中编辑哪些内容,请再次运行pyi-makespec qt_template.py
,并在编辑器中打开qt_template.spec
。在该文件中,您将发现正在创建四种对象:Analysis
、PYZ
、EXE
和COLLECT
。
Analysis
构造函数接收有关脚本、数据文件和库的信息。它使用这些信息来分析项目的依赖关系,并生成五个路径表,指向应该包含在分发中的文件。这五个表格是:
scripts
:作为入口点并将转换为可执行文件的 Python 文件pure
:脚本所需的纯 Python 模块binaries
:脚本所需的二进制库datas
:非 Python 数据文件,如文本文件或图像zipfiles
:任何压缩的 Python.egg
文件
在我们的文件中,Analysis
部分如下所示:
a = Analysis(['qt_template.py'],
pathex=['/home/alanm/temp/qt_template'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
您可以看到 Python 脚本的名称、路径和许多空关键字参数。这些参数中的大多数对应于输出表,用于手动补充 PyInstaller 无法检测到的分析结果,包括以下内容:
binaries
对应于binaries
表。datas
对应于datas
表。hiddenimports
对应于pure
表。excludes
允许我们省略可能自动包含但实际上不需要的模块。hookspath
和runtime_hooks
允许您手动指定 PyInstaller挂钩;挂钩允许您覆盖分析的各个方面。它们通常用于处理麻烦的依赖关系。
下一个创建的对象是PYZ
对象:
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
PYZ
对象表示在分析阶段检测到的所有纯 Python 脚本的压缩存档。我们项目中的所有纯 Python 脚本都将被编译成字节码(.pyc
文件)并打包到这个归档文件中。
注意Analysis
和PYZ
中都存在cipher
参数;此参数可用于使用 AES256 加密进一步模糊 Python 字节码。虽然它不能完全阻止代码的解密和反编译,但如果您计划将代码商业化分发,它可以对好奇的人起到有用的威慑作用。要使用此选项,请在创建文件时使用--key
参数指定加密字符串,如下所示:
$ pyi-makespec --key=n0H4CK1ngPLZ qt_template.py
在PYZ
部分之后,生成一个EXE()
对象:
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='qt_template',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
EXE
对象表示可执行文件。这里的位置参数表示我们绑定到可执行文件中的所有文件表。现在,这只是压缩的 Python 库和主脚本;如果我们指定了--onefile
选项,其他表格(binaries
、zipfiles
和datas
也将包括在这里。
EXE
的关键字参数允许我们控制可执行文件的各个方面:
name
是可执行文件的文件名debug
切换可执行文件的调试输出upx
切换是否使用UPX压缩可执行文件console
在 Windows 和 macOS 中切换是在控制台还是 GUI 模式下运行程序;在 Linux 中,它没有效果
UPX is a free executable packer available for multiple platforms from https://upx.github.io/. If you have it installed, enabling this argument can make your executables smaller.
流程的最后一个阶段是生成一个COLLECT
对象:
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='qt_template')
此对象将所有必需的文件收集到最终分发目录中。它仅在一种目录模式下运行,其位置参数包括要包含在目录中的组件。我们还可以覆盖文件夹的一些其他方面,例如是否在二进制文件上使用 UPX 以及输出目录的名称。
现在我们对 PyInstaller 的工作原理有了更多的了解,让我们打包 qticActoE。
PyInstaller 在使用单个脚本时非常简单,但它如何与我们的模块式项目安排配合使用呢?我们不能将 PyInstaller 指向我们的模块,因为它将返回一个错误;它需要指向作为入口点的 Python 脚本,比如我们的run.py
文件。
这似乎有效:
$ pyinstaller run.py
然而,产生的分发和可执行文件现在被称为run
,这并不太好。你可能会想把run.py
改为qtictactoe.py
;事实上,一些关于 Python 打包的教程推荐这种安排(也就是说,run
脚本与主模块同名)。
但是,如果尝试此操作,您可能会发现出现如下错误:
Traceback (most recent call last):
File "qtictactoe/__init__.py", line 3, in <module>
from .mainwindow import MainWindow
ModuleNotFoundError: No module named '__main__.mainwindow'; '__main__' is not a package
[3516] Failed to execute script qtictactoe
由于 Python 模块可以是.py
文件或目录,PyInstaller 无法确定是哪一个模块构成qtictactoe
模块,因此两者的名称相同将失败。
正确的方法是在创建我们的.spec
文件或运行pyinstaller
时使用--name
开关:
$ pyinstaller --name qtictactoe run.py
# or, to just create the spec file:
# pyi-makespec --name qtictactoe run.py
这将创建qtictactoe.spec
并将EXE
和COLLECT
的name
参数设置为qtictactoe
,如下所示:
exe = EXE(pyz,
#...
name='qtictactoe',
#...
coll = COLLECT(exe,
#...
name='qtictactoe')
当然,这也可以通过编辑.spec
文件手动完成。
我们的程序运行了,但我们回到了老问题,'X'
和'O'
图像没有显示。这里有两个问题:第一,我们的 PNG 文件没有成为发行版的一部分,第二,即使是在发行版中,程序也找不到它们。
为了解决第一个问题,我们必须告诉 PyInstaller 在构建的Analysis
阶段将我们的文件包括在datas
表中。我们可以在命令行中执行此操作,如下所示:
# On Linux and macOS:
$ pyinstaller --name qtictactoe --add-data qtictactoe/images:images run.py
# On Windows:
$ pyinstaller --name qtictactoe --add-data qtictactoe\images;images run.py
--add-data
参数采用由冒号(在 macOS 和 Linux 上)或分号(在 Windows 上)分隔的源路径和目标路径。源路径相对于我们正在运行的项目根目录pyinstaller
(QTicTacToe
,在本例中),目标路径相对于分发根文件夹。
如果我们不想制作一个长而复杂的命令行,我们还可以更新qtictactoe.spec
文件的Analysis
部分:
a = Analysis(['run.py'],
#...
datas=[('qtictactoe/images', 'images')],
这里,源路径和目标路径只是datas
列表中的一个元组。源值也可以是模式,例如qtictactimg/*.png
。如果您使用这些更改运行pyinstaller qtictactoe.spec
,您应该在dist/qtictactoe
中找到一个images
目录,其中包含我们的 PNG 文件。
这解决了图像的第一个问题,但我们仍然需要解决第二个问题。在使用 setuptools 分发的部分中,我们使用__file__
内置变量解决了定位 PNG 文件的问题。但是,当您从 PyInstaller 可执行文件运行时,__file__
的值是而不是可执行文件的路径;它实际上是一个临时目录的路径,可执行文件在其中解压压缩字节码。此目录的位置根据我们是处于一个文件模式还是一个目录模式而变化。为了解决这个问题,我们需要更新代码,以检测程序是否已生成可执行文件,如果是,则使用不同的方法来定位文件。
当我们运行 PyInstaller 可执行文件时,PyInstaller 向sys
模块添加了两个属性以帮助我们:
sys.frozen
属性,该属性的值为True
sys._MEIPASS
属性,用于存储可执行目录的路径
因此,我们可以将board.py
中的代码更新为如下内容:
if getattr(sys, 'frozen', False):
directory = sys._MEIPASS
else: # Not frozen
directory = path.dirname(__file__)
self.mark_pngs = {
'X': qtg.QPixmap(path.join(directory, 'images', 'X.png')),
'O': qtg.QPixmap(path.join(directory, 'images', 'O.png'))
}
现在,当从冻结的 PyInstaller 环境执行时,我们的代码将能够正确定位文件。重新运行pyinstaller qtictactoe.spec
,您会发现X
和O
图形显示正确。好极了
As mentioned before, the far better solution in a PyQt5 application is to use the Qt Resource files discussed in Chapter 6, Styling Qt Applications. For non-PyQt programs, the setuptools
library has a tool called pkg_resources
that might be helpful.
如果您的构建仍然有问题,有两种方法可以获得更多关于正在发生的事情的信息。
首先,确保代码作为 Python 脚本正确运行。如果您的任何模块文件中存在语法错误或其他代码问题,则将在不使用它们的情况下生成发行版。这些遗漏既不会停止构建,也不会在命令行输出中提及。
确认后,检查构建目录以了解 PyInstaller 正在执行的操作的详细信息。在build/projectname/
下,您应该会看到一些可以帮助您调试的文件,包括:
warn-projectname.txt
:包含Analysis
流程输出的警告。其中一些是没有意义的(通常只是找不到平台上不存在的特定于平台的库),但如果库有错误或找不到,这些问题将记录在此处。.toc
文件:包含构建过程各阶段创建的目录;例如,Analysis-00.toc
显示在Analysis()
中找到的表格。您可以检查这些,以查看项目的依赖项是否被错误地标识或从错误的位置提取。base_library.zip
:此归档文件应包含应用使用的所有纯 Python 模块的 Python 字节码文件。你可以检查一下,看看是否有什么东西丢失了。
如果需要更详细的输出,可以使用--log-level
开关将输出的细节增加到warn-projectname.txt
。DEBUG
的设置将提供更多细节:
$ pyinstaller --log-level DEBUG my_project.py
更多调试提示可在找到 https://pyinstaller.readthedocs.io/en/latest/when-things-go-wrong.html 。
在本章中,您学习了如何与他人共享您的项目。您学习了项目目录的最佳布局,使您能够与其他 Python 编码器和 Python 工具协作。您学习了如何使用setuptools
为 PyPI 等站点制作可分发的 Python 包。最后,您学习了如何使用 PyInstaller 将代码转换为可执行文件。
祝贺你已经看完这本书了。现在,您应该对使用 Python 和 PyQt5 从头开始开发引人注目的 GUI 应用的能力充满信心。从基本的输入窗体到高级的网络、数据库和多媒体应用,您现在拥有了创建和分发惊人程序的工具。即使我们已经讨论了所有的主题,PyQt 中仍有更多的内容有待发现。不断学习,成就伟大的事业!
尝试回答以下问题以测试您在本章中的知识:
- 您已经在名为
Scan & Print Tool-box.py
的文件中编写了 PyQt 应用。您希望将其转换为模块式组织;你应该做什么改变? - PyQt5 数据库应用有一组包含应用使用的查询的
.sql
文件。当你的应用是与.sql
文件位于同一目录中的单个脚本时,它就起作用了,但现在你已经将它转换为模块式的组织,查询就找不到了。你该怎么办? - 您正在编写一个详细的
README.rst
文件来记录您的新应用,然后再将其上载到代码共享站点。应分别使用哪些字符在 1 级、2 级和 3 级标题下划线? - 您正在为您的项目创建一个
setup.py
脚本,以便将其上载到 PyPI。您希望包含项目常见问题页面的 URL。你如何做到这一点? - 您已在
setup.py
文件中指定了include_package_data=True
,但由于某些原因,docs
文件夹未包含在分发包中。发生了什么? - 您运行
pyinstaller fight_fighter3.py
将新游戏打包为可执行文件。不过,出了点问题;在哪里可以找到构建过程的日志? - 尽管名称不同,PyInstaller 实际上无法为应用生成安装程序或程序包。为您选择的平台研究一些选项。
有关更多信息,请参阅以下内容:
-
有关
ReStructuredText
标记的教程可在上找到 http://docutils.sourceforge.net/docs/user/rst/quickstart.html 。 -
关于 Python GUI 应用的设计、结构化、文档化和打包的更多信息,请参阅作者的第一本书TkinterPython GUI 编程,可从Packt 出版物获得。
-
如果您有兴趣将包发布到 PyPI,请参阅https://blog.jetbrains.com/pycharm/2017/05/how-to-publish-your-package-on-pypi/ 获取有关该过程的教程。
-
对于在 Python 库中为非 PyQt 代码包含图像的问题,更好的解决方案是
setuptools
提供的pkg_resources
工具。您可以在上阅读 https://setuptools.readthedocs.io/en/latest/pkg_resources.html 。 -
PyInstaller 的高级用法记录在中的 PyInstaller 手册中 https://pyinstaller.readthedocs.io/en/stable/ 。