Dostęp do danych w podkatalogu pakietu [duplikat]

132

Piszę pakiet Pythona z modułami, które muszą otwierać pliki danych w ./data/podkatalogu. W tej chwili mam ścieżki do plików zakodowane na stałe w moich klasach i funkcjach. Chciałbym napisać solidniejszy kod, który będzie miał dostęp do podkatalogu niezależnie od tego, gdzie jest zainstalowany w systemie użytkownika.

Próbowałem różnych metod, ale jak dotąd nie miałem szczęścia. Wygląda na to, że większość poleceń „bieżącego katalogu” zwraca katalog systemowego interpretera języka Python, a nie katalog modułu.

Wydaje się, że powinien to być banalny, powszechny problem. Jednak nie mogę tego rozgryźć. Częścią problemu jest to, że moje pliki danych nie są .pyplikami, więc nie mogę używać funkcji importu i tym podobnych.

Jakieś sugestie?

W tej chwili mój katalog z pakietami wygląda następująco:

/
__init__.py
module1.py
module2.py
data/   
   data.txt

Próbuję uzyskać dostęp data.txtz module*.py!

Jacob Lyles
źródło

Odpowiedzi:

25

Możesz użyć, __file__aby uzyskać ścieżkę do pakietu, na przykład:

import os
this_dir, this_filename = os.path.split(__file__)
DATA_PATH = os.path.join(this_dir, "data", "data.txt")
print open(DATA_PATH).read()
RichieHindle
źródło
46
To nie zadziała, jeśli pliki znajdują się w dystrybucji (tj. Egg). Użyj pkg_resources, aby dostać się do pliku danych.
Chris,
3
Rzeczywiście, to jest zepsute.
Federico
1
Ponadto, __file__nie działa z py2exe, gdy wartość będzie ścieżka do pliku zip.
Pod
1
To faktycznie zadziałało dla mnie. Nie miałem żadnych problemów. Używam Pythona 3.6
Jorge
2
To nie zadziała w przypadku dystrybucji (jaja itp.).
Adarsh ​​Trivedi
174

Standardowym sposobem na to jest użycie pakietów setuptools i pkg_resources.

Możesz ułożyć pakiet zgodnie z następującą hierarchią i skonfigurować plik instalacyjny pakietu tak, aby wskazywał mu zasoby danych, zgodnie z tym łączem:

http://docs.python.org/distutils/setupscript.html#installing-package-data

Następnie możesz ponownie znaleźć i użyć tych plików za pomocą pkg_resources, zgodnie z tym linkiem:

http://peak.telecommunity.com/DevCenter/PkgResources#basic-resource-access

import pkg_resources

DATA_PATH = pkg_resources.resource_filename('<package name>', 'data/')
DB_FILE = pkg_resources.resource_filename('<package name>', 'data/sqlite.db')
elliot42
źródło
7
Czy pkg_resources nie utworzy zależności czasu wykonywania od setuptools ? Na przykład, rozpowszechniam pakiet Debiana, więc dlaczego miałbym polegać python-setuptoolstylko na tym? Jak dotąd __file__działa dobrze dla mnie.
mlt
4
Dlaczego to jest lepsze: Klasa ResourceManager zapewnia jednolity dostęp do zasobów pakietu, niezależnie od tego, czy te zasoby istnieją jako pliki i katalogi, czy są skompresowane w jakimś archiwum
vrdhn
4
Świetna sugestia, dzięki. Zaimplementowałem standardowy plik otwarty za pomocąfrom pkg_resources import resource_filename open(resource_filename('data', 'data.txt'), 'rb')
eageranalyst
5
Jak to zadziała w przypadku korzystania z pakietu, gdy nie jest on zainstalowany? Mam na myśli tylko lokalne testy
Claudiu
11
W pythonie 3.7 importlib.resourceszastępuje pkg_resourcesw tym celu (z powodu problemów z wydajnością).
benjimin
14

Aby zapewnić rozwiązanie działające dzisiaj. Zdecydowanie użyj tego interfejsu API, aby nie wymyślać na nowo wszystkich tych kół.

Wymagana jest prawdziwa nazwa pliku systemu plików. Jajka spakowane zostaną wyodrębnione do katalogu pamięci podręcznej:

from pkg_resources import resource_filename, Requirement

path_to_vik_logo = resource_filename(Requirement.parse("enb.portals"), "enb/portals/reports/VIK_logo.png")

Zwraca czytelny obiekt podobny do pliku dla określonego zasobu; może to być rzeczywisty plik, StringIO lub podobny obiekt. Strumień jest w „trybie binarnym”, w tym sensie, że jakiekolwiek bajty w zasobie zostaną odczytane tak, jak są.

