การนำเข้าสัมพัทธ์ใน Python 3


713

ฉันต้องการนำเข้าฟังก์ชั่นจากไฟล์อื่นในไดเรกทอรีเดียวกัน

บางครั้งมันก็ใช้ได้สำหรับฉันด้วยfrom .mymodule import myfunctionแต่บางครั้งฉันก็:

SystemError: Parent module '' not loaded, cannot perform relative import

บางครั้งก็ใช้งานได้from mymodule import myfunctionแต่บางครั้งฉันก็มี:

SystemError: Parent module '' not loaded, cannot perform relative import

ฉันไม่เข้าใจตรรกะที่นี่และฉันไม่สามารถหาคำอธิบายใด ๆ มันดูสุ่มอย่างสมบูรณ์

มีคนอธิบายให้ฉันฟังหน่อยว่าตรรกะอะไรที่อยู่เบื้องหลังทั้งหมดนี้


76
ซึ่งหมายความว่าคุณกำลังเรียกใช้โมดูลภายในแพ็คเกจเป็นสคริปต์ เรียกใช้สคริปต์จากภายนอกแพ็คเกจเท่านั้น
Martijn Pieters

3
อาจคุณควรกำหนดเงื่อนไขที่คุณมี 'บางครั้ง' ที่คุณพูดถึง ฉันเข้าใจว่าคุณไม่ได้หมายความว่าคุณมีข้อผิดพลาดแบบสุ่ม
joaquin

15
@MartijnPieters: น่าเสียดายที่โมดูลนี้ต้องอยู่ในแพคเกจและมันก็จำเป็นต้องเรียกใช้เป็นสคริปต์ได้ด้วยบางครั้ง ความคิดใดที่ฉันสามารถบรรลุเป้าหมายนั้นได้?
John Smith ทางเลือก

22
@JohnSmithOptional: การผสมสคริปต์ภายในแพ็คเกจนั้นยุ่งยากและควรหลีกเลี่ยงหากเป็นไปได้ ใช้สคริปต์ตัวตัดคำที่นำเข้าแพ็คเกจและเรียกใช้ฟังก์ชัน 'scripty' แทน
Martijn Pieters

3
ดูเหมือนว่าโชคร้าย ฉันสร้างโมดูลหลักพร้อมคลาส / วิธีการที่สามารถแยก / วิเคราะห์ไฟล์บางประเภทและฉันยังมีโมดูลรองและสคริปต์แยกส่วน (ซึ่งส่วนใหญ่เป็นตัวฉันเอง) ซึ่งนำเข้า - เหล่านี้สามารถนวด / แปลงไฟล์เหล่านั้นได้ แต่ฉันก็อยากที่จะมอบไฟล์แกนเดี่ยวนั้น (ไม่ใช่แพ็คเกจที่ซับซ้อนทั้งหมด) ให้กับผู้ใช้เพื่อให้พวกเขาสามารถวางไว้ถัดจากไฟล์และเรียกใช้ ใน "โหมดสคริปต์" มันจะแยกวิเคราะห์และวิเคราะห์ไฟล์และการเข้ารหัสซึ่งจะนับฟิลด์ / ค่า / อักขระพิเศษต่าง ๆ และให้รายงาน แต่มันไม่ได้แก้ไขไฟล์จริงๆ ต่อต้านรูปแบบ?
Jon Coombs

คำตอบ:


527

โชคไม่ดีที่โมดูลนี้ต้องอยู่ในแพ็คเกจและจำเป็นต้องเรียกใช้เป็นสคริปต์ในบางครั้ง ความคิดใดที่ฉันสามารถบรรลุผลนั้นได้?

มันค่อนข้างธรรมดาที่จะมีเลย์เอาต์เช่นนี้ ...

main.py
mypackage/
    __init__.py
    mymodule.py
    myothermodule.py

... ด้วยสิ่งmymodule.pyนี้ ...

#!/usr/bin/env python3

# Exported function
def as_int(a):
    return int(a)

# Test function for module  
def _test():
    assert as_int('1') == 1

if __name__ == '__main__':
    _test()

... myothermodule.pyเช่นนี้ ...

#!/usr/bin/env python3

from .mymodule import as_int

# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __name__ == '__main__':
    _test()

... และสิ่งmain.pyนี้ ...

#!/usr/bin/env python3

from mypackage.myothermodule import add

def main():
    print(add('1', '1'))

