Jak poprawnie ustalić bieżący katalog skryptów?

260

Chciałbym zobaczyć, jaki jest najlepszy sposób określenia bieżącego katalogu skryptów w Pythonie?

Odkryłem, że ze względu na wiele sposobów wywoływania kodu python trudno jest znaleźć dobre rozwiązanie.

Oto kilka problemów:

  • __file__nie jest zdefiniowany, jeśli skrypt jest wykonywany za pomocą exec,execfile
  • __module__ jest zdefiniowany tylko w modułach

Przypadków użycia:

  • ./myfile.py
  • python myfile.py
  • ./somedir/myfile.py
  • python somedir/myfile.py
  • execfile('myfile.py') (z innego skryptu, który może znajdować się w innym katalogu i może mieć inny katalog bieżący.

Wiem, że nie ma idealnego rozwiązania, ale szukam najlepszego rozwiązania, które rozwiązałoby większość przypadków.

Najczęściej stosowanym podejściem jest, os.path.dirname(os.path.abspath(__file__))ale to naprawdę nie działa, jeśli wykonasz skrypt z innego za pomocą exec().

Ostrzeżenie

Każde rozwiązanie korzystające z bieżącego katalogu zawiedzie, może być inaczej w zależności od sposobu wywołania skryptu lub można je zmienić w działającym skrypcie.

bogdan
źródło
1
Czy możesz być bardziej szczegółowy, skąd musisz wiedzieć, skąd pochodzi plik? - w kodzie importującym plik (host uwzględniający) lub w importowanym pliku? (samoświadomy niewolnik)
synthesizerpatel
3
Zobacz Ron Kalian za pathlibrozwiązanie jeśli używasz Pythona 3.4 lub nowszy: stackoverflow.com/a/48931294/1011724
Dan
Więc rozwiązaniem NIE jest użycie żadnego bieżącego katalogu w kodzie, ale użycie pliku konfiguracyjnego?
ZhaoGang,
Ciekawe odkrycie, które właśnie wykonałem: działając python myfile.pyz powłoki, działa, ale zarówno :!python %i :!python myfile.pywewnątrz vima zawodzą z System nie może znaleźć określonej ścieżki. To dość denerwujące. Czy ktoś może skomentować przyczynę tego i potencjalne obejścia?
inVader,

Odpowiedzi:

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

jest naprawdę najlepszy, co możesz dostać.

Wykonywanie skryptu przy pomocy exec/ execfile; normalnie powinieneś używać infrastruktury modułów do ładowania skryptów. Jeśli musisz użyć tych metod, sugeruję ustawienie __file__wglobals skrypcie, aby mógł odczytać tę nazwę pliku.

Nie ma innego sposobu na uzyskanie nazwy pliku w wykonywanym kodzie: jak zauważyłeś, CWD może znajdować się w zupełnie innym miejscu.

Bobin
źródło
2
Nigdy nie mów nigdy? Zgodnie z tym: stackoverflow.com/a/18489147 odpowiedzią na wiele platform jest abspath (getsourcefile (lambda: 0))? A może brakuje mi czegoś jeszcze?
Jeff Ellen,
131

Jeśli naprawdę chcesz uwzględnić przypadek wywołania skryptu execfile(...), możesz użyć inspectmodułu, aby wydedukować nazwę pliku (w tym ścieżkę). O ile mi wiadomo, będzie to działać we wszystkich przypadkach, które wymieniłeś:

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
Sven Marnach
źródło
4
Myślę, że jest to rzeczywiście najbardziej niezawodna metoda, ale wątpię w stwierdzoną potrzebę PO. Często widzę, że programiści robią to, gdy używają plików danych w lokalizacjach względem modułu wykonawczego, ale pliki danych IMO powinny być umieszczone w znanej lokalizacji.
Ryan Ginstrom
14
@ Ryan LOL, jeśli byłoby świetnie, gdybyś był w stanie zdefiniować „znaną lokalizację”, która jest wieloplatformowa i zawiera również moduł. Jestem gotów się założyć, że jedyną bezpieczną lokalizacją jest lokalizacja skryptu. Uwaga: nie oznacza to, że skrypt powinien zapisać w tej lokalizacji, ale do odczytu danych jest bezpieczny.
sorin
1
Jednak rozwiązanie nie jest dobre, po prostu spróbuj zadzwonić chdir()przed funkcją, zmieni to wynik. Również wywołanie skryptu python z innego katalogu zmieni wynik, więc nie jest to dobre rozwiązanie.
sorin
2
os.path.expanduser("~")to wieloplatformowy sposób na uzyskanie katalogu użytkownika. Niestety, nie jest to najlepsza praktyka systemu Windows dotycząca miejsca, w którym należy przykleić dane aplikacji.
Ryan Ginstrom,
6
@sorin: Próbowałem chdir()przed uruchomieniem skryptu; daje poprawny wynik. Próbowałem wywołać skrypt z innego katalogu i to również działa. Wyniki są takie same, jak inspect.getabsfile()rozwiązania oparte na .
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())

