Como ler um arquivo (estático) de dentro de um pacote Python?

106

Você poderia me dizer como posso ler um arquivo que está dentro do meu pacote Python?

Minha situação

Um pacote que carrego contém vários modelos (arquivos de texto usados ​​como strings) que desejo carregar de dentro do programa. Mas como faço para especificar o caminho para esse arquivo?

Imagine que eu queira ler um arquivo de:

package\templates\temp_file

Algum tipo de manipulação de caminho? Rastreamento do caminho básico do pacote?

Ronszon
fonte
1
possível duplicata de Encontrar um arquivo em uma distribuição de módulo Python
Andreas Jung
possível duplicata de dados de acesso Python no
subdiretório do

Respostas:

-12

[adicionado 2016-06-15: aparentemente, isso não funciona em todas as situações. consulte as outras respostas]


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

TLDR; Use o importlib.resourcesmódulo da biblioteca padrão conforme explicado no método no 2, abaixo.

O tradicional pkg_resourcesdesetuptools não é mais recomendado porque o novo método:

  • é significativamente mais eficiente ;
  • Isso é mais seguro, pois o uso de pacotes (em vez de path-stings) levanta erros de tempo de compilação;
  • é mais intuitivo porque você não precisa "unir" caminhos;
  • é mais rápido durante o desenvolvimento, pois você não precisa de uma dependência extra ( setuptools), mas confie apenas na biblioteca padrão do Python.

Eu mantive o tradicional listado primeiro, para explicar as diferenças com o novo método ao portar o código existente (porting também explicado aqui ).



Vamos supor que seus modelos estejam localizados em uma pasta aninhada dentro do pacote do seu módulo:

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

Nota 1: Com certeza, NÃO devemos mexer com o __file__atributo (por exemplo, o código será quebrado quando fornecido a partir de um CEP).

Observação 2: se você estiver criando este pacote, lembre-se de declarar seus arquivos de dados como package_dataoudata_files em seu setup.py.

1) Usando pkg_resourcesde setuptools(lento)

Você pode usar o pkg_resourcespacote da distribuição de setuptools , mas isso vem com um custo e desempenho :

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)