if __name__ == '__main__':
    main()

... ซึ่งทำงานได้ดีเมื่อคุณเรียกใช้main.pyหรือmypackage/mymodule.pyล้มเหลวด้วยmypackage/myothermodule.pyเนื่องจากการนำเข้าที่เกี่ยวข้อง ...

from .mymodule import as_int

วิธีที่คุณควรจะรันมันคือ ...

python3 -m mypackage.myothermodule

... แต่มันเป็นเรื่องที่ค่อนข้างละเอียดและไม่ผสมให้เข้ากันกับสาย shebang #!/usr/bin/env python3เช่น

การแก้ไขที่ง่ายที่สุดสำหรับกรณีนี้สมมติว่าชื่อmymoduleนั้นมีความเป็นสากลไม่ควรใช้การนำเข้าแบบสัมพัทธ์และเพียงแค่ใช้ ...

from mymodule import as_int

... แม้ว่าจะไม่ซ้ำกันหรือโครงสร้างแพ็คเกจของคุณซับซ้อนกว่าคุณจะต้องรวมไดเรกทอรีที่มีไดเรกทอรีแพ็คเกจของคุณPYTHONPATHและทำเช่นนี้ ...

from mypackage.mymodule import as_int

... หรือถ้าคุณต้องการให้มันทำงานนอกกรอบคุณสามารถใส่PYTHONPATHรหัสลงไปก่อนด้วย ...

import sys
import os

PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__))))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))

from mypackage.mymodule import as_int

มันช่างเป็นความเจ็บปวด แต่มีเงื่อนงำว่าทำไมในอีเมลที่เขียนโดย Guido van Rossum บางคน ...

ฉัน -1 ในเรื่องนี้และใน twiddlings อื่น ๆ ที่เสนอของ__main__ เครื่องจักร กรณีใช้งานเพียงอย่างเดียวดูเหมือนว่ากำลังเรียกใช้สคริปต์ที่เกิดขึ้นว่าอยู่ในไดเรกทอรีของโมดูลซึ่งฉันเคยเห็นในฐานะ Antipattern ที่จะทำให้ฉันเปลี่ยนใจคุณต้องโน้มน้าวฉันว่าไม่ใช่

ไม่ว่าจะทำงานสคริปต์ภายในแพคเกจเป็น antipattern หรือไม่เป็นอัตนัย แต่ส่วนตัวผมพบว่ามันมีประโยชน์จริงๆในแพคเกจผมมีซึ่งมีเครื่องมือ wxPython กำหนดเองบางส่วนเพื่อให้สามารถเรียกใช้สคริปต์ใด ๆ ของไฟล์ที่มาในการแสดงwx.Frameที่มีเพียง วิดเจ็ตนั้นสำหรับวัตถุประสงค์ในการทดสอบ


7
วิธีที่ดีกว่าที่จะได้รับ SCRIPTDIR จะได้รับในความคิดเห็นของนำเข้าโมดูลจากทางญาติเป็นos.path.realpath(os.path.dirname(inspect.getfile(inspect.currentframe())))ถ้าคุณมั่นใจว่าโมดูลของคุณมีเสมอที่เหมาะสมคุณยังสามารถใช้file os.path.realpath(os.path.dirname(__file__))
marcz

2
คุณสามารถขยาย PYTHONPATH ของคุณโดยใช้ข้อมูลโค้ดที่สั้นและอ่านได้มากขึ้น: sys.path.append( os.path.join( os.path.dirname(__file__), os.path.pardir ) )
Alex-Bogdanov

12
...which I've always seen as an antipattern.ฉันไม่เห็นว่ามันเป็นรูปแบบการต่อต้าน ... ดูเหมือนว่ามันจะสะดวกสุด ๆ เพียงแค่ให้การนำเข้าแบบสัมพัทธ์ทำงานโดยสังหรณ์ใจ ฉันต้องการนำเข้าสิ่งที่ฉันรู้ว่าอยู่ในไดเรกทอรีเดียวกัน ฉันสงสัยว่าเหตุผลของเขาคืออะไร
YungGun

9
กุยโด้ประท้วงอีกครั้ง: สิ่งที่เป็นสิ่งที่อาจเป็นประโยชน์ นั่นจะไม่เกิดขึ้นอีกแล้ว
javadba

4
นี่คือสิ่งที่น่าเศร้าที่สุดที่ฉันเคยเห็นเกี่ยวกับ Python
AtilioA

