Impor paket saudara

199

Saya sudah mencoba membaca pertanyaan tentang impor saudara kandung dan bahkan dokumentasi paket , tetapi saya belum menemukan jawabannya.

Dengan struktur sebagai berikut:

├── LICENSE.md
├── README.md
├── api
   ├── __init__.py
   ├── api.py
   └── api_key.py
├── examples
   ├── __init__.py
   ├── example_one.py
   └── example_two.py
└── tests
   ├── __init__.py
   └── test_one.py

Bagaimana skrip dalam direktori examplesdan testsimpor dari apimodul dan dijalankan dari commandline?

Juga, saya ingin menghindari sys.path.insertperetasan yang buruk untuk setiap file. Tentunya ini bisa dilakukan dengan Python, kan?

zachwill
sumber
7
Saya merekomendasikan untuk melewati semua sys.pathperetasan dan membaca satu-satunya solusi aktual yang telah diposting sejauh ini (setelah 7 tahun!).
Aran-Fey
1
Omong-omong, masih ada ruang untuk solusi bagus lainnya: Memisahkan kode yang dapat dieksekusi dari kode perpustakaan; sebagian besar waktu skrip di dalam sebuah paket seharusnya tidak dapat dieksekusi untuk memulai.
Aran-Fey
Ini sangat membantu, baik pertanyaan maupun jawabannya. Saya hanya ingin tahu, mengapa "Jawaban yang Diterima" tidak sama dengan yang dianugerahi hadiah dalam kasus ini?
Indominus
@ Aran-Fey Itu pengingat yang diremehkan dalam T & As kesalahan impor relatif ini. Saya telah mencari peretasan selama ini, tetapi jauh di lubuk hati saya tahu ada cara sederhana untuk merancang jalan keluar dari masalah. Bukan untuk mengatakan bahwa itu adalah solusi untuk semua orang di sini yang membaca, tetapi itu adalah pengingat yang baik bagi banyak orang.
colorlace

Jawaban:

69

Tujuh tahun kemudian

Karena saya menulis jawaban di bawah ini, memodifikasi sys.pathmasih merupakan trik cepat dan kotor yang berfungsi dengan baik untuk skrip pribadi, tetapi ada beberapa peningkatan

  • Menginstal paket (dalam virtualenv atau tidak) akan memberikan apa yang Anda inginkan, meskipun saya sarankan menggunakan pip untuk melakukannya daripada menggunakan setuptools secara langsung (dan menggunakan setup.cfguntuk menyimpan metadata)
  • Menggunakan -mflag dan menjalankannya sebagai sebuah paket juga berfungsi (tetapi akan menjadi sedikit canggung jika Anda ingin mengubah direktori kerja Anda menjadi sebuah paket yang dapat diinstal).
  • Untuk tes, khususnya, pytest dapat menemukan paket api dalam situasi ini dan menangani sys.pathperetasan untuk Anda

Jadi itu sangat tergantung pada apa yang ingin Anda lakukan. Namun, dalam kasus Anda, karena tampaknya tujuan Anda adalah membuat paket yang tepat, menginstal melalui pip -emungkin merupakan taruhan terbaik Anda, bahkan jika itu belum sempurna.

Jawaban lama

Seperti yang sudah dinyatakan di tempat lain, kebenarannya adalah Anda harus melakukan peretasan jelek untuk mengizinkan impor dari modul saudara kandung atau paket orang tua dari sebuah __main__modul. Masalahnya dirinci dalam PEP 366 . PEP 3122 berusaha menangani impor dengan cara yang lebih rasional tetapi Guido telah menolaknya

Satu-satunya use case tampaknya menjalankan skrip yang kebetulan tinggal di dalam direktori modul, yang saya selalu lihat sebagai antipattern.

(di sini )

Padahal, saya menggunakan pola ini secara teratur dengan

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

Ini path[0]adalah folder induk skrip Anda yang sedang berjalan dan dir(path[0])folder tingkat atas Anda.

Saya masih belum dapat menggunakan impor relatif dengan ini, tetapi itu memang memungkinkan impor absolut dari tingkat atas (dalam apifolder induk contoh Anda ).