from pkg_resources import resource_stream, Requirement

vik_logo_as_stream = resource_stream(Requirement.parse("enb.portals"), "enb/portals/reports/VIK_logo.png")

Wykrywanie pakietów i dostęp do zasobów przy użyciu pkg_resources

Sascha Gottfried
źródło
10

Często nie ma sensu udzielanie odpowiedzi, która szczegółowo opisuje kod, który nie działa tak, jak jest, ale uważam, że jest to wyjątek. Dodano Python 3.7, importlib.resourcesktóry ma zastąpić pkg_resources. To działałoby w przypadku dostępu do plików w pakietach, które nie mają ukośników w nazwie, tj

foo/
    __init__.py
    module1.py
    module2.py
    data/   
       data.txt
    data2.txt

tj. możesz uzyskać dostęp do data2.txtwnętrza pakietu foona przykład

importlib.resources.open_binary('foo', 'data2.txt')

ale zakończy się niepowodzeniem z wyjątkiem

>>> importlib.resources.open_binary('foo', 'data/data.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.7/importlib/resources.py", line 87, in open_binary
    resource = _normalize_path(resource)
  File "/usr/lib/python3.7/importlib/resources.py", line 61, in _normalize_path
    raise ValueError('{!r} must be only a file name'.format(path))
ValueError: 'data/data2.txt' must be only a file name

Nie mogą być mocowane jedynie przez umieszczenie __init__.pyw data, a następnie wykorzystanie go jako pakiet:

importlib.resources.open_binary('foo.data', 'data.txt')

Przyczyną takiego zachowania jest „to jest zgodne z projektem” ; ale projekt może się zmienić ...

Antti Haapala
źródło
Czy masz lepszy link „to jest zgodne z projektem” niż film z YouTube - najlepiej taki z tekstem?
gerrit
@gerrit druga zawiera tekst. "This was a deliberate choice, but I think you have a valid use case. @brettcannon what do you think? And if we allow this, should we make sure it gets into Python 3.7?"
Antti Haapala
1
Projekt został teraz zmieniony na przechodnie API (dostępne w standardowym Pythonie 3.9+). Więcej szczegółów w duplikacie tutaj -> stackoverflow.com/a/58941536/674039
wim
8

Potrzebujesz nazwy dla całego modułu, otrzymujesz drzewo katalogów, które nie zawiera tych szczegółów, dla mnie to zadziałało:

import pkg_resources
print(    
    pkg_resources.resource_filename(__name__, 'data/data.txt')
)

Warto zauważyć, że setuptools nie wydaje się rozwiązywać plików na podstawie dopasowania nazwy do spakowanych plików danych, więc nie musisz dodawać data/przedrostka prawie bez względu na wszystko. Możesz użyć, os.path.join('data', 'data.txt)jeśli potrzebujesz alternatywnych separatorów katalogów, jednak generalnie nie znajduję problemów ze zgodnością z zakodowanymi na stałe separatorami katalogów w stylu unixowym.

ThorSummoner
źródło
docs.python.org/3.6/distutils/… > Zauważ, że wszelkie nazwy ścieżek (plików lub katalogów) podane w skrypcie instalacyjnym powinny być zapisane przy użyciu konwencji Uniksa, tj. oddzielone ukośnikiem. Distutils zajmie się konwersją tej neutralnej dla platformy reprezentacji na wszystko, co jest odpowiednie na twojej obecnej platformie, zanim faktycznie użyje nazwy ścieżki. To sprawia, że ​​skrypt instalacyjny jest przenośny w różnych systemach operacyjnych, co oczywiście jest jednym z głównych celów Distutils. W tym duchu wszystkie ścieżki w tym dokumencie są oddzielone ukośnikiem.
changyuheng
5

Myślę, że znalazłem odpowiedź.

Tworzę moduł data_path.py, który importuję do innych moich modułów, zawierający:

data_path = os.path.join(os.path.dirname(__file__),'data')

A potem otwieram wszystkie moje pliki za pomocą

open(os.path.join(data_path,'filename'), <param>)
Jacob Lyles
źródło
2
To nie zadziała, gdy zasób znajduje się w dystrybucji archiwalnej (takiej jak spakowane jajko). Wolę coś takiego:pkg_resources.resource_string('pkg_name', 'data/file.txt')
ankostis
@ankostis setuptools jest wystarczająco sprytny, aby rozpakować archiwum, jeśli wykryje, że __file__gdzieś użyłeś . W moim przypadku używam biblioteki, która naprawdę chce ścieżek, a nie strumieni. Oczywiście mogłem tymczasowo zapisać pliki na dysku, ale będąc leniwym, po prostu korzystam z funkcji setuptools.
letmaik