Python:相对于当前运行脚本添加到sys.path的最佳方法

148

我有一个脚本目录(假设为project/bin),还有一个位于project/lib的库文件,希望让脚本自动加载它。这是我通常在每个脚本顶部使用的代码:

#!/usr/bin/python
from os.path import dirname, realpath, sep, pardir
import sys
sys.path.append(dirname(realpath(__file__)) + sep + pardir + sep + "lib")

# ... now the real code
import mylib

这种方法有点繁琐、丑陋,并且必须复制到每个文件的开头。有没有更好的方法?

我真正希望的是像这样简洁明了的方法:

#!/usr/bin/python
import sys.path
from os.path import pardir, sep
sys.path.append_relative(pardir + sep + "lib")

import mylib

或者更好的,即使我的编辑器(或者其他有提交权限的人)在整理过程中重新排列导入时也不会出问题的东西:

#!/usr/bin/python --relpath_append ../lib
import mylib

虽然这种方法不能直接迁移到非posix平台,但它可以使事情更加清晰。


2
参见:https://dev59.com/QXE95IYBdhLWcg3wWMhQ#20749411 - dreftymac
1
我建议读者查看下面的@EyalLevin答案,因为它设置了脚本命令行调用的路径,并完全避免了触及shell环境设置。您也不必将任何路径依赖项嵌入到提交的代码中。 - KayCee
12个回答

159

这是我使用的方法:

import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), "lib"))

9
我会执行sys.path.insert(0, ..),这样它就不会被其他路径覆盖。 - John Jiang
1
有没有办法让它自动运行? - Denis de Bernardy
2
这有点冒险。如果__file__是相对于当前工作目录的相对文件名(例如,setup.py),那么os.path.dirname(__file__)将是空字符串。针对此问题和John Jiang提出的类似问题ekhumoro更通用的解决方案是强烈推荐的。 - Cecil Curry

45

我正在使用:

import sys,os
sys.path.append(os.getcwd())

10
我经常只是执行 sys.path.append('.') - kimbo
5
如果脚本从不同的目录运行,会怎样呢?例如通过提供完整的系统路径从根目录运行。那么os.getcwd()将返回“/”。 - obayhan
18
永远不要这样做。当前工作目录(CWD)不能保证是你想要的 - 特别是在你明显应该预见到的意外情况下。像任何有点理智的开发人员一样,只需引用__file__即可。 - Cecil Curry

35

如果您不想编辑每个文件

  • 像普通Python库一样安装您的库
    或者
  • PYTHONPATH设置为您的lib

或者,如果您愿意在每个文件顶部添加一行代码,可以添加一个导入语句,例如:

import import_my_lib

import_my_lib.py放在bin目录中,import_my_lib可以正确地将Python路径设置为任何您想要的lib


1
设置 PYTHONPATH 对于测试来说更加方便。 - Zhang LongQI

23

创建一个包装器模块project/bin/lib,其中包含以下内容:

import sys, os

