Как правильно определить текущий каталог скриптов?

261

Я хотел бы посмотреть, как лучше определить текущий каталог скриптов в python?

Я обнаружил, что из-за множества способов вызова кода на Python трудно найти хорошее решение.

Вот некоторые проблемы:

  • __file__не определен , если скрипт выполняется с exec,execfile
  • __module__ определяется только в модулях

Случаи использования:

  • ./myfile.py
  • python myfile.py
  • ./somedir/myfile.py
  • python somedir/myfile.py
  • execfile('myfile.py') (из другого скрипта, который может находиться в другом каталоге и может иметь другой текущий каталог.

Я знаю, что не существует идеального решения, но я ищу лучший подход, который решает большинство случаев.

Наиболее часто используется такой подход, os.path.dirname(os.path.abspath(__file__))но он действительно не работает, если вы выполняете скрипт из другого exec().

Предупреждение

Любое решение, которое использует текущую директорию, потерпит неудачу, это может отличаться в зависимости от того, как вызывается скрипт, или его можно изменить внутри запущенного скрипта.

Bogdan
источник
1
Можете ли вы более конкретно указать, откуда вам нужно знать, откуда этот файл? - в коде, который импортирует файл (включающий хост), или в файле, который импортируется? (самосознающий раб)
синтезаторпатель
3
См. pathlibРешение Рона Каляна, если вы используете Python 3.4 или выше: stackoverflow.com/a/48931294/1011724
Дан,
Таким образом, решение состоит не в том, чтобы использовать какой-либо текущий каталог в коде, а в том, чтобы использовать какой-либо конфигурационный файл?
Чжао Ган
Интересное открытие я только что сделал: при выполнении python myfile.pyиз оболочки это работает, но оба :!python %и :!python myfile.pyизнутри vim терпят неудачу с системой не удается найти указанный путь. Это довольно раздражает. Кто-нибудь может прокомментировать причину этого и возможные обходные пути?
inVader

Ответы:

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

это действительно лучшее, что вы собираетесь получить.

Необычно выполнять скрипт с exec/ execfile; обычно вы должны использовать инфраструктуру модуля для загрузки скриптов. Если вы должны использовать эти методы, я предлагаю установить __file__в globalsсценарии, который вы передаете, чтобы он мог прочитать это имя файла.

Нет другого способа получить имя файла в исходном коде: как вы заметили, CWD может находиться в совершенно другом месте.

bobince
источник
2
Никогда не говори никогда? В соответствии с этим: stackoverflow.com/a/18489147 ответить кроссплатформенное решение является abspath (getsourcefile (lambda: 0))? Или я что-то пропустил?
Джефф Эллен
131

Если вы действительно хотите охватить случай, через который вызывается скрипт execfile(...), вы можете использовать inspectмодуль для определения имени файла (включая путь). Насколько мне известно, это будет работать для всех перечисленных вами случаев:

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
Свен Марнах
источник
4
Я думаю, что это действительно самый надежный метод, но я подвергаю сомнению заявленную ОП в этом необходимость. Я часто вижу, как разработчики делают это, когда они используют файлы данных в местах, относящихся к исполняющему модулю, но файлы данных IMO должны быть помещены в известное место.
Райан Гинстрем
14
@Ryan LOL, было бы здорово, если бы вы могли определить «известное местоположение», которое является мультиплатформенным и также поставляется с модулем. Я готов поспорить, что единственное безопасное место - местоположение скрипта. Обратите внимание, это не означает, что скрипт должен писать в это место, но для чтения данных это безопасно.
сорин
1
Тем не менее, решение не является хорошим, просто попробуйте вызвать chdir()функцию до того, как она изменит результат. Также вызов скрипта python из другого каталога изменит результат, поэтому это не очень хорошее решение.
сорин
2
os.path.expanduser("~")это кроссплатформенный способ получить каталог пользователя. К сожалению, это не лучшая практика Windows для хранения данных приложения.
Райан Джинстром
6
@sorin: я пробовал chdir()перед запуском скрипта; это дает правильный результат. Я попытался вызвать скрипт из другого каталога, и он также работает. Результаты такие же, как и на inspect.getabsfile()основе решения .
JFS
43
#!/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-файла (/ egg) . Работает, если скрипт «импортирован» ( PYTHONPATH=/path/to/library.zip python -mscript_to_run) из zip-файла; в этом случае он возвращает путь к архиву. Это работает, если скрипт скомпилирован в отдельный исполняемый файл ( sys.frozen). Это работает для символических ссылок ( realpathустраняет символические ссылки). Работает в интерактивном переводчике; в этом случае он возвращает текущий рабочий каталог.

JFS
источник
Прекрасно работает с PyInstaller.
Габорист
1
Есть ли какая-то причина, по которой getabsfile(..)не упоминается в документации дляinspect ? Он появляется в источнике, связанном с этой страницей.
Евгений Сергеев
@ EvgeniSergeev это может быть ошибка. Это простая оболочка getsourcefile(), getfile()которая задокументирована.
JFS
24

В Python 3.4+ вы можете использовать более простой pathlibмодуль:

from inspect import currentframe, getframeinfo
from pathlib import Path

filename = getframeinfo(currentframe()).filename
parent = Path(filename).resolve().parent
Евгений Ярмаш
источник
2
Отличная простота!
Cometsong
3
Вы, вероятно, можете использовать Path(__file__)(нет необходимости в inspectмодуле).
Пек
При выполнении команды @Peque создается путь, включающий имя текущего файла, а не родительский каталог. Если я пытаюсь получить текущий каталог скриптов, чтобы указать на файл в том же каталоге, например, ожидая загрузки файла конфигурации в тот же каталог, что и скрипт, Path(__file__)выдаст, /path/to/script/currentscript.pyкогда ОП захотел получить/path/to/script/
Давос
8
О, я неправильно понял, вы хотите избежать модуля проверки и просто использовать что-то вроде parent = Path(__file__).resolve().parent Это намного приятнее.
Давос
3
@ Dut A. Вы должны использовать .joinpath()(или /оператор) для этого, а не +.
Евгений Ярмаш
13

os.path...Подход был «сделать вещь» в Python 2.

В Python 3 вы можете найти каталог скриптов следующим образом:

from pathlib import Path
cwd = Path(__file__).parents[0]
Рон Калян
источник
11
Или просто Path(__file__).parent. Но cwdэто неправильно, это не текущий рабочий каталог , а каталог файла . Они могут быть одинаковыми, но обычно это не так.
Нуно Андре
5

Просто используйте os.path.dirname(os.path.abspath(__file__))и очень внимательно изучите, существует ли реальная необходимость в том случае, когда execиспользуется. Это может быть признаком проблемного дизайна, если вы не можете использовать свой скрипт в качестве модуля.

Имейте в виду Zen of Python # 8 , и если вы считаете, что есть хороший аргумент для варианта использования, для которого он должен работать exec, тогда, пожалуйста, сообщите нам более подробную информацию об истории проблемы.

Wim
источник
2
Если вы не используете exec (), вы потеряете контекст отладчика. Также exec () должен быть значительно быстрее, чем запуск нового процесса.
сорин
@sorin Это не вопрос exec против запуска нового процесса, так что это спорный аргумент. Это вопрос exec против использования импорта или вызова функции.
Вим
4

Бы

import os
cwd = os.getcwd()

делай что хочешь? Я не уверен, что именно вы подразумеваете под "текущим каталогом скриптов". Каким будет ожидаемый результат для приведенных вами вариантов использования?

Уилл Маккатчен
источник
3
Это не поможет. Я считаю, что @bogdan ищет каталог для скрипта, который находится на вершине стека вызовов. то есть во всех его случаях она должна распечатать каталог, в котором находится «myfile.py». Тем не менее, ваш метод будет печатать только каталог файла, который вызывает exec('myfile.py'), так же, как __file__и sys.argv[0].
Zhang18
Да, это имеет смысл. Я просто хотел убедиться, что @bogdan не пропустил что-то простое, и я не мог точно сказать, чего они хотят.
Уилл Маккатчен
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. Для перехвата нагрузки «модуль» более высокого уровня вы можете использовать sys.path_hooks ( Автор любезности PyMOTW).

Это все, что у меня есть на макушке.

synthesizerpatel
источник
2

Вот частичное решение, все еще лучшее, чем все опубликованные.

import sys, os, os.path, inspect

#os.chdir("..")

if '__file__' not in locals():
    __file__ = inspect.getframeinfo(inspect.currentframe())[0]

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

Теперь это работает для всех вызовов, но если кто-то использует chdir()для изменения текущего каталога, это также не удастся.

Ноты:

  • sys.argv[0]не будет работать, вернется, -cесли вы выполните скрипт сpython -c "execfile('path-tester.py')"
  • Я опубликовал полный тест на https://gist.github.com/1385555, и вы можете его улучшить.
Сорин
источник
1

Это должно работать в большинстве случаев:

import os,sys
dirname=os.path.dirname(os.path.realpath(sys.argv[0]))
Jahid
источник
5
Это решение использует текущий каталог, и в вопросе явно указано, что такое решение не удастся.
скайкинг
1

Надеюсь, это поможет: - Если вы запустите скрипт / модуль из любого места, вы сможете получить доступ к __file__переменной, которая является переменной модуля, представляющей местоположение скрипта.

С другой стороны, если вы используете интерпретатор, у вас нет доступа к этой переменной, где вы получите имя NameErrorи os.getcwd()неверный каталог, если вы запускаете файл откуда-то еще.

Это решение должно дать вам то, что вы ищете во всех случаях:

from inspect import getsourcefile
from os.path import abspath
abspath(getsourcefile(lambda:0))

Я не проверил это полностью, но это решило мою проблему.

отметка
источник
Это даст файл, а не каталог
Shital Shah