Skip to content

33. 使用相对路径

路径描述了文件在文件系统中的位置。

当我们将外部数据文件加载到应用程序中时,通常会使用路径来完成这一操作。虽然从理论上讲这很简单,但实际操作中可能会遇到一些问题。随着应用程序规模的扩大,维护这些路径可能会变得有些繁琐,因此值得退一步考虑实施一个更可靠的系统。

相对路径

路径有两种类型——绝对路径和相对路径。绝对路径描述了从文件系统根目录(底部)开始的完整路径,而相对路径则描述了从当前文件系统位置开始的路径(或相对于当前位置的路径)。

这并不明显,但当您仅提供文件名时,例如:hello.jpg,这实际上是一个相对路径。当文件被加载时,它是相对于当前活动文件夹加载的。令人困惑的是,当前活动文件夹并不一定就是您的脚本所在的文件夹。

在“控件”一章中,我们介绍了一种处理加载图像时出现此问题的简单方法。我们使用内置函数 __file__ 获取当前正在运行的脚本(我们的应用程序)的路径,然后使用 os 函数首先获取脚本的目录,再用该目录构建完整路径。

Listing 240. basic/widgets_2b.py

python
import os
import sys

from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow

basedir = os.path.dirname(__file__)
print("Current working folder:", os.getcwd()) #1
print("Paths are relative to:", basedir) #2


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.setWindowTitle("My App")
        
        widget = QLabel("Hello")
        widget.setPixmap(QPixmap(os.path.join(basedir, "otje.jpg")))
        
        self.setCentralWidget(widget)
        
        
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

对于简单应用程序而言,这种方法效果良好,尤其是当您仅有一个主脚本且需要加载的文件较少时。然而,在加载每个文件时都需要重复计算基目录,并使用 os.path.join 方法在各处构建路径,这很快就会变成维护噩梦。如果您需要重新组织项目中的文件结构,那将是一场噩梦。幸运的是,有一种更简单的方法!

tips

为什么不直接使用绝对路径呢?因为它们只适用于您的文件系统,或者结构完全相同的文件系统。如果我在自己的主目录中开发应用程序,并使用绝对路径来引用文件,例如:/home/martin/myapp/images/somefile.png,那么它只会在其他也拥有名为 martin 的主目录并将其放置在该目录下的人的系统上生效。这会有点奇怪。

使用 Paths 类

应用程序需要加载的数据文件通常具有一定的结构——加载的文件类型通常较为常见,或者加载这些文件的目的是为了实现常见的功能。通常,您会将相关的文件存储在相关的文件夹中,以便于管理。我们可以利用这种现有的结构,建立一种常规的方法来构建文件的路径。

要实现这一点,我们可以创建一个自定义的 Paths 类,该类通过结合使用属性与方法来分别构建文件夹和文件路径。其核心逻辑与上述使用的 os.path.dirname(__file__)os.path.join() 方法相同,但具有更高的自包含性且易于修改。

现在请您将以下代码添加到项目根目录下的一个名为 paths.py 的文件中。

Listing 241. further/paths.py

python
import os


class Paths:
    
    base = os.path.dirname(__file__)
    ui_files = os.path.join(base, "ui")
    images = os.path.join(base, "images")
    icons = os.path.join(images, "icons")
    data = os.path.join(base, "images")
    
    # 文件加载器.
    @classmethod
    def ui_file(cls, filename):
        return os.path.join(cls.ui_files, filename)
    
    @classmethod
    def icon(cls, filename):
        return os.path.join(cls.icons, filename)
    
    @classmethod
    def image(cls, filename):
        return os.path.join(cls.images, filename)
    
    @classmethod
    def data(cls, filename):
        return os.path.join(cls.data, filename)

tips

要尝试使用 paths 模块,您可以启动一个 Python 解释器,位于您的项目根目录下,并使用 from paths import Paths

现在,在应用程序的任何位置,您都可以导入 Paths 类并直接使用它。属性 baseui_filesiconsimagesdata 均返回其对应文件夹在 base 文件夹下的路径。请注意 icons 文件夹是如何从 images 路径构建的——将该文件夹嵌套在该路径下。

tips

您可以自由地自定义路径的名称和结构等,以匹配您自己项目中的文件夹结构。

bash
>>> from paths import Paths
>>> Paths.ui_files
'U:\\home\\martin\\books\\create-simple-gui-applications\\code\\further\\ui'
>>> Paths.icons
'U:\\home\\martin\\books\\create-simple-gui-applications\\code\\further\\images\\icons'

information

我们不会从这个类创建对象实例——我们不会调用 Paths()——因为我们不需要它。路径是静态且不变的,因此无需通过创建对象来管理内部状态。注意,方法必须使用 @classmethod 装饰器才能在类本身访问。

方法 ui_fileiconimagedata 用于生成包含文件名的路径。在每种情况下,您都需要调用该方法并传入要添加到路径末尾的文件名。这些方法均依赖于上述描述的文件夹属性。例如,如果您想加载特定的图标,可以调用 Paths.icon() 方法,传入图标名称,以获取完整的路径。

bash
>>> Paths.icon('bug.png')
'U:\\home\\martin\\books\\create-simple-gui-applications\\code\\further\\images\\icons\\bug.png'

在您的应用程序代码中,您可以按照以下方式构建路径并加载图标:

python
QIcon(Paths.icon('bug.png'))

这使得您的代码更加整洁,有助于确保路径正确,并且如果您以后想重新组织文件的存储方式,会变得更加容易。例如,假设您想将图标移动到顶级文件夹:现在您只需修改 paths.py 中的定义,所有图标仍可正常工作。

python
    icons = os.path.join(images, 'icons')
    # 要提升到顶层,请让图标从基类继承。
    icons = os.path.join(base, 'icons')