Python: 将脚本的工作目录更改为脚本所在的目录。

243

我从crontab每一分钟运行一个Python shell:

* * * * * /home/udi/foo/bar.py

/home/udi/foo 目录下有一些必要的子目录,例如 /home/udi/foo/log/home/udi/foo/config/home/udi/foo/bar.py 脚本引用了这些子目录。

问题在于 crontab 从不同的工作目录运行脚本,因此尝试打开 ./log/bar.log 失败了。

有没有一个简单的方法告诉脚本将工作目录更改为自己所在的目录?我希望这个解决方案适用于任何脚本位置,而不是显式地告诉脚本其位置。

编辑:

os.chdir(os.path.dirname(sys.argv[0]))

这是最简洁优雅的解决方案。感谢您的答案和解释!


crontab用例无关:如果使用execfile()运行脚本,则sys.argv[0]__file__都会失败;可以使用基于inspect的解决方案。 - jfs
回答不应放在问题中,应该使用接受和投票机制来表示首选答案。将答案放在问题中会导致不能对该答案进行投票、单独发表评论等。详情请参阅[meta]上的“当问题的答案被添加到问题本身时应采取的适当措施是什么?” - Charles Duffy
5个回答

294

这将更改您的当前工作目录,以便打开相对路径时能正常工作:

import os
os.chdir("/home/udi/foo")

然而,你想知道如何切换到Python脚本所在的目录,即使你写脚本时不知道那个目录在哪里。为了做到这一点,你可以使用os.path函数:

import os

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)

这个函数获取你的脚本文件名,将其转换为绝对路径,然后提取该路径的目录,最后切换到该目录。


3
等同于硬编码目录。 - Ikke
2
如果您正在从符号链接运行它,则此方法将无法正常工作。请使用__file__代替sys.argv[0] - Chris Down
2
为什么要使用abspath步骤?为什么不直接使用os.chdir(os.path.dirname(__file__)) - Colonel Panic
8
在使用py2exe、PyInstaller和cx_Freeze创建的“冻结”程序中,__file__会出现故障。sys.argv[0]可以工作。如果想要跟踪符号链接,可以使用os.path.realpath() - jfs
4
如果__file__不是绝对路径,并且用户已更改工作目录,那么os.path.abspath也会失败。 - Arthur Tacca
显示剩余6条评论

65

你可以使用 sys.path[0] 来获取一个更短的版本。

os.chdir(sys.path[0])

摘自http://docs.python.org/library/sys.html#sys.path

程序启动时,这个列表的第一项path[0]被初始化为包含用于调用Python解释器的脚本的目录。


1
请注意,如果您 import git,则此方法无效。第一个路径指向 <pythondir>/lib/site-packages/git/ext/gitdb。至少这对我是这样的。 - marco
1
谢谢,@marco。我刚刚为此创建了一个PR https://github.com/gitpython-developers/GitPython/pull/1068 - xverges

27

不要这样做。

你的脚本和数据不应该混在一个大目录中。将代码放在一些已知位置(site-packages/var/opt/udi或其他地方),与数据分开。对代码使用良好的版本控制,以确保您有当前版本和先前版本分开,以便您可以回退到以前的版本并测试未来的版本。

底线:不要混合代码和数据。

数据很宝贵。代码来来去去。

将工作目录作为命令行参数值提供。你可以提供一个环境变量作为默认值。不要推断它(或猜测它)。

将其设置为必需的参数值,并执行此操作。

import sys
import os
working= os.environ.get("WORKING_DIRECTORY","/some/default")
if len(sys.argv) > 1: working = sys.argv[1]
os.chdir( working )

不要根据您的软件位置“假设”目录,这样长远来看会带来不好的结果。


14
我认为你对于将代码和数据分离的观点是正确的,但对于小型维护脚本来说,这种做法似乎有些牵强。我完全同意版本控制的重要性。 - Adam Matan
3
S. Lott是正确的。除非数据不是短暂的,否则一定要将数据和代码分开。例如,如果您有图标,那就是数据,但它不是短暂的,因此考虑与软件包(无论这意味着什么)相关是有意义的。 - Stefano Borini
7
@Udi Pasmon: 毫不牵强。正是这些“小维护脚本”会让组织陷入深深的麻烦。多年后,这些“小维护脚本”及其子脚本、派生物和数据文件将成为一个难以解开和重新实现的噩梦。尽可能将数据与代码分离- 为所有内容传递参数- 不要假设任何东西。 - S.Lott
1
+1 我本来想按照原帖的做法,但是在阅读了您的建议后,我改进了我的脚本。现在它需要一个参数来指定日志文件的位置。 - Iain Samuel McLean Elder
1
如果数据目录可以轻松自定义,那么为 Python 脚本创建软件包(rpm)会更容易。 - jfs
显示剩余2条评论

26

将您的crontab命令更改为

* * * * * (cd /home/udi/foo/ || exit 1; ./bar.py)

(...) 开始一个子shell,你的crond会将其作为单个命令执行。 || exit 1 会导致你的cronjob在目录不可用的情况下失败。

虽然其他解决方案对于你特定的脚本可能更加优雅,但我的例子仍然可以在你不能修改要执行的程序或命令的情况下有用。


2
这是一个非常可靠的解决方案。我通常会发现自己编辑其他人的答案,添加像 || exit 1 这样的东西。看到这个真是让人耳目一新。虽然我确实想知道为什么你不只是做 cd /home/udi/foo/ && ./bar.py - Bruno Bronosky
3
使用明确的 exit 1,您的 crond 将会收到一条错误通知。在大多数情况下,它会发送一封电子邮件通知失败的消息。请注意,这里不会有任何解释或其他内容返回。 - Ruud Althuizen
@RuudAlthuizen cd /home/udi/foo/ && ./bar.py 将会做同样的事情。 - amit kumar

0

以防万一,您也可以使用pathlib

import os
from pathlib import Path

file = Path(__file__)
parent = file.parent
os.chdir(parent)
# print(file, parent, os.getcwd())

或者单行显示:

os.chdir(Path(__file__).parent)

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