Evpok
sumber
3
Anda tidak perlu jika Anda menjalankan dari direktori proyek menggunakan -mformulir atau jika Anda menginstal paket (pip dan virtualenv membuatnya mudah)
jfs
2
Bagaimana cara pytest menemukan paket api untuk Anda? Yang mengherankan, saya menemukan utas ini karena saya mengalami masalah ini khusus dengan impor paket pytest dan saudara kandung.
JuniorIncanter
1
Saya punya dua pertanyaan. 1. Pola Anda tampaknya bekerja tanpa __package__ = "examples"untuk saya. Mengapa Anda menggunakan itu? 2. Dalam situasi apa __name__ == "__main__"tetapi __package__tidak None?
actual_panda
Pengaturan @actual_panda __packages__membantu jika Anda ingin jalur absolut seperti examples.apiuntuk bekerja iirc (tapi sudah lama sejak saya terakhir melakukan itu) dan memeriksa bahwa paket tersebut tidak ada.
Evpok
165

Bosan dengan hack sys.path?

Ada banyak sys.path.append-hack yang tersedia, tetapi saya menemukan cara alternatif untuk memecahkan masalah di tangan.

Ringkasan

  • Bungkus kode ke dalam satu folder (mis. packaged_stuff)
  • Gunakan buat setup.pyskrip tempat Anda menggunakan setuptools.setup () .
  • Pip instal paket dengan status yang dapat diedit pip install -e <myproject_folder>
  • Impor menggunakan from packaged_stuff.modulename import function_name

Mempersiapkan

Titik awal adalah struktur file yang Anda berikan, dibungkus dengan folder yang disebut myproject.

.
└── myproject
    ├── api
       ├── api_key.py
       ├── api.py
       └── __init__.py
    ├── examples
       ├── example_one.py
       ├── example_two.py
       └── __init__.py
    ├── LICENCE.md
    ├── README.md
    └── tests
        ├── __init__.py
        └── test_one.py

Saya akan memanggil .folder root, dan dalam contoh saya ini terletak di C:\tmp\test_imports\.

api.py

Sebagai uji coba, mari gunakan yang berikut ini ./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

Coba jalankan test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\myproject\tests\test_one.py", line 1, in <module>
    from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

Juga mencoba impor relatif tidak akan berfungsi:

Menggunakan from ..api.api import function_from_apiakan menghasilkan

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\tests\test_one.py", line 1, in <module>
    from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

Langkah

  1. Buat file setup.py ke direktori level root

Isi untuk setup.pyakan *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Gunakan lingkungan virtual

Jika Anda terbiasa dengan lingkungan virtual, aktifkan satu, dan lewati ke langkah berikutnya. Penggunaan lingkungan virtual tidak mutlak diperlukan, tetapi mereka benar - benar akan membantu Anda dalam jangka panjang (ketika Anda memiliki lebih dari 1 proyek yang sedang berlangsung ..). Langkah paling dasar adalah (dijalankan di folder root)

  • Buat virtual env
    • python -m venv venv
  • Aktifkan virtual env
    • source ./venv/bin/activate(Linux, macOS) atau ./venv/Scripts/activate(Win)

Untuk mempelajari lebih lanjut tentang ini, cukup Google keluar "python virtual env tutorial" atau serupa. Anda mungkin tidak pernah membutuhkan perintah selain membuat, mengaktifkan, dan menonaktifkan.

Setelah Anda membuat dan mengaktifkan lingkungan virtual, konsol Anda harus memberi nama lingkungan virtual dalam tanda kurung

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

dan pohon folder Anda akan terlihat seperti ini **

.
├── myproject
   ├── api
      ├── api_key.py
      ├── api.py
      └── __init__.py
   ├── examples
      ├── example_one.py
      ├── example_two.py
      └── __init__.py
   ├── LICENCE.md
   ├── README.md
   └── tests
       ├── __init__.py
       └── test_one.py
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip instal proyek Anda dalam keadaan dapat diedit