263

คำอธิบาย

จากPEP 328

การอิมพอร์ตสัมพัทธ์ใช้แอ็ตทริบิวต์ __name__ ของโมดูลเพื่อกำหนดตำแหน่งของโมดูลนั้นในลำดับชั้นของแพ็กเกจ หากชื่อของโมดูลไม่มีข้อมูลแพ็กเกจใด ๆ (เช่นถูกตั้งค่าเป็น '__main__') การ อิมพอร์ตสัมพัทธ์จะได้รับการแก้ไขราวกับว่าโมดูลนั้นเป็นโมดูลระดับบนสุดไม่ว่าโมดูลนั้นจะอยู่ที่ใดในระบบไฟล์

ณ จุดหนึ่งPEP 338ขัดแย้งกับPEP 328 :

... การนำเข้าสัมพัทธ์พึ่งพา__name__เพื่อกำหนดตำแหน่งของโมดูลปัจจุบันในลำดับชั้นของแพ็คเกจ ในโมดูลหลักค่าของ__name__เสมอ'__main__'ดังนั้นการนำเข้าที่เกี่ยวข้องอย่างชัดเจนจะล้มเหลวเสมอ (เนื่องจากทำงานเฉพาะกับโมดูลที่อยู่ในแพ็คเกจ)

และเพื่อแก้ไขปัญหาPEP 366แนะนำตัวแปรระดับบนสุด__package__:

ด้วยการเพิ่มแอททริบิวระดับโมดูลใหม่ PEP นี้จะช่วยให้การนำเข้าที่เกี่ยวข้องสามารถทำงานได้โดยอัตโนมัติหากดำเนินการโมดูลโดยใช้ สวิตช์-m จำนวนสำเร็จรูปขนาดเล็กในโมดูลตัวเองจะช่วยให้การนำเข้าที่เกี่ยวข้องในการทำงานเมื่อไฟล์จะถูกดำเนินการตามชื่อ [... ] เมื่อมัน [แอตทริบิวต์] มีอยู่การนำเข้าที่สัมพันธ์กันจะขึ้นอยู่กับคุณลักษณะนี้มากกว่าแอตทริบิวต์__name__โมดูล [ ... ] เมื่อโมดูลหลักมีการระบุโดยชื่อของมันแล้ว__package__แอตทริบิวต์จะถูกกำหนดให้ไม่มี [... ] เมื่อระบบนำเข้าพบการนำเข้าแบบสัมพัทธ์อย่างชัดเจนในโมดูลโดยไม่มีชุด __package__ (หรือตั้งค่าเป็นไม่มี) ระบบจะคำนวณและจัดเก็บค่าที่ถูกต้อง ( __name __. rpartition ('.') [0] สำหรับโมดูลปกติและ__name__สำหรับโมดูลการเริ่มต้นแพ็กเกจ

(เน้นที่เหมือง)

ถ้า__name__เป็น'__main__', __name__.rpartition('.')[0]ส่งกลับสตริงที่ว่างเปล่า นี่คือเหตุผลที่มีสตริงตัวอักษรว่างเปล่าในคำอธิบายข้อผิดพลาด:

SystemError: Parent module '' not loaded, cannot perform relative import

ส่วนที่เกี่ยวข้องของPyImport_ImportModuleLevelObjectฟังก์ชั่น CPython :

if (PyDict_GetItem(interp->modules, package) == NULL) {
    PyErr_Format(PyExc_SystemError,
            "Parent module %R not loaded, cannot perform relative "
            "import", package);
    goto error;
}

CPython ยกข้อยกเว้นนี้หากไม่สามารถหาpackage(ชื่อของแพ็คเกจ) ในinterp->modules(เข้าถึงได้เป็นsys.modules) ตั้งแต่sys.modulesเป็น"พจนานุกรมที่แมปชื่อโมดูลโมดูลที่ได้รับการโหลดแล้ว"ก็ตอนนี้ชัดเจนว่าโมดูลผู้ปกครองจะต้องชัดเจนแน่นอนนำเข้าก่อนที่จะดำเนินญาตินำเข้า

หมายเหตุ: โปรแกรมแก้ไขจากปัญหา 18018ได้เพิ่มบล็อกอื่นifซึ่งจะดำเนินการก่อนรหัสด้านบน:

if (PyUnicode_CompareWithASCIIString(package, "") == 0) {
    PyErr_SetString(PyExc_ImportError,
            "attempted relative import with no known parent package");
    goto error;
} /* else if (PyDict_GetItem(interp->modules, package) == NULL) {
    ...
*/

ถ้าpackage(เหมือนข้างบน) เป็นสตริงว่างข้อความแสดงข้อผิดพลาดจะเป็น

ImportError: attempted relative import with no known parent package

อย่างไรก็ตามคุณจะเห็นสิ่งนี้ใน Python 3.6 หรือใหม่กว่า

โซลูชัน # 1: เรียกใช้สคริปต์ของคุณโดยใช้ -m

พิจารณาไดเรกทอรี (ซึ่งเป็นแพ็คเกจ Python ):

.
├── package
│   ├── __init__.py
│   ├── module.py
│   └── standalone.py

ไฟล์ทั้งหมดในแพ็คเกจเริ่มต้นด้วยรหัส 2 บรรทัดเดียวกัน:

from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())

