一般来说,Python项目是如何结构化的?

28 投票
1 回答
12347 浏览
提问于 2025-04-17 20:48

我在项目结构方面有点迷茫。我试着把东西整理得有条理,但每天至少要重组两次。虽然我的项目不算大,但我希望能找到一个合适的结构,不用再频繁调整了。

我来描述一下我现在的程序,看看能不能理清思路。这个程序是一个图形界面程序,后面有一个数据库,用来计算帆的价格。虽然还没写完,但用户可以从两个下拉菜单中选择帆的类别和型号。根据选择的类别和型号,程序会显示一些复选框和输入框。当这些复选框和输入框的内容改变时,程序会从数据库中获取信息,并显示选中复选框的价格或输入框中某个数字(比如面积,以平方米为单位)的价格。

目前这个项目的结构是这样的:

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.pyOptionsWidget.py是Python文件,里面包含同名的对象,去掉.py后缀。MainWindow在它的显示界面中包含了OptionsWidget。这两个对象都使用各自的uirc文件。

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 个回答

44

我们先来解决你最后一个问题,因为在构建 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(或者其他的)。

这乍一看可能会觉得有点复杂,但找到一个适合的项目结构的主要好处是,你可以在未来的所有项目中重复使用它(并开始开发自己的模板和工具来设置和管理这些项目)。

撰写回答