Bir Python paketinin içinden (statik) bir dosya nasıl okunur?


108

Python paketimin içindeki bir dosyayı nasıl okuyabileceğimi bana söyleyebilir misiniz?

Benim durumum

Yüklediğim bir paket, programın içinden yüklemek istediğim bir dizi şablona (dizeler olarak kullanılan metin dosyaları) sahip. Ancak böyle bir dosyanın yolunu nasıl belirtebilirim?

Şuradan bir dosya okumak istediğimi düşünün:

package\templates\temp_file

Bir çeşit yol manipülasyonu mu? Paket temel yol takibi?



Yanıtlar:


-13

[eklendi 2016-06-15: Görünüşe göre bu her durumda işe yaramıyor. lütfen diğer cevaplara bakın]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')

177

TLDR; Aşağıdaki 2 numaralı yöntemde açıklandığı gibi standart kitaplık importlib.resourcesmodülünü kullanın .

Geleneksel pkg_resourcesdansetuptools artık önerilmemektedir yeni yöntem çünkü:

  • bunun önemli ölçüde daha fazla ölçülebilir ;
  • paketlerin kullanılması (yol işaretleri yerine) derleme zamanı hatalarını artırdığı için daha güvenlidir;
  • daha sezgiseldir çünkü yolları "birleştirmek" zorunda değilsiniz;
  • fazladan bir bağımlılığa ( setuptools) ihtiyacınız olmadığı için geliştirirken daha hızlıdır , ancak yalnızca Python'un standart kütüphanesine güvenirsiniz.

Mevcut kodu taşırken yeni yöntemle farklılıkları açıklamak için geleneksel olanı ilk sıraya koydum (taşıma burada da açıklanmıştır ).



Şablonlarınızın, modülünüzün paketinin içine yerleştirilmiş bir klasörde bulunduğunu varsayalım:

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

Not 1: Elbette, __file__öznitelikle uğraşmamalıyız (örneğin, bir zip'ten sunulduğunda kod kırılır).

Not 2: Bu paketi oluşturuyorsanız gibi veri dosyalarınızı declatre hatırlamak package_datayadata_files da In setup.py.

1) kullanılarak pkg_resourcesdan setuptoolsyavaş ()

Kurulum araçları dağıtımındaki pkg_resourcespaketi kullanabilirsiniz , ancak bu, performans açısından bir maliyetle birlikte gelir :

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)

