如何正确确定当前脚本目录?

405

我想知道在Python中确定当前脚本目录的最佳方法。

由于调用Python代码的方式很多,所以很难找到一个好的解决方案。

以下是一些问题:

  • 如果使用execexecfile执行脚本,则没有定义__file__
  • __module__仅在模块中定义

使用情况:

  • ./myfile.py
  • python myfile.py
  • ./somedir/myfile.py
  • python somedir/myfile.py
  • execfile('myfile.py')(从另一个脚本中调用,该脚本可以位于另一个目录中并且可以具有另一个当前目录)。

我知道没有完美的解决方案,但我正在寻找解决大多数情况的最佳方法。

最常用的方法是os.path.dirname(os.path.abspath(__file__)),但如果使用exec()从另一个脚本中执行脚本,这种方法实际上不起作用。

警告

任何使用当前目录的解决方案都会失败,这可能因脚本调用方式不同而有所不同,或者它可能在运行脚本时发生更改。


1
你能更具体地说明你需要知道文件来自哪里吗?是在导入文件的代码中(包括感知主机)还是在被导入的文件中(自我感知从属)? - synthesizerpatel
5
如果你使用的是Python 3.4或更高版本,请查看Ron Kalian的pathlib解决方案:https://dev59.com/QXA65IYBdhLWcg3wogEb#48931294 - Dan
所以解决方案不是在代码中使用任何当前目录,而是使用一些配置文件? - ZhaoGang
有趣的发现,我刚刚做了:从 shell 运行 python myfile.py 是可以的,但是在 vim 中运行 :!python %:!python myfile.py 都会失败并显示“系统找不到指定的路径”。这非常令人烦恼。有没有人能够评论一下背后的原因以及可能的解决方法? - inVader
16个回答

327
os.path.dirname(os.path.abspath(__file__))

这确实是您能获得的最好结果。

使用exec/execfile执行脚本并不常见;通常应该使用模块基础设施来加载脚本。如果您必须使用这些方法,我建议在传递给脚本的globals中设置__file__,以便它可以读取该文件名。

没有其他方法可以在执行的代码中获取文件名:正如您所指出的,CWD可能完全不同。


6
“永远不要说永远?”根据这个答案:https://dev59.com/enE85IYBdhLWcg3wvGOm#18489147,一个跨平台的解决方案是abspath(getsourcefile(lambda:0))?还是我漏掉了什么? - Jeff Ellen
4
使用pathlib进行更新:pathlib.Path(file).resolve()/'..'。 - dominecf
4
os.path.dirname(os.path.abspath(__file__)) 绝对不是你能得到的最好结果。实际上,这甚至都没有尝试过。正如 @JeffEllen 建议的那样,可以查看 此处此处,以获取更安全、更健壮和更可移植的替代方案。 - Cecil Curry

151

如果你真的想处理通过execfile(...)调用脚本的情况,你可以使用inspect模块来推断文件名(包括路径)。据我所知,这将适用于你列出的所有情况:

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))