Instal paket tingkat atas Anda myprojectmenggunakan pip. Caranya adalah dengan menggunakan -ebendera saat melakukan instalasi. Dengan cara ini diinstal dalam keadaan yang dapat diedit, dan semua pengeditan yang dilakukan pada file .py akan secara otomatis disertakan dalam paket yang diinstal.

Di direktori root, jalankan

pip install -e . (perhatikan titik, singkatan dari "direktori saat ini")

Anda juga dapat melihat bahwa itu diinstal dengan menggunakan pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
  Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. Tambahkan myproject.ke impor Anda

Perhatikan bahwa Anda harus menambahkan myproject.hanya ke impor yang tidak akan berfungsi sebaliknya. Impor yang berfungsi tanpa setup.py& pip installakan berfungsi dengan baik. Lihat contoh di bawah ini.


Uji solusinya

Sekarang, mari kita coba solusinya menggunakan yang api.pydidefinisikan di atas, dan yang test_one.pydidefinisikan di bawah ini.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

menjalankan tes

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* Lihat dokumen setuptools untuk lebih banyak contoh setup.py verbose.

** Pada kenyataannya, Anda dapat menempatkan lingkungan virtual Anda di mana saja di hard disk Anda.

np8
sumber
13
Terima kasih untuk posting terperinci. Ini masalah saya. Jika saya melakukan semua yang Anda katakan dan saya membekukan pip, saya mendapatkan garis -e git+https://[email protected]/folder/myproject.git@f65466656XXXXX#egg=myprojectAda ide tentang bagaimana menyelesaikan?
Si Sen
2
Mengapa solusi impor relatif tidak berfungsi? Saya percaya Anda, tapi saya mencoba memahami sistem berbelit-belit Python.
Jared Nielsen
8
Adakah yang memiliki masalah terkait dengan ModuleNotFoundError? Saya telah menginstal 'myproject' ke dalam virtualenv mengikuti langkah-langkah ini, dan ketika saya memasukkan sesi yang ditafsirkan dan menjalankan import myprojectsaya dapatkan ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectmenunjukkan bahwa itu ada di sana, direktori sudah benar, dan keduanya verison pipdan pythondiverifikasi benar.
ThoseKind
2
Hei @ np8, berfungsi, saya tidak sengaja menginstalnya di venv dan di os :) pip listmenunjukkan paket, sambil pip freezemenunjukkan nama-nama aneh jika diinstal dengan flag -e
Grzegorz Krug
3
Menghabiskan sekitar 2 jam mencoba mencari cara untuk membuat impor relatif bekerja, dan jawaban ini adalah yang akhirnya benar-benar melakukan sesuatu yang masuk akal. 👍👍
Graham Lea
43

Berikut adalah alternatif lain yang saya masukkan di atas file Python di testsfolder:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Cenk Alti
sumber
1
+1 benar-benar sederhana dan bekerja dengan sempurna. Anda perlu menambahkan kelas induk ke impor (ex api.api, example.example_two) tapi saya lebih suka begitu.
Evan Plaice
10
Saya pikir itu layak disebut untuk pemula (seperti saya) bahwa di ..sini relatif terhadap direktori Anda mengeksekusi --- bukan direktori yang berisi file tes / contoh. Saya mengeksekusi dari direktori proyek, dan saya membutuhkannya ./. Semoga ini bisa membantu orang lain.
Joshua Detwiler
@ JoshDetwiler, ya absoluely. Aku tidak menyadari itu sebelumnya. Terima kasih.
doak
1
Ini jawaban yang buruk. Meretas jalan bukanlah praktik yang baik; itu memalukan berapa banyak digunakan di dunia python. Salah satu poin utama dari pertanyaan ini adalah untuk melihat bagaimana impor dapat dilakukan sambil menghindari peretasan semacam ini.
jtcotton63
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

Anda tidak perlu dan tidak boleh meretas sys.pathkecuali jika perlu dan dalam hal ini tidak. Menggunakan:

import api.api_key # in tests, examples

Jalankan dari direktori proyek: python -m tests.test_one.

Anda mungkin harus pindah tests(jika mereka belum pernah api) di dalam apidan menjalankan python -m api.testuntuk menjalankan semua tes (dengan asumsi ada __main__.py) atau python -m api.test.test_oneuntuk menjalankan test_onesebagai gantinya.