Działa na CPython, Jython, Pypy. Działa, jeśli skrypt jest wykonywany przy użyciu execfile()( sys.argv[0]i __file__rozwiązania oparte na rozwiązaniu nie powiedzie się tutaj). Działa, jeśli skrypt znajduje się w wykonywalnym pliku zip (/ jajko) . Działa, jeśli skrypt jest „importowany” ( PYTHONPATH=/path/to/library.zip python -mscript_to_run) z pliku zip; w tym przypadku zwraca ścieżkę archiwum. Działa, jeśli skrypt jest skompilowany w samodzielny plik wykonywalny ( sys.frozen). Działa dla dowiązań symbolicznych ( realpatheliminuje dowiązania symboliczne). Działa w interaktywnym tłumaczu; w tym przypadku zwraca bieżący katalog roboczy.

jfs
źródło
Działa idealnie z PyInstaller.
gaboryczny
1
Czy jest jakiś powód, dla którego getabsfile(..)nie wymieniono go w dokumentacjiinspect ? Pojawia się w źródle połączonym z tą stroną.
Evgeni Sergeev
@EvgeniSergeev może to być błąd. Jest to proste opakowanie getsourcefile(), getfile()które jest udokumentowane.
jfs
24

W Pythonie 3.4+ możesz użyć prostszego pathlibmodułu:

from inspect import currentframe, getframeinfo
from pathlib import Path

filename = getframeinfo(currentframe()).filename
parent = Path(filename).resolve().parent
Eugene Yarmash
źródło
2
Doskonała prostota!
Cometsong
3
Prawdopodobnie możesz użyć Path(__file__)(nie potrzebujesz inspectmodułu).
Peque
@ Wykonanie tej czynności tworzy ścieżkę zawierającą nazwę bieżącego pliku, a nie katalogu nadrzędnego. Jeśli próbuję pobrać bieżący katalog skryptów, aby wskazać plik w tym samym katalogu, np. Spodziewam się załadować plik konfiguracyjny do tego samego katalogu co skrypt, Path(__file__)podaje, /path/to/script/currentscript.pykiedy OP chciał uzyskać/path/to/script/
Davos
8
Och, źle zrozumiałem, masz na myśli unikanie modułu inspekcji i po prostu użycie czegoś takiego. parent = Path(__file__).resolve().parent To o wiele ładniejsze.
Davos,
3
@Dut A. W tym celu należy użyć .joinpath()(lub /operatora) +.
Eugene Yarmash
13

os.path...Podejście było „coś zrobić” w Pythonie 2.

W Pythonie 3 możesz znaleźć katalog skryptów w następujący sposób:

from pathlib import Path
cwd = Path(__file__).parents[0]
Ron Kalian
źródło
11
Lub po prostu Path(__file__).parent. Ale cwdjest mylące, to nie jest bieżący katalog roboczy , ale katalog pliku . Mogą być takie same, ale zwykle tak nie jest.
Nuno André
5

Po prostu zastosuj os.path.dirname(os.path.abspath(__file__))i sprawdź bardzo dokładnie, czy istnieje rzeczywista potrzeba w przypadku execużycia. Może to oznaczać problem z projektowaniem, jeśli nie możesz użyć skryptu jako modułu.

Pamiętaj o Zen w Pythonie # 8 , a jeśli uważasz, że istnieje dobry argument za przypadkiem użycia, w którym musi on działać exec, to daj nam znać więcej szczegółów na temat tła problemu.

wim
źródło
2
Jeśli nie korzystasz z exec (), stracisz kontekst debugowania. Również exec () ma być znacznie szybsze niż rozpoczęcie nowego procesu.
sorin
@sorin To nie jest kwestia exec kontra rozpoczęcie nowego procesu, więc to jest argument typu strawman. Jest to kwestia wykonania kontra użycia wywołania importu lub funkcji.
wim
4

