一般来说,Python项目是如何结构化的?
我在项目结构方面有点迷茫。我试着把东西整理得有条理,但每天至少要重组两次。虽然我的项目不算大,但我希望能找到一个合适的结构,不用再频繁调整了。
我来描述一下我现在的程序,看看能不能理清思路。这个程序是一个图形界面程序,后面有一个数据库,用来计算帆的价格。虽然还没写完,但用户可以从两个下拉菜单中选择帆的类别和型号。根据选择的类别和型号,程序会显示一些复选框和输入框。当这些复选框和输入框的内容改变时,程序会从数据库中获取信息,并显示选中复选框的价格或输入框中某个数字(比如面积,以平方米为单位)的价格。
目前这个项目的结构是这样的:
COPYING
README.md
SailQt.pyw (Should program be called from here ...)
sailqt/
__init__.py (This holds a __version__ string)
SailQt.pyw (... or here?)
gui/
__init__.py
MainWindow.py (This needs access to a __version__ string)
MainWindow_rc.py
OptionsWidget.py
ui_MainWindow.py
ui_OptionsWidget.py
resources/
__init__.py
database.db
generate_gui.py
MainWindow.ui
MainWindow.qrc
OptionsWidget.ui
icons/
logo.png
再进一步说明一下。resources
文件夹里存放着所有在Qt Designer中制作的.ui
文件。这些文件是XML格式的,用来描述图形界面。可以用一个终端工具把它们转换成Python脚本,我把这个工具嵌入到了generate_gui.py
里。.qrc
文件也是一样。generate_gui.py
会把自动生成的文件放到gui
文件夹里,文件名前会加上ui_
前缀或后面加上_rc
后缀。database.db
现在是空的,但最终会用来存放价格和其他信息。
MainWindow.py
和OptionsWidget.py
是Python文件,里面包含同名的对象,去掉.py
后缀。MainWindow
在它的显示界面中包含了OptionsWidget
。这两个对象都使用各自的ui
和rc
文件。
SailQt.pyw
是用来创建MainWindow
实例的文件,它会让这个窗口显示出来,然后告诉(Py)Qt进入它的循环,接管后续的操作。这个文件就像很多图形应用程序的.exe
文件一样,是一个小文件,用来启动程序。
我最开始的想法是把SailQt.pyw
放在sailqt
文件夹里。但后来MainWindow.py
突然需要访问一个__version__
字符串。我找到的唯一解决办法就是把SailQt.pyw
移动到项目的根文件夹,让MainWindow.py
导入sailqt.__version__
。但考虑到这是我第n次调整结构并且需要重新修改大部分文件的代码,我决定在这里问问。
我的问题很明确:
- 一般来说,Python项目是怎么结构的?这个pydoc链接对我有帮助,但感觉更像是一个模块,而不是用户实际执行的东西。
- 我上面的结构安排对吗?
- 如果能回答这个问题就更好了,虽然有点偏题。为什么我可以执行
import os
,然后像os.system("sudo rm -rf /")
那样做,但我却不能执行import sailqt
,然后做sailqt.gui.generate_gui.generate()
?
1 个回答
我们先来解决你最后一个问题,因为在构建 Python 项目时,这个问题是最重要的。一旦你搞定了项目中的导入问题,其他的事情就会变得简单很多。
关键是要明白,当前运行的脚本所在的目录会自动添加到 sys.path
的开头。所以,如果你把 main.py
脚本(你现在叫的 SailQt.pyw
)放在包的外面,放在一个顶层的文件夹里,这样就能确保包的导入总是能正常工作,无论脚本从哪里执行。
所以,一个最简单的起始结构可能看起来像这样:
project/
main.py
package/
__init__.py
app.py
mainwindow.py
现在,因为 main.py
必须放在顶层 Python 包目录的外面,它应该只包含最少量的代码(仅仅足够启动程序)。根据上面的结构,这意味着代码不会多于这个:
if __name__ == '__main__':
import sys
from package import app
sys.exit(app.run())
而 app
模块将包含初始化程序和设置图形界面所需的大部分实际代码,导入时可以这样写:
from package.mainwindow import MainWindow
而这种完整的导入语句可以在包的任何地方使用。所以,例如,在这个稍微复杂一点的结构中:
project/
main.py
package/
__init__.py
app.py
mainwindow.py
utils.py
dialogs/
search.py
那么 search
模块可以这样从 utils
模块导入一个函数:
from package.utils import myfunc
关于访问 __version__
字符串的具体问题:对于一个 PyQt 程序,你可以在 app
模块的顶部放入以下内容:
QtGui.QApplication.setApplicationName('progname')
QtGui.QApplication.setApplicationVersion('0.1')
然后可以这样访问名称/版本:
name = QtGui.qApp.applicationName()
version = QtGui.qApp.applicationVersion()
你当前结构的其他问题主要与代码文件和资源文件的分离有关。
首先:包的结构应该只包含代码文件(也就是 Python 模块)。资源文件应该放在项目目录中(也就是包的外面)。其次:从资源生成的代码文件(例如,通过 pyuic 或 pyrcc 生成的)应该放在一个单独的子包中(这样也方便你的版本控制工具将它们排除)。这将导致一个整体项目结构如下:
project/
db/
database.db
designer/
mainwindow.ui
icons/
logo.png
LICENSE
Makefile
resources.qrc
main.py
package/
__init__.py
app.py
mainwindow.py
ui/
__init__.py
mainwindow_ui.py
resources_rc.py
在这里,Makefile
(或等效文件)负责生成 ui/rc 文件,编译 Python 模块,安装/卸载程序等。程序在运行时需要的资源(比如数据库文件)需要安装在程序知道如何找到的标准位置(例如在 Linux 上类似 /usr/share/progname/database.db
)。在安装时,Makefile
还需要生成一个可执行的 bash 脚本(或等效文件),知道你的程序在哪里以及如何启动它。也就是说,类似于:
#!/bin/sh
exec 'python' '/usr/share/progname/main.py' "$@"
这显然需要安装为 /usr/bin/progname
(或者其他的)。
这乍一看可能会觉得有点复杂,但找到一个适合的项目结构的主要好处是,你可以在未来的所有项目中重复使用它(并开始开发自己的模板和工具来设置和管理这些项目)。