Anda juga dapat menghapus __init__.pydari examples(ini bukan paket Python) dan menjalankan contoh-contoh dalam virtualenv di mana apidiinstal misalnya, pip install -e .dalam virtualenv akan menginstal apipaket inplace jika Anda memiliki yang tepat setup.py.

jfs
sumber
@Alex jawabannya tidak berasumsi bahwa tes adalah tes API kecuali untuk paragraf di mana dikatakan secara eksplisit "jika mereka adalah api unittests" .
jfs
sayangnya Anda terjebak dengan menjalankan dari direktori root dan PyCharm masih tidak menemukan file untuk fungsi yang bagus
mhstnsc
@ mhstnsc: itu tidak benar. Anda harus dapat menjalankan python -m api.test.test_onedari mana saja ketika virtualenv diaktifkan. Jika Anda tidak dapat mengonfigurasi PyCharm untuk menjalankan tes Anda, cobalah untuk mengajukan pertanyaan Stack Overflow baru (jika Anda tidak dapat menemukan pertanyaan yang ada tentang topik ini).
jfs
@ jfs Saya ketinggalan jalur env virtual tetapi saya tidak ingin menggunakan apa pun selain garis shebang untuk menjalankan hal ini dari direktori mana pun. Ini bukan tentang menjalankan dengan PyCharm. Pengembang dengan PyCharm akan tahu juga mereka memiliki fungsi penyelesaian dan melompat melalui yang saya tidak bisa membuatnya bekerja dengan solusi apa pun.
mhstnsc
@ mhstnsc shebang yang tepat sudah cukup dalam banyak kasus (arahkan ke virtualenv python binary. Setiap Python IDE yang layak harus mendukung virtualenv.
jfs
9

Saya belum memiliki pemahaman tentang Pythonology yang diperlukan untuk melihat cara berbagi kode yang dimaksud di antara proyek-proyek yang tidak terkait tanpa saudara / impor relatif hack. Sampai hari itu, ini solusi saya. Untuk examplesatau testsmengimpor barang dari ..\api, akan terlihat seperti:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
pengguna1330131
sumber
Ini masih akan memberi Anda direktori induk api dan Anda tidak akan memerlukan "/ .." concatenation sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( file )))) )
Camilo Sanchez
4

Untuk impor paket saudara, Anda dapat menggunakan sisipan atau metode append dari modul [sys.path] [2] :

if __name__ == '__main__' and if __package__ is None:
    import sys
    from os import path
    sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
    import api

Ini akan berfungsi jika Anda meluncurkan skrip Anda sebagai berikut:

python examples/example_one.py
python tests/test_one.py

Di sisi lain, Anda juga dapat menggunakan impor relatif:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

Dalam hal ini Anda harus meluncurkan skrip Anda dengan argumen '-m' (perhatikan bahwa, dalam hal ini, Anda tidak boleh memberikan ekstensi '.py' ):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

Tentu saja, Anda dapat mencampur kedua pendekatan tersebut, sehingga skrip Anda akan berfungsi tidak peduli bagaimana namanya:

if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
        import api
    else:
        import ..api.api
Paolo Rovelli
sumber
Saya menggunakan kerangka Klik yang tidak memiliki __file__global jadi saya harus menggunakan yang berikut: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Tapi itu berfungsi di direktori mana pun sekarang
GammaGames
3

TLDR

Metode ini tidak memerlukan setuptools, hack path, argumen baris perintah tambahan, atau menentukan tingkat atas paket di setiap file proyek Anda.

Buat saja skrip di direktori induk apa pun yang Anda panggil untuk menjadi milik Anda __main__dan jalankan semuanya dari sana. Untuk penjelasan lebih lanjut lanjutkan membaca.

Penjelasan

Ini dapat dilakukan tanpa meretas jalur baru bersama, argumen baris perintah tambahan, atau menambahkan kode ke setiap program Anda untuk mengenali saudara kandungnya.

