Jak odczytać (statyczny) plik z pakietu Pythona?

107

Czy możesz mi powiedzieć, jak mogę odczytać plik znajdujący się w moim pakiecie Python?

Moja sytuacja

Pakiet, który ładuję, ma wiele szablonów (plików tekstowych używanych jako ciągi znaków), które chcę załadować z poziomu programu. Ale jak określić ścieżkę do takiego pliku?

Wyobraź sobie, że chcę przeczytać plik z:

package\templates\temp_file

Jakaś manipulacja ścieżką? Śledzenie ścieżki bazowej pakietu?

ronszon
źródło

Odpowiedzi:

-13

[dodano 15.06.2016: najwyraźniej to nie działa we wszystkich sytuacjach. proszę zapoznać się z innymi odpowiedziami]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')
jcomeau_ictx
źródło
176

TLDR; Użyj importlib.resourcesmodułu biblioteki standardów, jak wyjaśniono w metodzie nr 2 poniżej.

Tradycyjny pkg_resourceszsetuptools nie jest już zalecane, ponieważ nowy sposób:

  • jest znacznie bardziej wydajny ;
  • jest bezpieczniejsze, ponieważ użycie pakietów (zamiast żądań ścieżek) powoduje błędy w czasie kompilacji;
  • jest bardziej intuicyjny, ponieważ nie musisz „łączyć” ścieżek;
  • jest szybszy podczas programowania, ponieważ nie potrzebujesz dodatkowej zależności ( setuptools), ale polegasz tylko na standardowej bibliotece Pythona.

Zachowałem tradycyjną listę jako pierwszą, aby wyjaśnić różnice w nowej metodzie podczas przenoszenia istniejącego kodu (portowanie również zostało wyjaśnione tutaj ).



Załóżmy, że Twoje szablony znajdują się w folderze zagnieżdżonym w pakiecie Twojego modułu:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.

Uwaga 1: Z pewnością NIE powinniśmy majstrować przy __file__atrybucie (np. Kod zepsuje się, gdy zostanie wyświetlony z zip).

Uwaga 2: Jeśli tworzysz ten pakiet, pamiętaj, aby zdeklarować pliki danych jako package_datalubdata_files w pliku setup.py.

1) Korzystanie pkg_resourcesz setuptools(wolno)

Możesz użyć pkg_resourcespakietu z dystrybucji setuptools , ale wiąże się to z kosztami i wydajnością :

import pkg_resources

# Could be any dot-separated package/module name or a "Requirement"
resource_package = __name__
resource_path = '/'.join(('templates', 'temp_file'))  # Do not use os.path.join()
template = pkg_resources.resource_string(resource_package, resource_path)
# or for a file-like stream:
template = pkg_resources.resource_stream(resource_package, resource_path)