sys.path.insert(0, os.path.join(
    os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib'))

import mylib

del sys.path[0], sys, os

然后你可以用以下代码替换脚本顶部的所有冗杂内容:

#!/usr/bin/python
from lib import mylib

21

使用Python 3.4+

import sys
from pathlib import Path

# As PosixPath
sys.path.append(Path(__file__).parent / "lib")

# Or as str as explained in https://dev59.com/qlwY5IYBdhLWcg3wWmxn#32701221
sys.path.append(str(Path(__file__).parent / "lib"))

1
Python2 兼容: import pathlib import os sys.path.append(os.path.dirname(file)) - michael

17

如果您不想以任何方式更改脚本内容,请在$PYTHONPATH之前添加当前工作目录.(见下面的示例)

PYTHONPATH=.:$PYTHONPATH alembic revision --autogenerate -m "First revision"

就这样吧!


https://docs.python.org/3/tutorial/modules.html#the-module-search-path 上说:“sys.path 是从这些位置初始化的:包含输入脚本的目录”。我认为这不是真的。 - user1261273
我倾向于这样做,特别是在 .envrc 中,这样使用 direnv 甚至可以自动化和隔离。 - EdvardM

16

您可以在相关的根目录下使用python -m运行脚本,并将"模块路径"作为参数传递。

例如:$ python -m module.sub_module.main # 注意末尾没有'.py'


另一个例子:

$ tree  # Given this file structure
.
├── bar
│   ├── __init__.py
│   └── mod.py
└── foo
    ├── __init__.py
    └── main.py

$ cat foo/main.py
from bar.mod import print1
print1()

$ cat bar/mod.py
def print1():
    print('In bar/mod.py')

$ python foo/main.py  # This gives an error
Traceback (most recent call last):
  File "foo/main.py", line 1, in <module>
    from bar.mod import print1
ImportError: No module named bar.mod

$ python -m foo.main  # But this succeeds
In bar/mod.py

8
使用 "m" 开关是建议的方法,而不是使用 sys 路径 hack。 - Mr_and_Mrs_D
哦,这比设置PYTHONPATH环境或sys.path答案更好。我今天学到了一些东西 - 我非常、非常感激! - KayCee

7

这是我经常使用的方法:

import os
import sys

current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "lib"))

4
每个提供的答案都存在一个问题,可以总结为“只需在脚本开头添加这个神奇的咒语即可。只用一两行代码就能实现想要的功能。”但是它们并不适用于所有情况! 例如,其中一种神奇的咒语使用了__file__。不幸的是,如果你使用cx_Freeze打包脚本或者使用IDLE,这将导致异常。
另一种神奇的咒语使用os.getcwd()。这只有在你从命令提示符中运行脚本且包含脚本的目录是当前工作目录时才有效(也就是在运行脚本之前使用cd命令切换到了该目录)。哦天啊!我希望我不必解释为什么如果你的Python脚本在PATH的某个位置,并且你只是通过键入脚本文件名来运行它,那么它将无法正常工作。
幸运的是,有一个神奇的咒语可以在我测试过的所有情况下都有效。不幸的是,这个神奇的咒语不仅仅是一两行代码。
import inspect
import os
import sys

# Add script directory to sys.path.
# This is complicated due to the fact that __file__ is not always defined.

def GetScriptDirectory():
    if hasattr(GetScriptDirectory, "dir"):
        return GetScriptDirectory.dir
    module_path = ""
    try:
        # The easy way. Just use __file__.
        # Unfortunately, __file__ is not available when cx_Freeze is used or in IDLE.
        module_path = __file__
    except NameError:
        if len(sys.argv) > 0 and len(sys.argv[0]) > 0 and os.path.isabs(sys.argv[0]):
            module_path = sys.argv[0]
        else:
            module_path = os.path.abspath(inspect.getfile(GetScriptDirectory))
            if not os.path.exists(module_path):
                # If cx_Freeze is used the value of the module_path variable at this point is in the following format.
                # {PathToExeFile}\{NameOfPythonSourceFile}. This makes it necessary to strip off the file name to get the correct
                # path.
                module_path = os.path.dirname(module_path)
    GetScriptDirectory.dir = os.path.dirname(module_path)
    return GetScriptDirectory.dir

sys.path.append(os.path.join(GetScriptDirectory(), "lib"))
print(GetScriptDirectory())
print(sys.path)

正如你所见,这并不是一项容易的任务!


1

我在你的示例中看到了一个shebang。如果你将你的bin脚本运行为./bin/foo.py而不是python ./bin/foo.py,那么使用shebang来更改$PYTHONPATH变量就有一种选择。

虽然你不能在shebang中直接更改环境变量,但是你需要一个小助手脚本。将这个python.sh放入你的bin文件夹中:

#!/usr/bin/env bash
export PYTHONPATH=$PWD/lib
exec "/usr/bin/python" "$@"

然后将您的./bin/foo.py的shebang更改为#!bin/python.sh


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