ฉันรวมสองบรรทัดนี้เท่านั้นเพื่อให้ลำดับการดำเนินการชัดเจน เราสามารถเพิกเฉยได้อย่างสมบูรณ์เนื่องจากไม่มีผลต่อการดำเนินการ

__init__.pyและmodule.pyมีเพียงสองบรรทัดเท่านั้น (กล่าวคือว่างเปล่าอย่างมีประสิทธิภาพ)

standalone.pyพยายามเพิ่มเติมนำเข้าmodule.pyผ่านการนำเข้าแบบสัมพัทธ์:

from . import module  # explicit relative import

เราตระหนักดีว่า/path/to/python/interpreter package/standalone.pyจะล้มเหลว อย่างไรก็ตามเราสามารถเรียกใช้โมดูลด้วย-mตัวเลือกบรรทัดคำสั่งที่จะ"ค้นหาsys.pathโมดูลที่มีชื่อและดำเนินการเนื้อหาเป็น__main__โมดูล" :

vaultah@base:~$ python3 -i -m package.standalone
Importing /home/vaultah/package/__init__.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/module.py
>>> __file__
'/home/vaultah/package/standalone.py'
>>> __package__
'package'
>>> # The __package__ has been correctly set and module.py has been imported.
... # What's inside sys.modules?
... import sys
>>> sys.modules['__main__']
<module 'package.standalone' from '/home/vaultah/package/standalone.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>

-mทำสิ่งที่นำเข้าทั้งหมดสำหรับคุณและตั้งค่าโดยอัตโนมัติ__package__แต่คุณสามารถทำได้ด้วยตัวคุณเองใน

โซลูชัน # 2: ตั้งค่า __ แพ็คเกจด้วยตนเอง

โปรดปฏิบัติต่อมันเป็นข้อพิสูจน์แนวคิดแทนที่จะเป็นทางออกที่แท้จริง มันไม่เหมาะสำหรับใช้ในรหัสโลกแห่งความจริง

PEP 366มีวิธีแก้ไขปัญหานี้อย่างไรก็ตามมันไม่สมบูรณ์เนื่องจากการตั้งค่า__package__เพียงอย่างเดียวไม่เพียงพอ คุณจะต้องนำเข้าอย่างน้อยNแพ็กเกจก่อนหน้านี้ในลำดับชั้นของโมดูลโดยที่Nคือจำนวนของพาเรนต์ไดเร็กทอรี (สัมพันธ์กับไดเร็กทอรีของสคริปต์) ที่จะค้นหาโมดูลที่กำลังอิมพอร์ต

ดังนั้น,

  1. เพิ่มไดเรกทอรีหลักของรุ่นก่อนNthของโมดูลปัจจุบันไปsys.path

  2. ลบไดเรกทอรีของไฟล์ปัจจุบันออก sys.path

  3. นำเข้าโมดูลหลักของโมดูลปัจจุบันโดยใช้ชื่อที่ผ่านการรับรอง

  4. ตั้งค่า__package__เป็นชื่อแบบเต็มจาก2

  5. ดำเนินการนำเข้าที่เกี่ยวข้อง

ฉันจะยืมไฟล์จากโซลูชัน # 1และเพิ่มแพ็คเกจย่อยเพิ่มเติม:

package
├── __init__.py
├── module.py
└── subpackage
    ├── __init__.py
    └── subsubpackage
        ├── __init__.py
        └── standalone.py