Porady:

  • Spowoduje to odczytanie danych, nawet jeśli Twoja dystrybucja jest spakowana, więc możesz ustawić zip_safe=Truew swoim setup.pyi / lub użyć długo oczekiwanego programu zipapppakującego z python-3.5 do tworzenia samodzielnych dystrybucji.

  • Pamiętaj, aby dodać setuptoolsdo swoich wymagań wykonawczych (np. W install_requires`).

... i zauważ, że zgodnie z Setuptools / pkg_resourcesdocs, nie powinieneś używać os.path.join:

Podstawowy dostęp do zasobów

Zwróć uwagę, że nazwy zasobów muszą być /oddzielnymi ścieżkami i nie mogą być bezwzględne (tj. Bez początku /) ani zawierać nazw względnych, takich jak „ ..”. Czy nie używać os.pathprocedur manipulować ścieżki zasobów, ponieważ są one nie systemu plików ścieżki.

2) Python> = 3.7 lub używając wstecznej importlib_resourcesbiblioteki

Użyj importlib.resourcesmodułu biblioteki standardowej, który jest bardziej wydajny niż setuptoolspowyżej:

try:
    import importlib.resources as pkg_resources
except ImportError:
    # Try backported to PY<37 `importlib_resources`.
    import importlib_resources as pkg_resources

from . import templates  # relative-import the *package* containing the templates

template = pkg_resources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = pkg_resources.open_text(templates, 'temp_file')

Uwaga:

Odnośnie funkcji read_text(package, resource):

  • packageMoże być ciągiem znaków lub moduł.
  • resourceNIE jest już droga, ale tylko nazwa pliku zasobu otworzyć, w ramach istniejącego pakietu; może nie zawierać separatorów ścieżek i może nie zawierać zasobów podrzędnych (tj. nie może być katalogiem).

Na przykład zadany w pytaniu musimy teraz:

  • przekształcić <your_package>/templates/ w odpowiedni pakiet, tworząc w nim pusty __init__.pyplik,
  • więc teraz możemy użyć prostej (prawdopodobnie względnej) importinstrukcji ( koniec z analizowaniem nazw pakietów / modułów),
  • i po prostu poproś o resource_name = "temp_file"(bez ścieżki).

Porady:

  • Aby uzyskać dostęp do pliku wewnątrz bieżącego modułu, ustaw argument pakietu na __package__, np. pkg_resources.read_text(__package__, 'temp_file')(Dzięki @ ben-mares).
  • Rzeczy stają się interesujący, gdy rzeczywista nazwa pliku jest proszony o path(), ponieważ teraz kontekst kierownicy służą do tymczasowego utworzonych plików (czytaj to ).
  • Dodaj backported bibliotekę, warunkowo dla starszych Pythonów, za pomocą install_requires=[" importlib_resources ; python_version<'3.7'"](zaznacz to, jeśli pakujesz swój projekt z setuptools<36.2.1).
  • Pamiętaj, aby usunąć setuptoolsbibliotekę z wymagań wykonawczych , jeśli migrowałeś z tradycyjnej metody.
  • Pamiętaj, aby dostosować setup.pylub MANIFESTaby zawierać żadnych plików statycznych .
  • Możesz również ustawić zip_safe=Truew swoim setup.py.
ankostis
źródło
1
str.join przyjmuje sekwencję resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen
1
Ciągle mam NotImplementedError: Can't perform this operation for loaders without 'get_data()'jakieś pomysły?
leoschet
Należy pamiętać, że importlib.resourcesi pkg_resourcesto nie koniecznie zgodna . importlib.resourcesdziała z dodanymi plikami zip sys.path, setuptools i pkg_resourceswspółpracuje z plikami egg, które są plikami zip przechowywanymi w katalogu, do którego sam jest dodawany sys.path. Np. Z sys.path = [..., '.../foo', '.../bar.zip']jajami .../foo, ale paczki bar.zipmogą być również importowane. Nie możesz użyć pkg_resourcesdo wyodrębnienia danych z pakietów w bar.zip. Nie sprawdziłem, czy setuptools rejestruje program ładujący niezbędny importlib.resourcesdo pracy z jajami.
Martijn Pieters
Czy wymagana jest dodatkowa konfiguracja setup.py, jeśli Package has no locationpojawi się błąd ?
zygimantus
1
W przypadku, gdy chcesz uzyskać dostęp do pliku wewnątrz bieżącego modułu (a nie do modułu podrzędnego, jak templatesna przykładzie), możesz ustawić packageargument na __package__, np.pkg_resources.read_text(__package__, 'temp_file')
Ben Mares
43

Wstęp do pakowania:

Zanim będziesz mógł się martwić czytaniem plików zasobów, pierwszym krokiem jest upewnienie się, że pliki danych są pakowane do Twojej dystrybucji - łatwo jest je odczytać bezpośrednio z drzewa źródłowego, ale ważną częścią jest upewnij się, że te pliki zasobów są dostępne z kodu w zainstalowanym pakiecie.

Zorganizuj swój projekt w ten sposób, umieszczając pliki danych w podkatalogu w pakiecie:

.
├── package
   ├── __init__.py
   ├── templates
      └── temp_file
   ├── mymodule1.py
   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

Należy przekazać include_package_data=Truedo setup()rozmowy. Plik manifestu jest potrzebny tylko wtedy, gdy chcesz używać setuptools / distutils i kompilować dystrybucje źródła. Aby upewnić się, że templates/temp_filezostanie spakowany dla tej przykładowej struktury projektu, dodaj wiersz podobny do tego w pliku manifestu:

recursive-include package *

Notatka historyczna: Używanie pliku manifestu nie jest potrzebne w przypadku nowoczesnych backendów kompilacji, takich jak flit czy poezja, które domyślnie będą zawierać pliki danych pakietu. Tak więc, jeśli używasz pyproject.tomli nie masz setup.pypliku, możesz zignorować wszystkie rzeczy MANIFEST.in.

Teraz, po usunięciu opakowania, w części do czytania ...

Rekomendacje:

Użyj standardowych pkgutilinterfejsów API biblioteki . W kodzie biblioteki będzie wyglądać tak:

# within package/mymodule1.py, for example
import pkgutil

data = pkgutil.get_data(__name__, "templates/temp_file")
print("data:", repr(data))
text = pkgutil.get_data(__name__, "templates/temp_file").decode()
print("text:", repr(text))

Działa w zamkach błyskawicznych. Działa na Pythonie 2 i Pythonie 3. Nie wymaga zależności innych firm. Nie znam żadnych wad (jeśli tak, to skomentuj odpowiedź).

Złe sposoby uniknięcia:

Zły sposób nr 1: użycie ścieżek względnych z pliku źródłowego

To jest obecnie akceptowana odpowiedź. W najlepszym przypadku wygląda to mniej więcej tak:

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()
print("data", repr(data))

Co z tym jest nie tak? Założenie, że masz dostępne pliki i podkatalogi, nie jest poprawne. To podejście nie działa, jeśli wykonuje się kod, który jest spakowany w zipie lub kółku i może być całkowicie poza kontrolą użytkownika, czy pakiet zostanie w ogóle wyodrębniony do systemu plików.

Zły sposób nr 2: używanie interfejsów API pkg_resources

Jest to opisane w odpowiedzi z największą liczbą głosów. Wygląda mniej więcej tak:

from pkg_resources import resource_string

data = resource_string(__name__, "templates/temp_file")
print("data", repr(data))

Co z tym jest nie tak? Dodaje zależność od środowiska wykonawczego do setuptools , która powinna być zależna tylko od czasu instalacji . Importowanie i używanie pkg_resourcesmoże stać się naprawdę powolne, ponieważ kod tworzy działający zestaw wszystkich zainstalowanych pakietów, nawet jeśli interesowały Cię tylko własne zasoby pakietów. To nie jest wielka sprawa w czasie instalacji (ponieważ instalacja jest jednorazowa), ale jest brzydka w czasie wykonywania.

Zły sposób nr 3: używanie interfejsów API importlib.resources

To jest obecnie zalecenie w odpowiedzi z największą liczbą głosów. Jest to niedawny dodatek do biblioteki standardowej ( nowość w Pythonie 3.7 ), ale dostępny jest również backport. To wygląda tak:

try:
    from importlib.resources import read_binary
    from importlib.resources import read_text
except ImportError:
    # Python 2.x backport
    from importlib_resources import read_binary
    from importlib_resources import read_text

data = read_binary("package.templates", "temp_file")
print("data", repr(data))
text = read_text("package.templates", "temp_file")
print("text", repr(text))

Co z tym jest nie tak? Cóż, niestety, to nie działa ... jeszcze. Jest to wciąż niepełny interfejs API, użycie importlib.resourcesbędzie wymagało dodania pustego pliku templates/__init__.py, aby pliki danych znajdowały się w pakiecie podrzędnym, a nie w podkatalogu. package/templatesUjawni również podkatalog jako samodzielny package.templatespakiet podrzędny, który można zaimportować . Jeśli to nic wielkiego i ci to nie przeszkadza, możesz dodać __init__.pytam plik i użyć systemu importu, aby uzyskać dostęp do zasobów. Jednak gdy już to robisz, równie dobrze możesz zamienić go w my_resources.pyplik i po prostu zdefiniować kilka bajtów lub zmiennych łańcuchowych w module, a następnie zaimportować je w kodzie Pythona. Tak czy inaczej, to system importu wykonuje tu ciężkie podnoszenie.

Przykładowy projekt:

Stworzyłem przykładowy projekt na github i załadowałem na PyPI , który demonstruje wszystkie cztery omówione powyżej podejścia. Wypróbuj z:

$ pip install resources-example
$ resources-example

Więcej informacji można znaleźć pod adresem https://github.com/wimglenn/resources-example .

wim
źródło
1
Został zredagowany w maju ubiegłego roku. Ale wydaje mi się, że łatwo przeoczyć wyjaśnienia na wstępie. Mimo to
radzisz
1
@ankostis Pozwólcie, że zamiast tego zadam wam pytanie, dlaczego miałbyś polecać importlib.resourcespomimo tych wszystkich niedociągnięć z niekompletnym interfejsem API, który już oczekuje na wycofanie ? Nowsze niekoniecznie znaczy lepsze. Powiedz mi, jakie zalety faktycznie oferuje w porównaniu ze standardowym pakietem pkgutil, o którym w Twojej odpowiedzi nie ma żadnej wzmianki?
wim
1
Drogi @wimie, ostatnia odpowiedź Brett Canona na temat korzystania z tego pkgutil.get_data()potwierdzonego przeczucia - to niedopracowany interfejs API, który ma być przestarzały. To powiedziawszy, zgadzam się z tobą, importlib.resourcesnie jest dużo lepszą alternatywą, ale dopóki PY3.10 nie rozwiąże tego problemu, stoję przy tym wyborze, heving dowiedział się, że nie jest to kolejny „standard” zalecany przez dokumentację.
ankostis
1
@ankostis Z przymrużeniem oka podchodzę do komentarzy Bretta. pkgutilnie jest w ogóle wymieniony w harmonogramie wycofywania PEP 594 - Usuwanie rozładowanych baterii ze standardowej biblioteki i jest mało prawdopodobne, aby został usunięty bez uzasadnionego powodu. Istnieje od Pythona 2.3 i jest określony jako część protokołu modułu ładującego w PEP 302 . Używanie "niedefiniowalnego API" nie jest zbyt przekonującą odpowiedzią, która mogłaby opisać większość standardowej biblioteki Pythona!
wim
2
Dodam: chcę, żeby zasoby importlib również się powiodły! Jestem zwolennikiem rygorystycznie zdefiniowanych interfejsów API. Po prostu w obecnym stanie nie można go polecić. Interfejs API wciąż podlega zmianom, nie nadaje się do użytku w przypadku wielu istniejących pakietów i jest dostępny tylko w stosunkowo nowych wersjach języka Python. W praktyce jest gorzej niż pkgutilpod każdym względem. Twoje „przeczucie” i apel do autorytetu są dla mnie bez znaczenia, jeśli występują problemy z get_dataładowaczami, pokaż dowody i praktyczne przykłady.
wim
14

Jeśli masz taką strukturę

lidtk
├── bin
   └── lidtk
├── lidtk
   ├── analysis
      ├── char_distribution.py
      └── create_cm.py
   ├── classifiers
      ├── char_dist_metric_train_test.py
      ├── char_features.py
      ├── cld2
         ├── cld2_preds.txt
         └── cld2wili.py
      ├── get_cld2.py
      ├── text_cat
         ├── __init__.py
         ├── README.md   <---------- say you want to get this
         └── textcat_ngram.py
      └── tfidf_features.py
   ├── data
      ├── __init__.py
      ├── create_ml_dataset.py
      ├── download_documents.py
      ├── language_utils.py
      ├── pickle_to_txt.py
      └── wili.py
   ├── __init__.py
   ├── get_predictions.py
   ├── languages.csv
   └── utils.py
├── README.md
├── setup.cfg
└── setup.py

potrzebujesz tego kodu:

import pkg_resources

# __name__ in case you're within the package
# - otherwise it would be 'lidtk' in this example as it is the package name
path = 'classifiers/text_cat/README.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

Dziwna część „zawsze używaj ukośnika” pochodzi z setuptoolsinterfejsów API

Zauważ również, że jeśli używasz ścieżek, musisz użyć ukośnika (/) jako separatora ścieżki, nawet jeśli używasz systemu Windows. Setuptools automatycznie konwertuje ukośniki na odpowiednie separatory specyficzne dla platformy w czasie kompilacji

Jeśli zastanawiasz się, gdzie jest dokumentacja:

Martin Thoma
źródło
Dziękuję za zwięzłą odpowiedź
Paolo
pkg_resourcesma koszty ogólne, które pkgutilprzezwyciężają. Ponadto, jeśli podany kod jest uruchamiany jako punkt wejścia, __name__zostanie oceniony na __main__, a nie na nazwę pakietu.
A. Hendry
8

Treść w „10.8. Reading Datafiles Within a Package” w książce Python Cookbook, wydanie trzecie autorstwa Davida Beazleya i Briana K. Jonesa, udzielających odpowiedzi.

Po prostu przekażę to tutaj:

Załóżmy, że masz pakiet z plikami zorganizowanymi w następujący sposób:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Teraz przypuśćmy, że plik spam.py chce odczytać zawartość pliku somedata.dat. Aby to zrobić, użyj następującego kodu:

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')

Wynikowe dane zmiennej będą ciągiem bajtów zawierającym nieprzetworzoną zawartość pliku.

Pierwszym argumentem metody get_data () jest ciąg znaków zawierający nazwę pakietu. Możesz podać go bezpośrednio lub użyć specjalnej zmiennej, takiej jak__package__ . Drugi argument to względna nazwa pliku w pakiecie. Jeśli to konieczne, możesz przejść do różnych katalogów, używając standardowych konwencji nazw plików Uniksa, o ile ostateczny katalog nadal znajduje się w pakiecie.

W ten sposób pakiet można zainstalować jako katalog, .zip lub .egg.

chaokunyang
źródło
Podoba mi się, że odwołałeś się do książki kucharskiej!
A. Hendry
0

Zaakceptowaną odpowiedzią powinno być użycie importlib.resources. pkgutil.get_datawymaga również, aby argument packagebył pakietem innym niż przestrzeń nazw ( zobacz dokumentację pkgutil ). Dlatego katalog zawierający zasób musi mieć __init__.pyplik, dzięki czemu ma dokładnie takie same ograniczenia jak importlib.resources. Jeśli kwestia kosztów ogólnych pkg_resourcesnie stanowi problemu, jest to również akceptowalna alternatywa.

A. Hendry
źródło
-1

Każdy moduł Pythona w twoim pakiecie ma __file__atrybut

Możesz go używać jako:

import os 
from mypackage

templates_dir = os.path.join(os.path.dirname(mypackage.__file__), 'templates')
template_file = os.path.join(templates_dir, 'template.txt')

Aby uzyskać zasoby dotyczące jaj, zobacz: http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources

Zaur Nasibov
źródło
To nie zadziała w przypadku kodu źródłowego zawartego w plikach zip.
A. Hendry
-3

zakładając, że używasz pilnika jajecznego; nie wyodrębniony:

„Rozwiązałem” to w ostatnim projekcie, używając skryptu poinstalacyjnego, który wyodrębnia moje szablony z jajka (pliku zip) do odpowiedniego katalogu w systemie plików. Było to najszybsze i najbardziej niezawodne rozwiązanie, jakie znalazłem, ponieważ __path__[0]czasami praca z nim może się nie udać (nie pamiętam nazwy, ale przeszedłem przez co najmniej jedną bibliotekę, która dodała coś przed tą listą!).

Pliki z jajami są również zwykle pobierane w locie do tymczasowej lokalizacji zwanej „składnicą jaj”. Możesz zmienić tę lokalizację za pomocą zmiennej środowiskowej, przed uruchomieniem skryptu lub nawet później, np.

os.environ['PYTHON_EGG_CACHE'] = path

Istnieją jednak zasoby pkg_resources, które mogą wykonać zadanie poprawnie.

Florian
źródło