为什么要使用importlib.resources而不是__file__?

3

我有一个类似于以下结构的包:

mypkg
    |-mypkg
        |- data
            |- data.csv
            |- __init__.py  # Required for importlib.resources 
        |- scripts
            |- module.py
        |- __init__.py

模块module.py需要data.csv才能执行某个任务。
我最初尝试访问data.csv的方法是幼稚的。
# module.py - Approach 1
from pathlib import Path

data_path = Path(Path.cwd().parent, 'data', 'data.csv')

但是,当我们通过from mypkg.scripts import module或类似方式导入module.py时,这显然会出现问题。我需要一种方法来访问data.csv,无论从何处导入mypkg

下一个天真的方法是使用__file__属性来获取module.py模块所在的路径。

# module.py - Approach 2
from pathlib import Path

data_path = Path(Path(__file__).resolve().parents[1], 'data', 'data.csv')

然而,就我对这个问题的研究而言,我发现这种方法并不被鼓励。例如,请参见如何从Python包内部读取(静态)文件?
虽然在解决这个问题方面似乎没有完全一致的意见,但看起来importlib.resources可能是最受欢迎的方法。我认为这可能是这样的:
# module.py - Approach 3
from pathlib import Path
import importlib.resources

data_path_resource = importlib.resources('mypkg.data', 'data.csv')
with data_path_resources as resource:
    data_path = resource

为什么这种最终方法比__file__更好?似乎如果源代码被压缩,__file__就无法使用。这是我不熟悉的情况,听起来有点边缘化。我不认为我的代码会被压缩运行。
importlib中添加的开销似乎有点荒谬。我需要在数据文件夹中添加一个空的__init__.py,我需要导入importlib,并且我需要使用上下文管理器才能访问相对路径。
我错过了importlib策略的好处吗?为什么不只是使用__file__
编辑: importlib方法的一个可能的理由是它具有稍微改进的语义。也就是说,data.csv应该被视为包的一部分,因此我们应该使用类似from mypkg import data.csv的方式访问它,但当然这个语法仅适用于导入.py python模块。但importlib.resources将“从某个包中导入某些内容”的语义移植到了更一般的文件类型。
相比之下,从__file__构建相对路径的语法有点说:这个模块恰好靠近文件结构中的数据文件,因此让我们利用它来访问。未利用数据文件是包的一部分这一事实。

2
你有没有看过wim的回答?它是按“趋势(最近投票计数更多)”排序的顶级答案。它讨论了为什么不要使用你提到的任何一个。它推荐在Python 3.9+中使用pkgutilimportlib_resources - aaron
@aaron 我想更好地理解链接问题中的顶级答案。(1)关于zip/wheel的事情有哪些更详细的信息?这种用例可能在什么情况下发生,具体是什么样子?(2)在链接答案中的方法中,我想知道如何获取资源路径,以便我可以使用任何辅助模块(csv、h5等)打开任何类型的二进制文件,而不仅仅是作为二进制文件打开。 - Jagerber48
我在Python讨论区上提出了这个问题:https://discuss.python.org/t/easy-and-recommended-way-to-get-path-of-datafile-within-package/20581/8 结论是,对于大多数应用来说,__file__ 是可以的,而且很容易理解,所以如果它能用,我就应该直接使用它。如果我将来涉及到压缩应用程序,我想我需要更多地了解其他方法。 - undefined
1个回答

3

您应该能够像这样使用__file__

import csv
from io import StringIO
from pathlib import Path
import pkgutil
import sys


def main():
    # Point to appropriate ancestor directory
    p = Path(__file__).parent.parent.parent
    sys.path.insert(0, str(p))
    data = pkgutil.get_data('mypkg.data', 'data.csv')
    reader = csv.reader(StringIO(data.decode()))
    for row in reader:
        print(row)


if __name__ == '__main__':
    main()

如果文件data.csv包含以下内容:
Col 1,Col 2
v1,v2

如果运行上述脚本,将会输出以下内容:
['Col 1', 'Col 2']
['v1', 'v2']

如果您选择“Shell”选项卡并运行python mypkg/scripts/module.py,可以在此处查看整个运行过程:https://replit.com/join/qnsnssmrfs-vsajip。请保留HTML标签。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接