Alasan ini gagal karena saya percaya disebutkan sebelumnya adalah program yang dipanggil telah __name__ditetapkan sebagai __main__. Ketika ini terjadi, skrip yang dipanggil menerima dirinya berada di tingkat atas paket dan menolak untuk mengenali skrip dalam direktori saudara.

Namun, segala sesuatu di bawah tingkat atas direktori masih akan mengenali APA SAJA LAIN di bawah tingkat atas. Ini berarti satu- satunya hal yang harus Anda lakukan untuk mendapatkan file dalam direktori saudara untuk mengenali / memanfaatkan satu sama lain adalah memanggil mereka dari skrip di direktori induknya.

Bukti Konsep Dalam sebuah dir dengan struktur berikut:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py berisi kode berikut:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py berisi:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

dan sib2 / callsib.py berisi:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

Jika Anda mereproduksi contoh ini, Anda akan melihat bahwa panggilan Main.pyakan menghasilkan "Got Called" dicetak sebagaimana didefinisikan sib2/callsib.py meskipun sib2/callsib.pydipanggil melalui sib1/call.py. Namun jika seseorang langsung menelepon sib1/call.py(setelah melakukan perubahan yang sesuai dengan impor) itu melempar pengecualian. Meskipun itu berfungsi ketika dipanggil oleh skrip di direktori induknya, itu tidak akan berfungsi jika ia percaya dirinya berada di tingkat atas paket.

Thunderwood
sumber
2

Saya membuat contoh proyek untuk menunjukkan bagaimana saya menangani ini, yang memang merupakan hack sys.path lain seperti yang ditunjukkan di atas. Contoh Impor Sirip Python , yang mengandalkan:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

Ini tampaknya cukup efektif selama direktori kerja Anda tetap pada akar proyek Python. Jika ada yang menyebarkan ini dalam lingkungan produksi nyata, akan lebih baik untuk mendengar jika itu bekerja di sana juga.

ADataGMan
sumber
Ini hanya berfungsi jika Anda menjalankan dari direktori induk skrip
Evpok
1

Anda perlu melihat untuk melihat bagaimana pernyataan impor ditulis dalam kode terkait. Jika examples/example_one.pymenggunakan pernyataan impor berikut:

import api.api

... maka ia mengharapkan direktori root proyek berada di jalur sistem.

Cara termudah untuk mendukung ini tanpa ada peretasan (seperti yang Anda katakan) adalah dengan menjalankan contoh dari direktori tingkat atas, seperti ini:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ.
sumber
Dengan Python 2.7.1 saya mendapatkan berikut: $ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api. Saya juga mendapatkan yang sama import api.api.
zachwill
Diperbarui jawaban saya ... Anda tidak harus menambahkan direktori ke jalur impor, ada jalan itu.
AJ.
1

Untuk berjaga-jaga jika seseorang menggunakan Pydev di Eclipse berakhir di sini: Anda dapat menambahkan jalur induk saudara (dan dengan demikian orangtua modul panggilan) sebagai folder pustaka eksternal menggunakan Project-> Properties dan mengatur Pustaka Eksternal di bawah menu kiri Pydev-PYTHONPATH . Kemudian Anda dapat mengimpor dari saudara Anda, mis from sibling import some_class.

Lord Henry Wotton
sumber
-3

Pertama, Anda harus menghindari memiliki file dengan nama yang sama dengan modul itu sendiri. Ini dapat merusak impor lainnya.

Ketika Anda mengimpor file, pertama penerjemah memeriksa direktori saat ini dan kemudian mencari direktori global.

Di dalam examplesatau testsAnda dapat menelepon:

from ..api import api

sumber
Saya mendapatkan yang berikut dengan Python 2.7.1:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
zachwill
2
Oh, maka Anda harus menambahkan __init__.pyfile ke direktori tingkat atas. Kalau tidak, Python tidak bisa memperlakukannya sebagai modul
8
Itu tidak akan berhasil. Masalahnya bukan bahwa folder induk tidak sebuah paket, adalah bahwa karena modul ini __name__adalah __main__bukan package.module, Python tidak bisa melihat paket induknya, sehingga .poin apa-apa.
Evpok