By

import os
cwd = os.getcwd()

rób co chcesz? Nie jestem pewien, co dokładnie masz na myśli przez „bieżący katalog skryptów”. Jaka byłaby oczekiwana wydajność dla podanych przypadków użycia?

Will McCutchen
źródło
3
To by nie pomogło. Uważam, że @bogdan szuka katalogu dla skryptu, który znajduje się na górze stosu wywołań. tzn. we wszystkich jego przypadkach powinien wydrukować katalog, w którym znajduje się „myfile.py”. Jednak Twoja metoda wypisuje tylko katalog pliku, który wywołuje exec('myfile.py'), tak samo jak __file__i sys.argv[0].
Zhang18
Tak, to ma sens. Chciałem tylko upewnić się, że @bogdan nie przeoczył czegoś prostego i nie mogłem powiedzieć dokładnie, czego chcieli.
Will McCutchen
3

Po pierwsze ... kilka brakujących przypadków użycia, jeśli mówimy o sposobach wstrzykiwania anonimowego kodu ..

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

Ale prawdziwe pytanie brzmi: jaki jest twój cel - czy próbujesz egzekwować jakieś zabezpieczenia? A może interesuje Cię tylko ładowanie.

Jeśli interesuje Cię bezpieczeństwo , nazwa pliku importowanego przez plik wykonywalny / wykonywalny jest nieistotna - powinieneś użyć rexec , który oferuje:

Ten moduł zawiera klasę RExec, która obsługuje metody r_eval (), r_execfile (), r_exec () i r_import (), które są ograniczonymi wersjami standardowych funkcji eval (), execfile () oraz instrukcji exec i import. Kod wykonywany w tym ograniczonym środowisku będzie miał dostęp tylko do modułów i funkcji uznanych za bezpieczne; możesz podklasować RExec, dodając lub usuwając możliwości według potrzeb.

Jeśli jednak jest to raczej akademickie zajęcie ... oto kilka głupich podejść, do których możesz być może zagłębić się trochę głębiej ...

Przykładowe skrypty:

./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)

Wynik

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

Oczywiście jest to sposób wymagający dużych nakładów, śledzenie całego kodu. Niezbyt wydajne. Ale myślę, że jest to nowatorskie podejście, ponieważ działa nadal, nawet gdy zagłębiasz się w gnieździe. Nie można zastąpić „eval”. Chociaż może przesłonić execfile ().

Zauważ, że to podejście obejmuje tylko exec / execfile, a nie „import”. Do przechwytywania obciążenia „modułu” na wyższym poziomie możesz użyć sys.path_hooks (Zapis dzięki uprzejmości PyMOTW).

To wszystko, co mam z głowy.

synthesizerpatel
źródło
2

Oto częściowe rozwiązanie, wciąż lepsze niż wszystkie dotychczas opublikowane.

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__))

Teraz to zadziała wszystkie połączenia, ale jeśli ktoś użyje chdir()do zmiany bieżącego katalogu, to również się nie powiedzie.

Uwagi:

  • sys.argv[0]nie będzie działać, powróci, -cjeśli wykonasz skrypt za pomocąpython -c "execfile('path-tester.py')"
  • Pełny test opublikowałem na https://gist.github.com/1385555 i możesz go ulepszyć.
sorin
źródło
1

Powinno to działać w większości przypadków:

import os,sys
dirname=os.path.dirname(os.path.realpath(sys.argv[0]))
Jahid
źródło
5
To rozwiązanie korzysta z bieżącego katalogu i jest wyraźnie stwierdzone w pytaniu, że takie rozwiązanie zawiedzie.
skyking
1

Mam nadzieję, że to pomoże: - Jeśli uruchomisz skrypt / moduł z dowolnego miejsca, będziesz mieć dostęp do __file__zmiennej, która jest zmienną modułową reprezentującą lokalizację skryptu.

Z drugiej strony, jeśli używasz interpretera, nie masz dostępu do tej zmiennej, gdzie uzyskasz nazwę NameErrori os.getcwd()poda ci niepoprawny katalog, jeśli uruchomisz plik z innego miejsca.

To rozwiązanie powinno dać ci to, czego szukasz we wszystkich przypadkach:

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

Nie przetestowałem go dokładnie, ale rozwiązało to mój problem.

znak
źródło
To da plik, a nie katalog
Shital Shah