5
我认为这确实是最强大的方法,但我质疑原作者声称需要这样做的原因。我经常看到开发人员在使用相对于执行模块位置的数据文件时这样做,但在我看来,数据文件应该放在已知的位置。 - Ryan Ginstrom
19
“@Ryan LOL”,如果你能定义一个跨平台且还带有模块的“已知位置”,那就太好了。我敢打赌,唯一安全的位置是脚本位置。注意,这并不意味着脚本应该写入此位置,但对于读取数据而言,它是安全的。 - sorin
2
然而,这个解决方案并不好,只是尝试在函数之前调用chdir(),它会改变结果。此外,从另一个目录调用Python脚本也会改变结果,因此这不是一个好的解决方案。 - sorin
2
os.path.expanduser("~") 是一种跨平台的获取用户目录的方式。不幸的是,这并不是 Windows 最佳实践中应用程序数据存储的位置。 - Ryan Ginstrom
7
@sorin说:在运行脚本之前,我尝试过使用chdir()函数,它能够产生正确的结果。我还尝试从另一个目录调用脚本,也能够正常工作。这些结果与使用基于inspect.getabsfile()的解决方案相同。(链接为 https://dev59.com/QXA65IYBdhLWcg3wogEb#22881871 ) - jfs
1
这种方法甚至可以在基于Python的框架中运行,而__file__则不行。 - Jetse

67

在Python 3.4+中,您可以使用更简单的pathlib模块:

from inspect import currentframe, getframeinfo
from pathlib import Path

filename = getframeinfo(currentframe()).filename
parent = Path(filename).resolve().parent

如果可用,您还可以使用__file__来完全避免使用inspect模块:

from pathlib import Path
parent = Path(__file__).resolve().parent

在Windows(10)上,当我尝试使用+运算符将另一个字符串附加到文件路径时,出现了以下错误:TypeError: unsupported operand type(s) for +: 'WindowsPath' and 'str'。一个有效的解决方法是在*parent*周围使用str()函数。 - Dut A.
4
您应该使用.joinpath()(或/运算符)进行此操作,而不是使用+ - Eugene Yarmash
2
请注意:如果您想要一个没有符号链接的绝对路径,必须使用resolve()。文档在此处(https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)。 - Jesuisme
在任何__file__不可用的情况下,为了简便起见,当使用pathlib.Path时,可以将__file__替换为点('.')。 因此,当前工作目录是directory = Path('.').resolve(),而parent = Path('..')resolve() - Tim Pozza
1
@TimPozza:当然,你可以这样做,但这不一定是当前脚本的目录。 - martineau

53
#!/usr/bin/env python
import inspect
import os
import sys

def get_script_dir(follow_symlinks=True):
    if getattr(sys, 'frozen', False): # py2exe, PyInstaller, cx_Freeze
        path = os.path.abspath(sys.executable)
    else:
        path = inspect.getabsfile(get_script_dir)
    if follow_symlinks:
        path = os.path.realpath(path)
    return os.path.dirname(path)

print(get_script_dir())

它适用于CPython、Jython、Pypy。如果使用execfile()执行脚本(基于sys.argv[0]__file__的解决方案将会失败),它也能够工作。如果脚本在可执行zip文件(/an egg)中,它也能够工作。如果脚本从zip文件中被"imported"(PYTHONPATH=/path/to/library.zip python -mscript_to_run),则此时它会返回存档路径。如果脚本被编译成独立可执行文件(sys.frozen),它也能够工作。它适用于符号链接(realpath消除符号链接)。在交互式解释器中也可以工作;此时它返回当前工作目录。


与PyInstaller完美兼容,运作良好。 - gaborous
2
getabsfile(..)inspect的文档中为什么没有提到呢?它似乎出现在该页面链接的源代码中。 - Evgeni Sergeev
@EvgeniSergeev 这可能是一个bug。它只是一个简单的包装器,围绕着已经被记录了的getsourcefile()getfile()函数。 - jfs

46

os.path... 方法在 Python 2 中是"成办法"。

在 Python 3 中,你可以按照以下方式找到脚本的目录:

from pathlib import Path
script_path = Path(__file__).parent

19
或者只需使用 Path(__file__).parent。但是,'cwd' 是一个名词误用,它不是指当前工作目录,而是指文件所在的目录。它们可能相同,但通常情况下并非如此。 - Nuno André
1
注意:这将仅返回相对路径。要获取绝对路径,您必须使用resolve()。文档在此处(https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)。 - Jesuisme

9

注意:此答案现已成为一个包(并具有安全的相对导入功能)

https://github.com/heetbeet/locate

$ pip install locate

$ python
>>> from locate import this_dir
>>> print(this_dir())
C:/Users/simon

对于.py脚本以及交互使用:

我经常使用我的脚本目录(用于访问存储在其旁边的文件),但我也经常在交互式 shell 中运行这些脚本进行调试。我将this_dir定义为:

  • 运行或导入.py文件时,文件的基本目录。这始终是正确的路径。
  • 在运行.ipyn笔记本时,当前工作目录。这始终是正确的路径,因为Jupyter将工作目录设置为.ipynb的基础目录。
  • 在REPL中运行时,当前工作目录。当代码与文件分离时,实际的“正确路径”是什么?相反,在调用REPL之前,将其更改为“正确路径”,这应该由您负责。

Python 3.4(及以上):

from pathlib import Path
this_dir = Path(globals().get("__file__", "./_")).absolute().parent

Python 2(及以上版本):

import os
this_dir = os.path.dirname(os.path.abspath(globals().get("__file__", "./_")))

解释:

  • globals()返回所有全局变量的字典形式。
  • .get("__file__", "./_")globals()中获取键为"__file__"的值,如果不存在,则返回提供的默认值"./_"
  • 代码的其余部分将__file__(或"./_")扩展为绝对文件路径,然后返回文件路径的基目录。

备选方案:

如果你确定__file__在周围的代码中可用,你可以简化成以下的代码:

  • >= Python 3.4: this_dir = Path(__file__).absolute().parent
  • >= Python 2: this_dir = os.path.dirname(os.path.abspath(__file__))

你好,Simon Streicher,感谢你的回答。 'locate' 库是否处理更改当前工作目录 (os.chdir) 的问题? - Jako
1
嗨@Jako。是的,对于正常使用情况,locate与当前工作目录分开工作,因此不受os.chdir等因素的影响。但是,在交互式Python会话中使用时则不然!它具有两个功能:1.get_dir函数将返回当前脚本的目录(或在交互式使用时返回os.getcwd())。2.prepend_sys_path(或append_sys_path)将相对于get_dir计算所有路径,然后将其添加到sys.path(更重要的是,它允许临时效果:with prepend_sys_path("../foopath"): import foo)。 - Simon Streicher

8

Would

import os
cwd = os.getcwd()

您想要做什么?我不确定您所指的“当前脚本目录”具体是什么意思。对于您提供的使用情况,预期输出值是什么?


3
不会有帮助。我相信@bogdan正在寻找调用栈顶部脚本的目录,即在他/她所有情况下,应该打印'myfile.py'所在的目录。然而,你的方法只会打印调用exec('myfile.py')的文件的目录,与__file__sys.argv[0]相同。 - Zhang18
是的,那很有道理。我只是想确保@bogdan没有忽略一些简单的东西,而我无法确定他们想要什么。 - Will McCutchen

5
只需使用os.path.dirname(os.path.abspath(__file__))并仔细检查是否真正需要使用exec。如果您无法将脚本用作模块,那可能是一种烦恼的设计信号。
请记住Python之禅#8,如果您认为必须为exec编写特定代码,请让我们了解更多有关问题背景的详细信息。

2
如果您不使用exec()运行,将会失去调试器上下文。此外,exec()应该比启动新进程要快得多。 - sorin
@sorin 这不是执行 vs 启动新进程的问题,所以这是个假命题。而是一个 exec vs 使用导入或函数调用的问题。 - wim

4
要获取包含当前脚本的目录的绝对路径,您可以使用以下方法:
from pathlib import Path
absDir = Path(__file__).parent.resolve()

请注意,需要调用 .resolve() 方法,因为这是使路径绝对化的关键。如果没有使用 resolve(),你会得到像 '.' 这样的结果。
该解决方案使用了 Python 的标准库自 v3.4 (2014) 以来就存在的 pathlib ,与使用 os 的其他解决方案相比更加可取。
官方的 pathlib 文档提供了一个有用的表格,将旧的 os 函数映射到新的函数上:https://docs.python.org/zh-cn/3/library/pathlib.html#correspondence-to-tools-in-the-os-module

3

首先,如果我们讨论注入匿名代码的方式,这里有几个缺失的用例。

code.compile_command()
code.interact()
imp.load_compiled()
imp.load_dynamic()
imp.load_module()
__builtin__.compile()
loading C compiled shared objects? example: _socket?)

