Comment déterminer correctement le répertoire de script actuel?

261

Je voudrais voir quelle est la meilleure façon de déterminer le répertoire de script actuel en python?

J'ai découvert qu'en raison des nombreuses façons d'appeler du code python, il est difficile de trouver une bonne solution.

Voici quelques problèmes:

  • __file__n'est pas défini si le script est exécuté avec exec,execfile
  • __module__ est défini uniquement dans les modules

Cas d'utilisation:

  • ./myfile.py
  • python myfile.py
  • ./somedir/myfile.py
  • python somedir/myfile.py
  • execfile('myfile.py') (à partir d'un autre script, qui peut se trouver dans un autre répertoire et qui peut avoir un autre répertoire en cours.

Je sais qu'il n'y a pas de solution parfaite, mais je recherche la meilleure approche qui résout la plupart des cas.

L'approche la plus utilisée est os.path.dirname(os.path.abspath(__file__))mais cela ne fonctionne vraiment pas si vous exécutez le script à partir d'un autre avec exec().

avertissement

Toute solution qui utilise le répertoire actuel échouera, cela peut être différent selon la façon dont le script est appelé ou il peut être modifié à l'intérieur du script en cours d'exécution.

bogdan
la source
1
Pouvez-vous être plus précis quant à la provenance du fichier? - dans le code qui importe le fichier (hôte sensible à l'inclusion) ou dans le fichier qui est importé? (esclave conscient de soi)
synthesizerpatel
3
Voir la pathlibsolution de Ron Kalian si vous utilisez python 3.4 ou supérieur: stackoverflow.com/a/48931294/1011724
Dan
La solution n'est donc PAS d'utiliser un répertoire courant dans le code, mais d'utiliser un fichier de configuration?
ZhaoGang
Découverte intéressante, je viens de faire: lorsque vous faites à python myfile.pypartir d'un shell, cela fonctionne, mais les deux :!python %et :!python myfile.pyà l'intérieur de vim échouent avec Le système ne peut pas trouver le chemin spécifié. C'est assez ennuyeux. Quelqu'un peut-il commenter la raison derrière cela et les solutions de contournement potentielles?
inVader

Réponses:

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

est en effet le meilleur que vous allez obtenir.

Il est inhabituel d'exécuter un script avec exec/ execfile; normalement, vous devez utiliser l'infrastructure du module pour charger des scripts. Si vous devez utiliser ces méthodes, je vous suggère de définir __file__le paramètre que globalsvous passez au script afin qu'il puisse lire ce nom de fichier.

Il n'y a pas d'autre moyen d'obtenir le nom de fichier dans le code exécuté: comme vous le constatez, le CWD peut être à un endroit complètement différent.

bobince
la source
2
Ne jamais dire jamais? Selon ceci: stackoverflow.com/a/18489147 répondre à une solution multiplateforme est abspath (getsourcefile (lambda: 0))? Ou y a-t-il autre chose qui me manque?
Jeff Ellen
131

Si vous voulez vraiment couvrir le cas où un script est appelé via execfile(...), vous pouvez utiliser le inspectmodule pour déduire le nom de fichier (y compris le chemin). Pour autant que je sache, cela fonctionnera pour tous les cas que vous avez énumérés:

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
Sven Marnach
la source
4
Je pense que c'est en effet la méthode la plus robuste, mais je remets en question le besoin déclaré du PO pour cela. Je vois souvent les développeurs faire cela lorsqu'ils utilisent des fichiers de données dans des emplacements relatifs au module d'exécution, mais les fichiers de données IMO doivent être placés dans un emplacement connu.
Ryan Ginstrom
14
@Ryan LOL, ce serait génial si vous pouviez définir un "emplacement connu" qui est multiplateforme et qui est également fourni avec le module. Je suis prêt à parier que le seul emplacement sûr est l'emplacement du script. Remarque, cela ne signifie pas que le script doit écrire à cet emplacement, mais pour lire les données, il est sûr.
sorin
1
Pourtant, la solution n'est pas bonne, essayez d'appeler chdir()avant la fonction, cela changera le résultat. L'appel du script python à partir d'un autre répertoire modifiera également le résultat, ce n'est donc pas une bonne solution.
sorin
2
os.path.expanduser("~")est un moyen multiplateforme pour obtenir le répertoire de l'utilisateur. Malheureusement, ce n'est pas la meilleure pratique de Windows pour savoir où coller les données d'application.
Ryan Ginstrom
6
@sorin: J'ai essayé chdir()avant d'exécuter le script; il produit un résultat correct. J'ai essayé d'appeler le script à partir d'un autre répertoire et cela fonctionne également. Les résultats sont les mêmes que pour une inspect.getabsfile()solution basée sur .
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())

Il fonctionne sur CPython, Jython, Pypy. Cela fonctionne si le script est exécuté en utilisant execfile()( sys.argv[0]et les __file__solutions basées ici échoueraient ici). Cela fonctionne si le script se trouve dans un fichier zip exécutable (/ un œuf) . Cela fonctionne si le script est "importé" ( PYTHONPATH=/path/to/library.zip python -mscript_to_run) à partir d'un fichier zip; il renvoie le chemin de l'archive dans ce cas. Cela fonctionne si le script est compilé dans un exécutable autonome ( sys.frozen). Cela fonctionne pour les liens symboliques ( realpathélimine les liens symboliques). Il fonctionne dans un interprète interactif; il renvoie le répertoire de travail actuel dans ce cas.

jfs
la source
Fonctionne parfaitement bien avec PyInstaller.
gaborous
1
Y a-t-il une raison pour laquelle cela getabsfile(..)n'est pas mentionné dans la documentationinspect ? Il apparaît dans la source liée à cette page.
Evgeni Sergeev
@EvgeniSergeev ce pourrait être un bogue. Il est un simple wrapper autour getsourcefile(), getfile()qui sont documentées.
jfs
24

Dans Python 3.4+, vous pouvez utiliser le pathlibmodule plus simple :

from inspect import currentframe, getframeinfo
from pathlib import Path

filename = getframeinfo(currentframe()).filename
parent = Path(filename).resolve().parent
Eugene Yarmash
la source
2
Excellente simplicité!
Cometsong
3
Vous pouvez probablement utiliser Path(__file__)(pas besoin du inspectmodule).
Peque
@Peque faisant cela produit un chemin incluant le nom du fichier actuel, pas le répertoire parent. Si j'essaie d'obtenir le répertoire de script actuel afin de pointer vers un fichier dans le même répertoire, par exemple en espérant charger un fichier de configuration dans le même répertoire que le script, Path(__file__)donne /path/to/script/currentscript.pyquand l'OP voulait obtenir/path/to/script/
Davos
8
Oh, j'ai mal compris, vous voulez éviter le module d'inspection et utiliser simplement quelque chose comme parent = Path(__file__).resolve().parent C'est beaucoup plus agréable.
Davos
3
@Dut A. Vous devez utiliser .joinpath()(ou l' /opérateur) pour cela, non +.
Eugene Yarmash
13

L' os.path...approche était la «chose faite» dans Python 2.

En Python 3, vous pouvez trouver le répertoire du script comme suit:

from pathlib import Path
cwd = Path(__file__).parents[0]
Ron Kalian
la source
11
Ou tout simplement Path(__file__).parent. Mais cwdest un terme impropre, ce n'est pas le répertoire de travail actuel , mais le répertoire du fichier . Ils pourraient être les mêmes, mais ce n'est généralement pas le cas.
Nuno André
5

Il suffit d'utiliser os.path.dirname(os.path.abspath(__file__))et d'examiner très attentivement s'il existe un réel besoin pour le cas où il execest utilisé. Cela pourrait être un signe de conception problématique si vous n'êtes pas en mesure d'utiliser votre script comme module.

Gardez à l'esprit Zen of Python # 8 , et si vous pensez qu'il y a un bon argument pour un cas d'utilisation où cela doit fonctionner exec, alors faites-nous savoir plus de détails sur l'arrière-plan du problème.

wim
la source
2
Si vous ne exécutez pas avec exec (), vous perdrez le contexte du débogueur. En outre, exec () est censé être considérablement plus rapide que le démarrage d'un nouveau processus.
sorin
@sorin Ce n'est pas une question d'exec vs de commencer un nouveau processus, c'est donc un argument paille. Il s'agit d'exec vs d'utiliser un import ou un appel de fonction.
wim
4

Aurait

import os
cwd = os.getcwd()

Faites ce que vous voulez? Je ne sais pas exactement ce que vous entendez par le "répertoire de script actuel". Quelle serait la sortie attendue pour les cas d'utilisation que vous avez donnés?

Will McCutchen
la source
3
Ça n'aiderait pas. Je pense que @bogdan recherche le répertoire du script qui se trouve en haut de la pile d'appels. c'est-à-dire dans tous ses cas, il devrait afficher le répertoire où se trouve «myfile.py». Pourtant, votre méthode afficherait uniquement le répertoire du fichier qui appelle exec('myfile.py'), de la même manière que __file__et sys.argv[0].
Zhang18
Ouais, ça a du sens. Je voulais juste m'assurer que @bogdan n'oubliait pas quelque chose de simple, et je ne pouvais pas dire exactement ce qu'ils voulaient.
Will McCutchen
3

Tout d'abord .. quelques cas d'utilisation manquants ici si nous parlons de moyens d'injecter du code anonyme ..

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

Mais, la vraie question est, quel est votre objectif - essayez-vous d'appliquer une sorte de sécurité? Ou êtes-vous simplement intéressé par ce qui est chargé.

Si vous êtes intéressé par la sécurité , le nom de fichier qui est importé via exec / execfile est sans conséquence - vous devriez utiliser rexec , qui offre ce qui suit:

Ce module contient la classe RExec, qui prend en charge les méthodes r_eval (), r_execfile (), r_exec () et r_import (), qui sont des versions restreintes des fonctions Python standard eval (), execfile () et les instructions exec et import. Le code exécuté dans cet environnement restreint n'aura accès qu'aux modules et fonctions jugés sûrs; vous pouvez sous-classer RExec pour ajouter ou supprimer des capacités selon vos besoins.

Cependant, si c'est plus une poursuite académique .. voici quelques approches loufoques que vous pourriez peut-être approfondir un peu ..

Exemples de scripts:

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

Production

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

Bien sûr, c'est une façon gourmande en ressources de le faire, vous traceriez tout votre code. Pas très efficace. Mais je pense que c'est une nouvelle approche car elle continue de fonctionner même si vous vous enfoncez plus profondément dans le nid. Vous ne pouvez pas remplacer 'eval'. Bien que vous puissiez remplacer execfile ().

Notez que cette approche ne couvre que exec / execfile, pas 'import'. Pour un accrochage de charge de «module» de niveau supérieur, vous pourriez être en mesure d'utiliser use sys.path_hooks (avec l'aimable autorisation de PyMOTW).

C'est tout ce que j'ai du haut de ma tête.

synthétiseurpatel
la source
2

Voici une solution partielle, encore meilleure que toutes celles publiées jusqu'à présent.

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

Maintenant, cela fonctionne tous les appels, mais si quelqu'un utilise chdir()pour changer le répertoire actuel, cela échouera également.

Remarques:

  • sys.argv[0]ne va pas fonctionner, reviendra -csi vous exécutez le script avecpython -c "execfile('path-tester.py')"
  • J'ai publié un test complet sur https://gist.github.com/1385555 et vous êtes invités à l'améliorer.
Sorin
la source
1

Cela devrait fonctionner dans la plupart des cas:

import os,sys
dirname=os.path.dirname(os.path.realpath(sys.argv[0]))
Jahid
la source
5
Cette solution utilise le répertoire actuel et il est explicitement indiqué dans la question qu'une telle solution échouera.
skyking
1

J'espère que cela aide: - Si vous exécutez un script / module depuis n'importe où, vous pourrez accéder à la __file__variable qui est une variable de module représentant l'emplacement du script.

D'un autre côté, si vous utilisez l'interpréteur, vous n'avez pas accès à cette variable, où vous obtiendrez un nom NameError et os.getcwd()vous donnera le répertoire incorrect si vous exécutez le fichier ailleurs.

Cette solution devrait vous donner ce que vous recherchez dans tous les cas:

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

Je ne l'ai pas testé à fond, mais cela a résolu mon problème.

marque
la source
Cela donnera un fichier, pas un répertoire
Shital Shah