เวลานี้standalone.pyจะนำเข้าmodule.pyจากแพ็คเกจแพ็กเกจโดยใช้การนำเข้าที่เกี่ยวข้องดังต่อไปนี้

from ... import module  # N = 3

เราจะต้องนำหน้าบรรทัดนั้นด้วยรหัสสำเร็จรูปเพื่อให้มันทำงานได้

import sys
from pathlib import Path

if __name__ == '__main__' and __package__ is None:
    file = Path(__file__).resolve()
    parent, top = file.parent, file.parents[3]

    sys.path.append(str(top))
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass

    import package.subpackage.subsubpackage
    __package__ = 'package.subpackage.subsubpackage'

from ... import module # N = 3

ช่วยให้เราสามารถรันstandalone.pyโดยชื่อไฟล์:

vaultah@base:~$ python3 package/subpackage/subsubpackage/standalone.py
Running /home/vaultah/package/subpackage/subsubpackage/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/subpackage/__init__.py
Importing /home/vaultah/package/subpackage/subsubpackage/__init__.py
Importing /home/vaultah/package/module.py

วิธีการแก้ปัญหาทั่วไปมากขึ้นห่อในฟังก์ชั่นที่สามารถพบได้ที่นี่ ตัวอย่างการใช้งาน:

if __name__ == '__main__' and __package__ is None:
    import_parents(level=3) # N = 3

from ... import module
from ...module.submodule import thing

โซลูชัน # 3: ใช้การนำเข้าและsetuptools แบบสัมบูรณ์

ขั้นตอนคือ -

  1. แทนที่การนำเข้าที่สัมพันธ์อย่างชัดเจนด้วยการนำเข้าสัมบูรณ์เทียบเท่า

  2. ติดตั้งpackageเพื่อให้สามารถนำเข้าได้

ตัวอย่างเช่นโครงสร้างไดเรกทอรีอาจเป็นดังนี้

.
├── project
│   ├── package
│   │   ├── __init__.py
│   │   ├── module.py
│   │   └── standalone.py
│   └── setup.py

ที่setup.pyอยู่

from setuptools import setup, find_packages
setup(
    name = 'your_package_name',
    packages = find_packages(),
)

ส่วนที่เหลือของไฟล์ที่ถูกยืมมาจากโซลูชั่น # 1

การติดตั้งจะช่วยให้คุณสามารถนำเข้าแพคเกจโดยไม่คำนึงถึงไดเรกทอรีการทำงานของคุณ (สมมติว่าจะไม่มีปัญหาการตั้งชื่อ)

เราสามารถแก้ไขstandalone.pyเพื่อใช้ประโยชน์นี้ (ขั้นตอนที่ 1):

from package import module  # absolute import

เปลี่ยนไดเรกทอรีทำงานของคุณเป็นprojectและเรียกใช้/path/to/python/interpreter setup.py install --user( --userติดตั้งแพคเกจในไดเรกทอรีไซต์แพ็คเกจของคุณ ) (ขั้นตอนที่ 2):

vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user

มาตรวจสอบกันว่าตอนนี้เป็นไปได้ที่จะเรียกใช้standalone.pyเป็นสคริปต์:

vaultah@base:~/project$ python3 -i package/standalone.py
Running /home/vaultah/project/package/standalone.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py
Importing /home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/.local/lib/python3.6/site-packages/your_package_name-0.0.0-py3.6.egg/package/module.py'>

หมายเหตุ : หากคุณตัดสินใจที่จะลงเส้นทางนี้คุณควรใช้สภาพแวดล้อมเสมือนจริงเพื่อติดตั้งแพ็คเกจแยก

โซลูชัน # 4: ใช้การนำเข้าแบบสัมบูรณ์และรหัสสำเร็จรูปบางอย่าง

ไม่จำเป็นต้องติดตั้ง - คุณสามารถเพิ่มรหัสสำเร็จรูปลงในสคริปต์ของคุณเพื่อให้การนำเข้าแบบสัมบูรณ์ทำงานได้

