Acessar dados no subdiretório do pacote

130

Estou escrevendo um pacote python com módulos que precisam abrir arquivos de dados em um ./data/subdiretório. No momento, tenho os caminhos dos arquivos codificados em minhas classes e funções. Gostaria de escrever um código mais robusto que possa acessar o subdiretório, independentemente de onde ele esteja instalado no sistema do usuário.

Eu tentei uma variedade de métodos, mas até agora não tive sorte. Parece que a maioria dos comandos "diretório atual" retornam o diretório do interpretador python do sistema, e não o diretório do módulo.

Parece que deve ser um problema comum e trivial. No entanto, eu não consigo entender. Parte do problema é que meus arquivos de dados não são .pyarquivos, então não posso usar funções de importação e coisas do gênero.

Alguma sugestão?

No momento, meu diretório de pacotes se parece com:

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

Estou tentando acessar data.txta partir module*.py!

Jacob Lyles
fonte

Respostas:

24

Você pode usar __file__para obter o caminho para o pacote, assim:

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
fonte
44
Isso não funcionará se os arquivos estiverem em uma distribuição (IE. Egg). Use pkg_resources para acessar o arquivo de dados.
Chris
2
De fato, isso está quebrado.
Federico
1
Além disso, __file__não funciona com py2exe, pois o valor será o caminho para o arquivo zip.
Pod
1
Isso realmente funcionou para mim. Não teve nenhum problema. Eu estou usando python 3.6
Jorge
1
Isso não funcionará em caso de distribuição (ovo, etc.).
Adarsh ​​Trivedi
166

A maneira padrão de fazer isso é com os pacotes setuptools e pkg_resources.

Você pode organizar seu pacote de acordo com a seguinte hierarquia e configurar o arquivo de instalação do pacote para apontar seus recursos de dados, conforme este link:

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

Você pode então encontrar novamente e usar esses arquivos usando pkg_resources, conforme este link:

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
fonte
7
O pkg_resources não criará uma dependência em tempo de execução do setuptools ? Por exemplo, eu redistribuo um pacote Debian, então por que dependeria python-setuptoolsapenas disso? Até agora __file__funciona bem para mim.
Mlt 12/07/2013
4
Por que isso é melhor: A classe ResourceManager fornece acesso uniforme aos recursos do pacote, se existem esses recursos como arquivos e diretórios ou são compactados em um arquivo de algum tipo
vrdhn
4
Sugestão brilhante, obrigado. Implementei um arquivo padrão aberto usandofrom pkg_resources import resource_filename open(resource_filename('data', 'data.txt'), 'rb')
eageranalyst
5
Como isso funcionará ao usar o pacote quando ele não estiver instalado? Apenas testando localmente quero dizer
Claudiu
11
No python 3.7, importlib.resourcessubstitui pkg_resourcespara esse fim (devido a problemas de desempenho).
benjimin
13

Para fornecer uma solução funcionando hoje. Definitivamente, use esta API para não reinventar todas essas rodas.

É necessário um nome de arquivo verdadeiro do sistema de arquivos. Ovos compactados serão extraídos para um diretório de cache:

from pkg_resources import resource_filename, Requirement

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

Retornar um objeto parecido com um arquivo legível para o recurso especificado; pode ser um arquivo real, um StringIO ou algum objeto semelhante. O fluxo está no "modo binário", no sentido de que quaisquer bytes existentes no recurso serão lidos como estão.

from pkg_resources import resource_stream, Requirement

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

Descoberta de pacotes e acesso a recursos usando pkg_resources

Sascha Gottfried
fonte
10

Muitas vezes, não faz sentido responder que detalha o código que não funciona como está, mas acredito que isso seja uma exceção. Python 3.7 adicionado importlib.resourcesque deve substituir pkg_resources. Funcionaria para acessar arquivos dentro de pacotes que não possuem barras nos nomes, ou seja,

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

ou seja, você pode acessar o data2.txtpacote interno foocom, por exemplo

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

mas falharia com uma exceção para

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

Isso não pode ser fixo, exceto colocando __init__.pyem datae, em seguida, usá-lo como um pacote:

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

A razão para esse comportamento é "é por design" ; mas o design pode mudar ...

Antti Haapala
fonte
Você tem um link melhor para "é por design" do que um vídeo do youtube - de preferência um com texto?
Gerrit
@gerrit o segundo contém texto. "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
8

Você precisa de um nome para todo o seu módulo; sua árvore de diretórios não fornece esses detalhes; para mim, isso funcionou:

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

Notavelmente, o setuptools não parece resolver arquivos com base em uma correspondência de nome com os arquivos de dados compactados, então você precisa incluir o data/prefixo praticamente, não importa o quê. Você pode usar os.path.join('data', 'data.txt)se precisar de separadores de diretório alternativos. Geralmente, não encontro problemas de compatibilidade com os separadores de diretório de estilo unix codificados.

ThorSummoner
fonte
docs.python.org/3.6/distutils/… > Observe que todos os nomes de caminho (arquivos ou diretórios) fornecidos no script de instalação devem ser escritos usando a convenção Unix, ou seja, separados por barras. O Distutils cuidará da conversão dessa representação neutra em plataforma para o que for apropriado na sua plataforma atual antes de realmente usar o nome do caminho. Isso torna seu script de instalação portátil em sistemas operacionais, o que obviamente é um dos principais objetivos do Distutils. Nesse espírito, todos os nomes de caminho neste documento são separados por barras.
changyuheng
6

Eu acho que procurei uma resposta.

Eu faço um módulo data_path.py, que importo em meus outros módulos contendo:

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

E então eu abro todos os meus arquivos com

open(os.path.join(data_path,'filename'), <param>)
Jacob Lyles
fonte
2
Isso não funcionará quando o recurso estiver em uma distribuição de arquivamento (como um ovo compactado). Prefira algo assim:pkg_resources.resource_string('pkg_name', 'data/file.txt')
ankostis
O @ankostis setuptools é inteligente o suficiente para extrair o arquivo se detectar que você o usou em __file__algum lugar. No meu caso, eu uso uma biblioteca que realmente deseja caminhos e não fluxos. É claro que eu poderia gravar os arquivos temporariamente no disco, mas sendo preguiçoso, apenas uso o recurso setuptools.
Letmaik