但是,真正的问题是,你的目标是什么 - 你是在试图强制实施某种安全性吗? 还是你只是对正在加载的内容感兴趣。

如果你对安全性感兴趣,通过exec / execfile导入的文件名并不重要 - 你应该使用rexec,它提供了以下功能:

该模块包含RExec类,支持r_eval(),r_execfile(),r_exec()和r_import()方法,这些方法是标准Python函数eval(),execfile()和exec和import语句的受限版本。在此受限环境中执行的代码将仅访问被视为安全的模块和函数; 您可以子类化RExec添加或删除所需的功能。

然而,如果这更多是一个学术追求..这里有一些愚蠢的方法,你可能能够深入挖掘..

示例脚本:

./deep.py

print ' >> level 1'
execfile('deeper.py')
print ' << level 1'

./deeper.py

print '\t >> level 2'
exec("import sys; sys.path.append('/tmp'); import deepest")
print '\t << level 2'

/tmp/deepest.py

print '\t\t >> level 3'
print '\t\t\t I can see the earths core.'
print '\t\t << level 3'

./codespy.py

import sys, os

def overseer(frame, event, arg):
    print "loaded(%s)" % os.path.abspath(frame.f_code.co_filename)

sys.settrace(overseer)
execfile("deep.py")
sys.exit(0)

输出

loaded(/Users/synthesizerpatel/deep.py)
>> level 1
loaded(/Users/synthesizerpatel/deeper.py)
    >> level 2
loaded(/Users/synthesizerpatel/<string>)
loaded(/tmp/deepest.py)
        >> level 3
            I can see the earths core.
        << level 3
    << level 2
<< level 1

当然,这是一种资源密集型的方法,你将追踪所有的代码... 不是很高效。但是,我认为这是一种新颖的方法,因为即使你深入嵌套,它仍然可以继续工作。你不能覆盖 'eval'。虽然你可以覆盖 execfile()。
请注意,这种方法只涵盖了exec/execfile,而不是'import'。对于更高级别的'module'加载钩子,您可能可以使用sys.path_hooks(PyMOTW的说明)。
这就是我能想到的全部。

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