İpuçları:

  • Bu dağıtım sıkıştırılmış olsa bile ayarladığınız bu yüzden, veri okuyacak zip_safe=TrueGözlerinde farklı setup.pyve / veya uzun zamandır beklenen kullanmak zipappPacker gelen piton-3,5 müstakil dağıtımları oluşturmak için.

  • setuptoolsÇalışma zamanı gereksinimlerinize eklemeyi unutmayın (örn. İnstall_requires` içinde).

... ve Setuptools / docs'a göre pkg_resourcesşunları kullanmamalısınız os.path.join:

Temel Kaynak Erişimi

Kaynak adlarının /-ayrılmış yollar olması gerektiğini ve mutlak olamayacağını (yani baştaki olmamasını /) veya " .." gibi göreli adlar içeremeyeceğini unutmayın . Do not kullanmak os.patholdukları gibi, kaynak yolları işlemek için rutinlerini değil dosya sistemi yolları.

2) Python> = 3.7 veya backported importlib_resourceskitaplığı kullanma

Yukarıdakinden daha verimli olan standart kitaplık importlib.resourcesmodülünü kullanın setuptools:

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

Dikkat:

İşlevle ilgili olarak read_text(package, resource):

  • packageBir dize veya bir modül olabilir.
  • resourceArtık bir yol, ancak varolan paket içinde açık kaynağın, sadece dosya adı değil; yol ayırıcıları içermeyebilir ve alt kaynakları olmayabilir (yani bir dizin olamaz).

Soruda sorulan örnek için, şimdi yapmalıyız:

  • yapmak <your_package>/templates/ boş oluşturarak, uygun bir paket içine __init__.pyo dosyayı,
  • bu yüzden şimdi basit (muhtemelen göreceli) bir importifade kullanabiliriz (artık paket / modül adlarını ayrıştırmaya gerek yok),
  • ve basitçe resource_name = "temp_file"(yol yok) isteyin .

İpuçları:

  • Geçerli modül içindeki bir dosyaya erişmek için, paket argümanını olarak ayarlayın __package__, örneğin pkg_resources.read_text(__package__, 'temp_file')(@ ben-mares sayesinde).
  • Bir zaman şeyler ilginç hale gerçek dosya ile istenir path()şimdi bağlam yöneticileri geçici olarak oluşturulan dosyaların (okuma için kullanıldığından, bu ).
  • İle, şartlı yaşlı piton için, backported kütüphane ekle install_requires=[" importlib_resources ; python_version<'3.7'"](kontrol Bu size projenizi paket halinde setuptools<36.2.1).
  • Geleneksel yöntemden geçiş yaptıysanız, setuptoolskitaplığı çalışma zamanı gereksinimlerinizden kaldırmayı unutmayın .
  • Özelleştirmeyi unutmayın setup.pyya MANIFESTkarşı herhangi statik dosyaları içerir .
  • Ayrıca ayarlayabilirsiniz zip_safe=TrueGözlerinde farklı setup.py.

1
str.join, resource_path = '/'.join(('templates', 'temp_file')) dizisini alır
Alex Punnen

2
NotImplementedError: Can't perform this operation for loaders without 'get_data()'Herhangi bir fikir almaya devam ediyor muyum ?
leoschet

Not importlib.resourcesve pkg_resourcesvardır mutlaka uyumlu değildir . importlib.resourceseklenen zipfiles ile çalışır sys.path, setuptools ve pkg_resourceskendisi eklenir bir dizinde saklanan zipfiles olan yumurta dosyaları ile çalışmak sys.path. Örneğin sys.path = [..., '.../foo', '.../bar.zip'], yumurtalar içeri girer , .../fooancak paketler bar.zipde ithal edilebilir. İçindeki pkg_resourcespaketlerden veri ayıklamak için kullanamazsınız bar.zip. Setuptools'un importlib.resourcesyumurtalarla çalışmak için gerekli yükleyiciyi kaydedip kaydetmediğini kontrol etmedim .
Martijn Pieters

Hata Package has no locationgörünürse ek setup.py yapılandırması gerekiyor mu?
zygimantus

1
Eğer geçerli modül içindeki dosya (ve benzeri olmayan bir alt modülü erişmek istediğiniz templates, o zaman ayarlayabilirsiniz Örneğin uyarınca) packageiçin argüman __package__, örneğinpkg_resources.read_text(__package__, 'temp_file')
Ben Mares

46

Bir ambalaj başlangıcı:

Kaynak dosyalarını okuma konusunda endişelenmeden önce, ilk adım, veri dosyalarının ilk etapta dağıtımınıza paketlendiğinden emin olmaktır - bunları doğrudan kaynak ağacından okumak kolaydır, ancak önemli olan şey yapmaktır. bu kaynak dosyalarına kurulu bir paket içindeki koddan erişilebildiğinden emin olun .

Bir alt dizinin içine veri dosyaları koyarak böyle Yapı projenizin içinde paketinin:

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

Sen geçmelidir include_package_data=Trueiçinde setup()çağrı. Manifest dosyası yalnızca setuptools / distutils kullanmak ve kaynak dağıtımları oluşturmak istiyorsanız gereklidir. templates/temp_fileBu örnek proje yapısının paketlendiğinden emin olmak için , manifest dosyasına şuna benzer bir satır ekleyin:

recursive-include package *

Tarihsel temel not: Varsayılan olarak paket veri dosyalarını içerecek olan flit, şiir gibi modern derleme arka uçları için bir bildirim dosyası kullanmak gerekmez . Yani, kullanıyorsanız pyproject.tomlve bir setup.pydosyanız yoksa, ilgili her şeyi göz ardı edebilirsiniz MANIFEST.in.

Şimdi, ambalajı yoldan çıkararak, okuma kısmına ...

Öneri:

Standart kitaplık pkgutilAPI'lerini kullanın . Kitaplık kodunda şöyle görünecek:

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

data = pkgutil.get_data(__name__, "templates/temp_file")

Fermuarlı çalışır. Python 2 ve Python 3 üzerinde çalışır. Üçüncü taraf bağımlılıkları gerektirmez. Herhangi bir dezavantajın gerçekten farkında değilim (eğer öyleyseniz, lütfen cevaba yorum yapın).

Kaçınmanın kötü yolları:

Kötü yol 1: Bir kaynak dosyadan göreli yolları kullanmak

Bu şu anda kabul edilen cevaptır. En iyi ihtimalle şuna benzer:

from pathlib import Path

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

Bunun derdi ne? Kullanılabilir dosyalarınız ve alt dizinleriniz olduğu varsayımı doğru değildir. Bu yaklaşım, bir zip veya bir tekerlek içinde paketlenmiş kod çalıştırılırken işe yaramaz ve paketinizin bir dosya sistemine çıkarılıp çıkarılmayacağı tamamen kullanıcının kontrolü dışında olabilir.

Kötü yol 2: pkg_resources API'lerini kullanma

Bu, en çok oylanan cevapta açıklanmıştır. Şuna benzer:

from pkg_resources import resource_string

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

Bunun derdi ne? Bir ekleyen çalışma zamanı bağımlılığı setuptools tercihen bir olmalı, yüklemek sadece zaman bağımlılığı. pkg_resourcesKod, yalnızca kendi paket kaynaklarınızla ilgilenmiş olsanız bile , tüm kurulu paketlerin bir çalışma kümesini oluşturduğundan, içe aktarma ve kullanma gerçekten yavaşlayabilir . Bu, yükleme sırasında önemli bir sorun değil (yükleme bir kez kapalı olduğundan), ancak çalışma zamanında çirkin.

Kötü yol 3: importlib.resources API'lerini kullanma

Bu, şu anda en çok oy alan cevapta öneridir. Yeni bir standart kitaplık ekidir ( Python 3.7'de yeni ). Şöyle görünüyor:

from importlib.resources import read_binary

data = read_binary("package.templates", "temp_file")

Bunun derdi ne? Maalesef işe yaramıyor ... henüz. Bu hala tamamlanmamış bir API'dir; kullanmak , veri dosyalarının bir alt dizin yerine bir alt paket içinde yer alması için importlib.resourcesboş bir dosya eklemenizi gerektirir templates/__init__.py. Ayrıca, package/templatesalt dizini package.templateskendi başına içe aktarılabilir bir alt paket olarak ortaya çıkaracaktır . Bu büyük bir sorun değilse ve sizi rahatsız etmiyorsa, devam edip __init__.pydosyayı oraya ekleyebilir ve kaynaklara erişmek için içe aktarma sistemini kullanabilirsiniz. Bununla birlikte, oradayken, bunun my_resources.pyyerine bir dosyaya dönüştürebilir ve modülde sadece bazı baytlar veya dize değişkenleri tanımlayabilir, ardından bunları Python koduna aktarabilirsiniz. Her iki şekilde de burada ağır işi yapan ithalat sistemi.

Onur sözü: daha yeni importlib_resources API'leri kullanma

Bu henüz importlib_resourcesbaşka bir yanıtta belirtilmemiştir, ancak Python 3.7+ importlib.resourceskodunun basit bir arka planından daha fazlasıdır . Bunun gibi kullanabileceğiniz geçiş yapılabilir API'lara sahiptir:

import importlib_resources

my_resources = importlib_resources.files("package")
data = (my_resources / "templates" / "temp_file").read_bytes()

Bu Python 2 ve 3 üzerinde çalışır, zip şeklinde çalışır ve __init__.pykaynak alt dizinlerine sahte dosyaların eklenmesini gerektirmez . Görebildiğim tek dezavantajı pkgutil, bu yeni API'lerin henüz stdlib'e ulaşmamış olması, dolayısıyla hala bir üçüncü taraf bağımlılığı var. Python 3.9'da daha yeni API'ler importlib_resourcesstdlib'e ulaşmalıdır importlib.resources.

Örnek proje:

Github'da örnek bir proje oluşturdum ve yukarıda tartışılan beş yaklaşımı da gösteren PyPI'ye yükledim . Şununla deneyin:

$ pip install resources-example
$ resources-example

Daha fazla bilgi için https://github.com/wimglenn/resources-example adresine bakın .


1
Geçen Mayıs ayında düzenlenmiştir. Ama sanırım girişteki açıklamaları kaçırmak kolay. Yine de insanlara standarda karşı tavsiyede bulunuyorsunuz - bu ısırması zor bir mermi :-)
ankostis

1
@ankostis Bunun yerine soruyu size yönelteyim, neden importlib.resourcestüm bu eksikliklere rağmen zaten kullanımdan kaldırılmayı bekleyen tamamlanmamış bir API ile öneriyorsunuz ? Daha yenisi mutlaka daha iyi değildir. Bana aslında stdlib pkgutil'e göre ne gibi avantajlar sağladığını söyleyin , cevabınız bundan hiç bahsetmiyor mu?
wim

1
Sevgili @wim, Brett Canon'unpkgutil.get_data() içgüdüsel duygularımı doğrulamasına ilişkin son yanıtı - bu, az gelişmiş, kullanımdan kaldırılacak bir API. Bununla birlikte, size katılıyorum, importlib.resourcesçok daha iyi bir alternatif değil, ancak PY3.10 bunu çözene kadar bu seçimin arkasında duruyorum, bunun sadece dokümanlar tarafından önerilen başka bir "standart" olmadığını öğrendim.
ankostis

1
@ankostis Brett'in yorumlarını biraz tuzlu bir şekilde alırdım. PEP 594'ünpkgutil kullanımdan kaldırılma çizelgesinde hiç bahsedilmemiştir - Bitmiş pillerin standart kitaplıktan çıkarılması ve iyi bir neden olmaksızın kaldırılması olası değildir. Python 2.3'ten beri var ve PEP 302'de yükleyici protokolünün bir parçası olarak belirtildi . "Yetersiz tanımlanmış bir API" kullanmak, Python standart kitaplığının çoğunu tanımlayabilecek çok ikna edici bir yanıt değildir!
wim

2
Ekleyeyim : importlib kaynaklarının da başarılı olduğunu görmek istiyorum! Ben titizlikle tanımlanmış API'lerden yanayım. Sadece şu anki haliyle, gerçekten tavsiye edilemez. API hala değişiyor, mevcut birçok paket için kullanılamıyor ve yalnızca nispeten yeni Python sürümlerinde mevcut. Pratikte, pkgutilher şekilde olduğundan daha kötüdür . "İçgüdüleriniz" ve otoriteye başvurmanız benim için anlamsız, get_datayükleyicilerle ilgili sorunlar varsa kanıt ve pratik örnekler gösterin.
wim

14

Bu yapıya sahipseniz

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

bu koda ihtiyacınız var:

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)

Garip "her zaman eğik çizgi kullan" kısmı setuptoolsAPI'lerden gelir

Ayrıca, yolları kullanıyorsanız, Windows'ta olsanız bile yol ayırıcı olarak eğik çizgi (/) kullanmanız gerektiğine dikkat edin. Setuptools, derleme zamanında eğik çizgileri uygun platforma özgü ayırıcılara dönüştürür

Belgelerin nerede olduğunu merak ediyorsanız:


Kısa cevabınız için teşekkür ederiz
Paolo

pkg_resourcespkgutilüstesinden gelen ek yükü vardır . Ayrıca, sağlanan kod giriş noktası olarak çalıştırılırsa , paket adı değil olarak __name__değerlendirilir __main__.
A. Hendry

8

Python Cookbook, Third Edition, David Beazley ve Brian K. Jones'un "10.8. Bir Paket İçinde Veri Dosyalarını Okumak" bölümündeki içerik cevapları veriyor.

Sadece buraya getireceğim:

Aşağıdaki gibi düzenlenmiş dosyalara sahip bir paketiniz olduğunu varsayalım:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Şimdi spam.py dosyasının somedata.dat dosyasının içeriğini okumak istediğini varsayalım. Bunu yapmak için aşağıdaki kodu kullanın:

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

Ortaya çıkan değişken veriler, dosyanın ham içeriğini içeren bir bayt dizesi olacaktır.

Get_data () işlevinin ilk argümanı, paket adını içeren bir dizedir. Doğrudan sağlayabilir veya gibi özel bir değişken kullanabilirsiniz __package__. İkinci argüman, paketteki dosyanın göreceli adıdır. Gerekirse, son dizin paketin içinde bulunduğu sürece standart Unix dosya adı kurallarını kullanarak farklı dizinlere gidebilirsiniz.

Bu şekilde paket dizin, .zip veya .egg olarak kurulabilir.


Yemek kitabına atıfta bulunman hoşuma gitti!
A. Hendry


-1

Kabul edilen cevap kullanmak olmalıdır importlib.resources. pkgutil.get_dataayrıca bağımsız değişkenin packagead alanı olmayan bir paket olmasını gerektirir ( bkz. pkgutil belgeleri ). Bu nedenle, kaynağı içeren dizinin bir __init__.pydosyası olması gerekir , bu da onu importlib.resources. Genel gider konusu pkg_resourcesbir endişe değilse, bu da kabul edilebilir bir alternatiftir.

Pre-Python-3.3, tüm paketlerde bir __init__.py. Post-Python-3.3klasörün __init__.pypaket olmasına gerek yoktur . Buna a namespace package. Maalesef, ile pkgutilçalışmaz namespace packages( pkgutil belgelerine bakın ).

Örneğin, paket yapısıyla:

+-- foo/
|   +-- __init__.py
|   +-- bar/
|   |   +-- hi.txt

hi.txtsadece olduğu yerde Hi!, aşağıdakileri alırsınız

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
None

Bununla birlikte, bir ile __init__.pyin barelde edersiniz

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
b'Hi!'

Bu cevap yanlış - kaynakları içeren dizinin bir paket olması gerekmiyor. Bir paket içinde bir alt dizin olabilir . Sınırlaması importlib.resources, pkgutilyok, dizin içeren kaynaklar kendisi olması gerektiğini oldu __init__.py, çok bir olmak zorunda yani Alt paket . Bu __init__.py, paketteki veri alt dizinleri yerine üst düzey dizinde olup olmadığıyla ilgili ad alanı paketi sorunlarıyla ilgisi yoktur .
wim

@wim Üzgünüm ama yanıldığına inanıyorum. pre-Python 3.3+, tüm paketlerin __init__.pyyüklenmesi gerekiyordu. 3.3 sonrası paketlerin bunlara ihtiyacı yoktur. Olmadan paketleri __init__.pyvardır namespace packages. Başına pkgutilbir ad paketinden kaynak yüklenmeye çalışırsanız dokümanlar, alırsınız None. Lütfen güncellenmiş düzenlenmiş cevabıma bakın.
A. Hendry

pkgutilYanlış kullanıyordunuz . Deneyinpkgutil.get_data("foo", "bar/hi.txt")
wim

-3

bir yumurta dosyası kullandığınızı varsayarsak; çıkarılmamış:

Bunu yakın zamandaki bir projede, şablonlarımı yumurtadan (zip dosyası) dosya sistemindeki uygun dizine çıkaran bir yükleme sonrası komut dosyası kullanarak "çözdüm". Bulduğum en hızlı, en güvenilir çözümdü, çünkü üzerinde çalışmak __path__[0]bazen yanlış gidebiliyor (adı hatırlamıyorum, ancak en az bir kitaplığa bakıyorum ve bu listenin önüne bir şey ekliyor!).

Ayrıca yumurta dosyaları genellikle "yumurta önbelleği" adı verilen geçici bir konuma anında çıkarılır. Bu konumu, komut dosyanızı başlatmadan önce veya daha sonra bir ortam değişkeni kullanarak değiştirebilirsiniz, örn.

os.environ['PYTHON_EGG_CACHE'] = path

Bununla birlikte , işi düzgün bir şekilde yapabilecek pkg_resources var.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.