Python 多模块对同一文件读取时引发的相对路径问题

假设我的项目中有一个模块 A.py 和模块 B.py, A 和 B 位于不同的路径深度. B 里面有一个读取文件的方法 (用的是相对路径).

当单独运行 B 时没有问题. 但当 A 去调用 B 的读取方法, 就会报错.

本文提出的问题就是, 该如何解决 不同层级的模块相互调用时, 对同一文件读取时的相对路径该怎么统一地表示呢?


先说结论

为了解决这个问题, 我自制了一个 “file_locator.py”, 用于统一路径调用方法.

您可以从网盘下载此文件 “file_locator_20190414_182658.py” (注意下载后去掉文件名中的时间戳. 或者直接复制后面贴出的 文件源码): https://www.jianguoyun.com/p/DdwXlkEQ7t6sBxiy7LEB

然后放到项目中:

D:workspace/my_project/
|- data/
	|- hello.txt
|- M/
	|- N/
		|- O/
			|- P.py
		|- Q.py
|- util/
	|- B.py
	|- file_locator.py  # 假设放到了这里
|- A.py

使用方法:

file_locator.py 主要的方法只有一个 getfile(path), path 参数传入的是 相对于项目根目录的路径.

这意味着无论你在哪个模块中调用它, 只需要使用 “相同” 的路径, 就能找到同一个文件:

# ./A.py
from util.file_locator import getfile

print(getfile('data/hello.txt'))
# 打印结果: 'D:workspace/my_project/data/hello.txt'

# ----------------------------------------------------------------

# ./util/B.py
from util.file_locator import getfile

print(getfile('data/hello.txt'))
# 打印结果: 'D:workspace/my_project/data/hello.txt'

# ----------------------------------------------------------------

# ./M/N/O/P.py
from util.file_locator import getfile

print(getfile('data/hello.txt'))
# 打印结果: 'D:workspace/my_project/data/hello.txt'

# ----------------------------------------------------------------

# ./M/N/Q.py
from util.file_locator import getfile

print(getfile('data/hello.txt'))
# 打印结果: 'D:workspace/my_project/data/hello.txt'

file_locator.py 源码

我的基本思路是, 把原本每个模块中使用相对路径的地方, 全部地方都改用绝对路径来定位.

当然不可能真的在代码里面这样写, 一是团队协作或多机同步时每台电脑的绝对路径可能是不一样的, 二是写法太繁琐.

那么就让 绝对路径在运行时生成 是最好的解法.

file_locator.py 便是基于此工作的. 完整代码如下:

import os
from sys import argv


# -------------------------------- prettifier

def prettify_dir(adir: str) -> str:
    """
    美化文件夹路径.
    
    输入: "..\\A\\B\\C"
    输出: "../A/B/C/"
    """
    if '\\' in adir:
        adir = adir.replace('\\', '/')
    if adir[-1] != '/':
        adir += '/'
    return adir


def prettify_file(afile: str) -> str:
    """
    美化文件路径.
    
    输入: "A\\B\\C.txt"
    输出: "A/B/C.txt"
    """
    if '\\' in afile:
        afile = afile.replace('\\', '/')
    return afile


# -------------------------------- basic path locator

def get_launch_path():
    """
    获得启动模块的绝对路径
    
    输入:
        sys.argv[0]
            示例: 'D:\\workspace\\my_project\\A.py'
    输出:
        'D:/workspace/my_project/A.py'
    """
    path = argv[0]
    return prettify_file(path)


def find_project_dir(apath='', recursive_times=0):
    """
    获得当前项目的根目录的绝对路径.
    已知 afile = "D:/workspace/my_project/A/B/C/D.py", 求哪一个目录是该项目的根路径?

    提示:
        1. 项目根路径下面有 ".idea" 文件夹, 这是一个判断特征
        2. os.listdir() 是一个可迭代对象, 使用 `iter(os.listdir())` 可将其转换为迭代器
        2. 使用迭代器, 可以大幅减少遍历文件树花费的时间

    原理:
        假设我们知道启动文件的绝对路径是 "D:/workspace/my_project/A/B/C/D.py", 我们以此
        为观察点, 不断向上查找 (递归), 看哪一层能够找到 ".idea" 文件夹, 当找到时, 就认为这
        是项目的根路径.

    输入:
        启动文件的绝对路径
            示例: "D:/workspace/my_project/A/B/C/D.py"
    输出:
        "D:/workspace/my_project/"

    注意:
        1. 本设计有一个缺陷, 假如我的项目结构为:
            D:/workspace/main_project/
                |- .idea
                |- sub_project
                    |- .idea
                    |- B.py
                |- A.py
            此时我打算从 main_project 项目启动并运行 B.py, 则本函数会误认为 sub_project
            是项目根路径.
            要避免此错误, 请这样做 (方法二选一):
                1. 删除 sub_project 下的 .idea 文件夹
                2. 新建一个 D:/workspace/main_project/C.py 引用 B.py 启动, 而不要直接
                从 B.py 启动

    """
    if not apath:
        apath = get_launch_path()
        # 第一次查找时, 获取启动文件的绝对路径
    if recursive_times > 10:
        # 预防机制: 当启动文件路径过深时 (默认深度为10层), 本函数会报错
        raise AttributeError
    
    d = os.path.dirname(apath)  # d 是 directory 的缩写
    p = iter(os.listdir(d))  # p 是 paths 的缩写
    
    while True:
        sd = p.__next__()  # sd 是 sub-directory 的缩写
        if sd[0] == '.':
            if sd == '.idea':
                return prettify_dir(d)  # 返回项目根目录
            else:
                # 可能遇到了 '.abc', '.git' 等文件 (夹)
                continue
        else:
            break
    
    return find_project_dir(d, recursive_times + 1)  # 递归查找


# --------------------------------

root = find_project_dir()


def getfile(path):
    return root + path

使用注意

file_locator.py 需要在 Pycharm 中使用. 其他 IDE 未测试, 但很可能会报错.

原因在于, 我在 file_locator.get_launch_path() 方法中查找启动文件的绝对路径时, 用的是 sys 模块的 argv 参数.

Pycharm 有一个好处是, 当你启动文件时, 会自动在 argv 中添加当前启动文件的绝对路径, 所以就给我提供了很大的方便.

原理在于, Pycharm 的配置中有一个这个选项 (默认是开启的):

Python 多模块对同一文件读取时引发的相对路径问题_第1张图片

你可能感兴趣的:(默认分类)