ฉันจะยืมไฟล์จากโซลูชัน # 1และเปลี่ยนstandalone.py :

  1. เพิ่มไดเรกทอรีหลักของแพคเกจไปsys.path ก่อนที่จะพยายามที่จะอะไรที่นำเข้าจากแพคเกจการใช้การนำเข้าแน่นอน:

    import sys
    from pathlib import Path # if you haven't already done so
    file = Path(__file__).resolve()
    parent, root = file.parent, file.parents[1]
    sys.path.append(str(root))
    
    # Additionally remove the current file's directory from sys.path
    try:
        sys.path.remove(str(parent))
    except ValueError: # Already removed
        pass
  2. แทนที่การนำเข้าแบบสัมพัทธ์ด้วยการนำเข้าแบบสัมบูรณ์:

    from package import module  # absolute import

standalone.pyทำงานโดยไม่มีปัญหา:

vaultah@base:~$ python3 -i package/standalone.py
Running /home/vaultah/package/standalone.py
Importing /home/vaultah/package/__init__.py
Importing /home/vaultah/package/module.py
>>> module
<module 'package.module' from '/home/vaultah/package/module.py'>
>>> import sys
>>> sys.modules['package']
<module 'package' from '/home/vaultah/package/__init__.py'>
>>> sys.modules['package.module']
<module 'package.module' from '/home/vaultah/package/module.py'>

ฉันรู้สึกว่าฉันควรเตือนคุณ: พยายามอย่าทำเช่นนี้โดยเฉพาะถ้าโครงการของคุณมีโครงสร้างที่ซับซ้อน


ตามหมายเหตุด้านข้างPEP 8แนะนำให้ใช้การนำเข้าแบบสัมบูรณ์ แต่ระบุว่าในบางสถานการณ์การนำเข้าที่สัมพันธ์กันอย่างชัดเจนเป็นที่ยอมรับ:

แนะนำให้ใช้การนำเข้าแบบสัมบูรณ์เนื่องจากสามารถอ่านได้มากขึ้นและมีแนวโน้มที่จะประพฤติตัวดีขึ้น (หรืออย่างน้อยก็ให้ข้อความแสดงข้อผิดพลาดที่ดีกว่า) [... ] อย่างไรก็ตามการนำเข้าแบบสัมพัทธ์ที่ชัดเจนนั้นเป็นทางเลือกที่ยอมรับได้สำหรับการนำเข้าแบบสัมบูรณ์โดยเฉพาะอย่างยิ่งเมื่อต้องรับมือกับรูปแบบแพคเกจที่ซับซ้อน


3
เป็นไปได้หรือไม่ที่จะตั้งค่า__package__ด้วยตนเองหากชื่อ__main__เพื่อแก้ไขปัญหา?
เปาโล Scardine

ขอบคุณคำตอบที่ดี! ฉันสามารถโหลดโมดูลโดยใช้impโมดูลและตั้งค่า__package__ตามความเหมาะสม แต่ผลลัพธ์เป็นรูปแบบการต่อต้านอย่างชัดเจน
เปาโล Scardine

AttributeError: 'PosixPath' object has no attribute 'path'ฉันได้รับข้อผิดพลาด
ผู้ใช้

ขอบคุณสำหรับการตอบกลับอย่างรวดเร็ว ฉันใช้แพ็คเกจ nltk ฉันได้รับข้อผิดพลาด: `ไฟล์" /usr/local/lib/python3.5/dist-packages/nltk/__init__.py "บรรทัดที่ 115 ใน <โมดูล> จาก nltk.decorators นำเข้ามัณฑนากรจำไฟล์ "/usr/local/lib/python3.5/dist-packages/nltk/decorators.py" บรรทัดที่ 23 ใน <module> sys.path = [p สำหรับ p ใน sys.path ถ้า "nltk "ไม่ได้อยู่ใน p] ไฟล์" /usr/local/lib/python3.5/dist-packages/nltk/decorators.py ", บรรทัดที่ 23 ใน <listcomp> sys.path = [p สำหรับ p ใน sys.path ถ้า" nltk "ไม่อยู่ใน p] TypeError: อาร์กิวเมนต์ประเภท 'PosixPath' ไม่สามารถทำการซ้ำได้`
ผู้ใช้

1
นอกจากนี้คุณยังสามารถนำเข้าไฟล์ตามเส้นทางของไฟล์ (ที่เกี่ยวข้องเช่นกัน): docs.python.org/3/library/…
Ctrl-C

85

วางสิ่งนี้ไว้ในไฟล์ __init__.py ของแพ็คเกจของคุณ :

# For relative imports to work in Python 3.6
import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))

สมมติว่าแพ็คเกจของคุณเป็นเช่นนี้:

├── project
   ├── package
      ├── __init__.py
      ├── module1.py
      └── module2.py
   └── setup.py

ตอนนี้ใช้การนำเข้าปกติในแพ็คเกจของคุณเช่น:

# in module2.py
from module1 import class1

ใช้ได้ทั้ง python 2 และ 3


1
ใช้งานได้ไหมถ้าเราบรรจุมันเป็น Weel
Alex Punnen

1
ฉันคิดว่าสิ่งนี้สมควรได้รับคะแนนเสียงมากกว่านี้ การใส่สิ่งนี้ลงในทุก ๆ__init__.pyโดยทั่วไปจะแก้ไขข้อผิดพลาดการนำเข้าที่เกี่ยวข้องทั้งหมด
frankliuao

3
ฉันไม่สามารถพูดกับคนอื่นได้ แต่ฉันมักจะหลีกเลี่ยงการแก้ไขsys.pathเพราะฉันกังวลว่าอาจส่งผลกระทบต่อรหัสอื่น (บางส่วนนี้เป็นเพราะผมไม่ทราบความซับซ้อนของวิธีการทำงาน.)
pianoJames

@pianoJames ฉันรู้ว่าสิ่งที่คุณหมายถึงนี้แก้ไขมายากล (ดูเหมือนหลังจากที่รอบรู้มากรอบ) ดูเหมือนจะง่ายเกินไปเล็กน้อย แต่มันได้ผล จะสนใจไม่ทราบจากผู้ที่รู้ว่าถ้ามันมีผลข้างเคียงเชิงลบ
Jon

ฉันใช้สิ่งนี้ในตอนนี้และดีมาก
javadba

37

ฉันพบปัญหานี้ วิธีแก้ปัญหาการแฮ็กกำลังนำเข้าผ่านบล็อก if / else ดังนี้:

#!/usr/bin/env python3
#myothermodule

if __name__ == '__main__':
    from mymodule import as_int
else:
    from .mymodule import as_int


# Exported function
def add(a, b):
    return as_int(a) + as_int(b)

# Test function for module  
def _test():
    assert add('1', '1') == 2

if __name__ == '__main__':
    _test()

29
นั่นไม่ใช่ทางออกที่ดีมาก ยังเปลือยexcept:ไม่ดี ใช้except ImportError:แทน!
ThiefMaster

6
มันอยู่SystemErrorที่นี่ (Py 3.4)
Avi

8
นี่ไม่ใช่ความคิดที่แย่ แต่ควรตรวจสอบว่าการนำเข้าใดที่จะใช้แทนที่จะลอง / ยกเว้น if __name__ == '__main__': from mymod import as_int; else: from .mymod import as_intสิ่งที่ชอบ
Perkins

@Perkins ดี ... ในกรณีส่วนใหญ่มัน จะไม่ได้ ฉันคิดว่าการนำเข้าที่เกี่ยวข้องอาจเป็นข้อยกเว้น
wizzwizz4

8

หวังว่านี่จะเป็นสิ่งที่มีค่าสำหรับใครบางคนในนั้น - ฉันได้ผ่านโพสต์สแต็คโอเวอร์โฟลว์จำนวนครึ่งโหลที่พยายามคิดการนำเข้าแบบสัมพัทธ์คล้ายกับอะไรที่โพสต์ไว้ที่นี่ ฉันตั้งค่าทุกอย่างตามที่แนะนำ แต่ฉันยังกดปุ่มอยู่ModuleNotFoundError: No module named 'my_module_name'

ตั้งแต่ฉันเพิ่งพัฒนาในประเทศและเล่นไปรอบ ๆ ฉันไม่ได้สร้าง / เรียกใช้setup.pyไฟล์ PYTHONPATHฉันยังไม่ได้ตั้งที่เห็นได้ชัดของฉัน

ฉันรู้ว่าเมื่อฉันรันโค้ดเหมือนเมื่อตอนที่การทดสอบอยู่ในไดเรกทอรีเดียวกับโมดูลฉันไม่พบโมดูลของฉัน:

$ python3 test/my_module/module_test.py                                                                                                               2.4.0
Traceback (most recent call last):
  File "test/my_module/module_test.py", line 6, in <module>
    from my_module.module import *
ModuleNotFoundError: No module named 'my_module'

อย่างไรก็ตามเมื่อฉันระบุเส้นทางสิ่งต่าง ๆ เริ่มทำงานอย่างชัดเจน:

$ PYTHONPATH=. python3 test/my_module/module_test.py                                                                                                  2.4.0
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

ดังนั้นในกรณีที่ทุกคนลองใช้คำแนะนำเล็กน้อยเชื่อว่ารหัสของพวกเขามีโครงสร้างอย่างถูกต้องและยังพบว่าตัวเองอยู่ในสถานการณ์ที่คล้ายกันเนื่องจากฉันลองทำสิ่งใดสิ่งหนึ่งต่อไปนี้หากคุณไม่ส่งออกไดเรกทอรีปัจจุบันไปยัง PYTHONPATH ของคุณ

  1. เรียกใช้รหัสของคุณและระบุเส้นทางดังนี้: $ PYTHONPATH=. python3 test/my_module/module_test.py
  2. เพื่อหลีกเลี่ยงการโทรPYTHONPATH=.สร้างsetup.pyไฟล์ที่มีเนื้อหาเช่นต่อไปนี้และเรียกใช้python setup.py developmentเพื่อเพิ่มแพ็คเกจไปยังเส้นทาง:
# setup.py
from setuptools import setup, find_packages

setup(
    name='sample',
    packages=find_packages()
)

6

ฉันต้องการเรียกใช้ python3 จากไดเรกทอรีโครงการหลักเพื่อให้ทำงานได้

ตัวอย่างเช่นถ้าโครงการมีโครงสร้างต่อไปนี้:

project_demo/
├── main.py
├── some_package/
   ├── __init__.py
   └── project_configs.py
└── test/
    └── test_project_configs.py

สารละลาย

ฉันจะเรียกใช้ python3 ในโฟลเดอร์project_demo /และดำเนินการ

from some_package import project_configs

4

เพื่อหลีกเลี่ยงปัญหานี้ฉันคิดวิธีแก้ปัญหาด้วยแพคเกจrepackageซึ่งทำงานให้ฉันมาระยะหนึ่งแล้ว มันเพิ่มไดเรกทอรีบนไปยังเส้นทาง lib:

import repackage
repackage.up()
from mypackage.mymodule import myfunction

การทำแพ็กเกจใหม่สามารถทำการอิมพอร์ตสัมพัทธ์ที่ทำงานในหลากหลายกรณีโดยใช้กลยุทธ์อัจฉริยะ (ตรวจสอบ call stack)


ทางออกที่ง่ายที่สุด! ขอบคุณ!
CodingInCircles

1
ขอบคุณ! แทนที่จะพยายามให้คำตอบที่ดีที่สุดฉันพยายามให้คำตอบที่ใช้ได้ :-)
fralau

3

หากแพ็กเกจทั้งสองอยู่ในพา ธ การนำเข้าของคุณ (sys.path) และโมดูล / คลาสที่คุณต้องการอยู่ในตัวอย่าง / example.py จากนั้นเข้าถึงคลาสโดยไม่ลองนำเข้าแบบสัมพัทธ์:

from example.example import fkt

1

ฉันคิดว่าทางออกที่ดีที่สุดคือการสร้างแพ็คเกจสำหรับโมดูลของคุณ: นี่คือข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำ

เมื่อคุณมีแพ็คเกจที่คุณไม่ต้องกังวลกับการนำเข้าแบบสัมพัทธ์คุณก็สามารถนำเข้าแบบสัมบูรณ์ได้


0

ฉันมีปัญหาที่คล้ายกัน: ฉันต้องการบริการ Linux และปลั๊กอิน cgi ซึ่งใช้ค่าคงที่ทั่วไปเพื่อร่วมมือ วิธีที่ 'เป็นธรรมชาติ' ในการทำเช่นนี้คือการวางไว้ใน init .py ของแพ็คเกจ แต่ฉันไม่สามารถเริ่มปลั๊กอิน cgi ด้วยพารามิเตอร์ -m

โซลูชันสุดท้ายของฉันคล้ายกับโซลูชัน # 2 ด้านบน:

import sys
import pathlib as p
import importlib

pp = p.Path(sys.argv[0])
pack = pp.resolve().parent

pkg = importlib.import_module('__init__', package=str(pack))

ข้อเสียคือคุณต้องขึ้นต้นค่าคงที่ (หรือฟังก์ชันทั่วไป) ด้วย pkg:

print(pkg.Glob)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.