Dicas:

  • Isso lerá os dados mesmo se sua distribuição for compactada, então você pode definir zip_safe=Trueem seu setup.pye / ou usar o zipappempacotador tão esperado do python-3.5 para criar distribuições independentes.

  • Lembre-se de adicionar setuptoolsem seus requisitos de tempo de execução (por exemplo, em install_requires`).

... e observe que de acordo com Setuptools / pkg_resourcesdocs, você não deve usar os.path.join:

Acesso a recursos básicos

Observe que os nomes dos recursos devem ser /caminhos separados e não podem ser absolutos (ou seja, sem no início /) ou conter nomes relativos como " ..". Você não usar os.pathrotinas para manipular caminhos de recursos, como eles são não caminhos do sistema de arquivos.

2) Python> = 3.7 ou usando a importlib_resourcesbiblioteca com backport

Use o importlib.resourcesmódulo da biblioteca padrão que é mais eficiente do que o setuptoolsacima:

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

Atenção:

Em relação à função read_text(package, resource):

  • O packagepode ser uma string ou um módulo.
  • O resourceNÃO é mais um caminho, mas apenas o nome do arquivo do recurso a ser aberto, dentro de um pacote existente; ele pode não conter separadores de caminho e pode não ter sub-recursos (ou seja, não pode ser um diretório).

Para o exemplo feito na pergunta, devemos agora:

  • transformá-lo <your_package>/templates/ em um pacote adequado, criando um __init__.pyarquivo vazio nele,
  • então agora podemos usar uma importinstrução simples (possivelmente relativa) (sem mais análise de nomes de pacotes / módulos),
  • e simplesmente pergunte por resource_name = "temp_file"(sem caminho).

Dicas:

  • Para acessar um arquivo dentro do módulo atual, defina o argumento do pacote como __package__, por exemplo pkg_resources.read_text(__package__, 'temp_file')(graças a @ben-mares).
  • As coisas se tornam interessantes quando um nome de arquivo real é solicitado path(), já que agora os gerenciadores de contexto são usados ​​para arquivos criados temporariamente (leia isto ).
  • Adicione a biblioteca portada para trás, condicionalmente para Pythons mais antigos, com install_requires=[" importlib_resources ; python_version<'3.7'"](marque isto se você empacotar seu projeto com setuptools<36.2.1).
  • Lembre-se de remover a setuptoolsbiblioteca de seus requisitos de tempo de execução , se você migrou do método tradicional.
  • Recorde personalizar setup.pyou MANIFESTpara incluir quaisquer arquivos estáticos .
  • Você também pode definir zip_safe=Trueem seu setup.py.
Ankostis
fonte
1
str.join leva a sequência resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen
Eu continuo tendo NotImplementedError: Can't perform this operation for loaders without 'get_data()'alguma ideia?
leoschet
Observe que importlib.resourcese nãopkg_resources são necessariamente compatíveis . importlib.resourcesfunciona com arquivos zip adicionados sys.path, ferramentas de instalação e pkg_resourcesfunciona com arquivos egg, que são arquivos zip armazenados em um diretório ao qual ele próprio é adicionado sys.path. Por exemplo sys.path = [..., '.../foo', '.../bar.zip'], com , os ovos entram .../foo, mas os pacotes bar.ziptambém podem ser importados. Você não pode usar pkg_resourcespara extrair dados de pacotes em bar.zip. Não verifiquei se o setuptools registra o carregador necessário para importlib.resourcestrabalhar com ovos.
Martijn Pieters
É necessária uma configuração setup.py adicional se ocorrer um erro Package has no location?
zygimantus
1
Caso você queira acessar um arquivo dentro do módulo atual (e não um submódulo como templatesno exemplo), então você pode definir o packageargumento como __package__, por exemplopkg_resources.read_text(__package__, 'temp_file')
Ben Mares
42

Um prelúdio de embalagem:

Antes mesmo de se preocupar com a leitura de arquivos de recursos, a primeira etapa é certificar-se de que os arquivos de dados estão sendo empacotados em sua distribuição - é fácil lê-los diretamente da árvore de origem, mas a parte importante é fazer certifique-se de que esses arquivos de recursos sejam acessíveis a partir do código de um pacote instalado .

Estruture seu projeto assim, colocando os arquivos de dados em um subdiretório dentro do pacote:

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

Você deve passar include_package_data=Truena setup()chamada. O arquivo de manifesto é necessário apenas se você quiser usar setuptools / distutils e distribuições de código-fonte de compilação. Para garantir que o templates/temp_fileseja empacotado para esta estrutura de projeto de exemplo, adicione uma linha como esta no arquivo de manifesto:

recursive-include package *

Nota crítica histórica: o uso de um arquivo de manifesto não é necessário para back-ends de compilação moderna , como flit, poesia, que incluirá os arquivos de dados do pacote por padrão. Portanto, se você estiver usando pyproject.tomle não tiver um setup.pyarquivo, poderá ignorar todas as informações sobre MANIFEST.in.

Agora, com a embalagem fora do caminho, para a parte de leitura ...

Recomendação:

Use pkgutilAPIs de biblioteca padrão . Vai ficar assim no código da biblioteca:

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

Funciona em zips. Funciona em Python 2 e Python 3. Não requer dependências de terceiros. Não estou realmente ciente de quaisquer desvantagens (se você estiver, por favor, comente sobre a resposta).

Maneiras ruins de evitar:

Maneira ruim nº 1: usando caminhos relativos de um arquivo de origem

Esta é atualmente a resposta aceita. Na melhor das hipóteses, é mais ou menos assim:

from pathlib import Path

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

O que há de errado nisso? A suposição de que você possui arquivos e subdiretórios disponíveis não é correta. Essa abordagem não funciona se estiver executando um código compactado em um zip ou roda, e pode estar totalmente fora do controle do usuário, independentemente de seu pacote ser extraído ou não para um sistema de arquivos.

Maneira ruim nº 2: usando APIs pkg_resources

Isso é descrito na resposta mais votada. É mais ou menos assim:

from pkg_resources import resource_string

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

O que há de errado nisso? Ele adiciona uma dependência de tempo de execução em ferramentas de instalação , que deve ser preferencialmente apenas uma dependência de tempo de instalação . A importação e o uso pkg_resourcespodem se tornar muito lentos, pois o código constrói um conjunto de trabalho de todos os pacotes instalados, mesmo que você esteja interessado apenas em seus próprios recursos de pacote. Isso não é grande coisa no momento da instalação (já que a instalação é única), mas é feio no momento da execução.

Maneira ruim nº 3: usando APIs importlib.resources

Esta é atualmente a recomendação na resposta mais votada. É uma adição recente à biblioteca padrão ( nova no Python 3.7 ), mas também está disponível uma porta traseira. Se parece com isso:

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

O que há de errado nisso? Bem, infelizmente, não funciona ... ainda. Esta ainda é uma API incompleta, o uso importlib.resourcesexigirá que você adicione um arquivo vazio templates/__init__.pypara que os arquivos de dados residam em um subpacote em vez de em um subdiretório. Ele também irá expor o package/templatessubdiretório como um subpacote importável package.templatespor conta própria. Se isso não é um grande problema e não o incomoda, então você pode ir em frente e adicionar o __init__.pyarquivo lá e usar o sistema de importação para acessar os recursos. No entanto, enquanto você está nisso, você também pode transformá-lo em um my_resources.pyarquivo e apenas definir alguns bytes ou variáveis ​​de string no módulo e importá-los no código Python. É o sistema de importação que está fazendo o trabalho pesado aqui de qualquer maneira.

Projeto de exemplo:

Criei um projeto de exemplo no github e carreguei no PyPI , que demonstra todas as quatro abordagens discutidas acima. Experimente com:

$ pip install resources-example
$ resources-example

Veja https://github.com/wimglenn/resources-example para mais informações.

wim
fonte
1
Foi editado em maio passado. Mas acho que é fácil perder as explicações na introdução. Ainda assim, você aconselha as pessoas contra o padrão - isso é difícil de morder :-)
ankostis
1
@ankostis Deixe-me colocar a questão para você, por que você recomendaria, importlib.resourcesapesar de todas essas deficiências, uma API incompleta que já está com suspensão de uso ? Mais recente não é necessariamente melhor. Diga-me quais são as vantagens que ele realmente oferece sobre o stdlib pkgutil, sobre o qual sua resposta não faz nenhuma menção?
wim
1
Caro @wim, a última resposta de Brett Canon sobre o uso de pkgutil.get_data()confirmou minha intuição - é uma API subdesenvolvida e a ser obsoleta. Dito isso, concordo com você, importlib.resourcesnão é uma alternativa muito melhor, mas até que PY3.10 resolva isso, mantenho essa escolha, mas aprendi que não é apenas mais um "padrão" recomendado pelos documentos.
ankostis
1
@ankostis Eu levaria os comentários de Brett com um grão de sal. pkgutilnão é mencionado de forma alguma no cronograma de reprovação do PEP 594 - Remoção de baterias gastas da biblioteca padrão e é improvável que seja removido sem um bom motivo. Ele existe desde Python 2.3 e é especificado como parte do protocolo do carregador no PEP 302 . Usar uma "API subdefinida" não é uma resposta muito convincente, que poderia descrever a maior parte da biblioteca padrão do Python!
wim
2
Deixe-me acrescentar: também quero ver os recursos importlib bem-sucedidos! Sou totalmente a favor de APIs rigorosamente definidas. Só que em seu estado atual não pode ser recomendado. A API ainda está passando por mudanças, é inutilizável para muitos pacotes existentes e está disponível apenas em versões relativamente recentes do Python. Na prática, é pior do que pkgutilem quase todos os sentidos. Seu "pressentimento" e apelo à autoridade não tem sentido para mim. Se houver problemas com get_datacarregadores, mostre evidências e exemplos práticos.
wim
15

Caso você tenha essa estrutura

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

você precisa deste código:

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)

A parte estranha de "sempre usar barra" vem de setuptoolsAPIs

Observe também que se você usar caminhos, deverá usar uma barra (/) como o separador de caminho, mesmo se estiver no Windows. Setuptools converte automaticamente barras em separadores específicos de plataforma apropriados no momento da construção

Caso você queira saber onde está a documentação:

Martin Thoma
fonte
Obrigado por sua resposta concisa
Paolo
8

O conteúdo em "10.8. Lendo Arquivos de Dados em um Pacote" do Python Cookbook, Terceira Edição de David Beazley e Brian K. Jones fornecendo as respostas.

Vou apenas chegar aqui:

Suponha que você tenha um pacote com arquivos organizados da seguinte maneira:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Agora, suponha que o arquivo spam.py deseja ler o conteúdo do arquivo somedata.dat. Para fazer isso, use o seguinte código:

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

Os dados variáveis ​​resultantes serão uma string de bytes contendo o conteúdo bruto do arquivo.

O primeiro argumento para get_data () é uma string contendo o nome do pacote. Você pode fornecê-lo diretamente ou usar uma variável especial, como __package__. O segundo argumento é o nome relativo do arquivo dentro do pacote. Se necessário, você pode navegar em diferentes diretórios usando as convenções de nome de arquivo Unix padrão, desde que o diretório final ainda esteja localizado dentro do pacote.

Desta forma, o pacote pode ser instalado como diretório, .zip ou .egg.

Chaokunyang
fonte
0

Cada módulo Python em seu pacote tem um __file__atributo

Você pode usá-lo como:

import os 
from mypackage

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

Para recursos de ovos, consulte: http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources

Zaur Nasibov
fonte
-2

assumindo que você está usando um arquivo de ovo; não extraído:

Eu "resolvi" isso em um projeto recente, usando um script de pós-instalação, que extrai meus modelos do ovo (arquivo zip) para o diretório apropriado no sistema de arquivos. Foi a solução mais rápida e confiável que encontrei, já que trabalhar com __path__[0]pode às vezes dar errado (não me lembro o nome, mas encontrei pelo menos uma biblioteca, que acrescentou algo na frente dessa lista!).

Além disso, os arquivos de ovo são normalmente extraídos instantaneamente para um local temporário chamado "cache de ovo". Você pode alterar esse local usando uma variável de ambiente, antes de iniciar seu script ou até mais tarde, por exemplo.

os.environ['PYTHON_EGG_CACHE'] = path

No entanto, há pkg_resources que podem fazer o trabalho corretamente.

Florian
fonte