คำอธิบาย
จาก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คือจำนวนของพาเรนต์ไดเร็กทอรี (สัมพันธ์กับไดเร็กทอรีของสคริปต์) ที่จะค้นหาโมดูลที่กำลังอิมพอร์ต
ดังนั้น,
เพิ่มไดเรกทอรีหลักของรุ่นก่อนNthของโมดูลปัจจุบันไปsys.path
ลบไดเรกทอรีของไฟล์ปัจจุบันออก sys.path
นำเข้าโมดูลหลักของโมดูลปัจจุบันโดยใช้ชื่อที่ผ่านการรับรอง
ตั้งค่า__package__
เป็นชื่อแบบเต็มจาก2
ดำเนินการนำเข้าที่เกี่ยวข้อง
ฉันจะยืมไฟล์จากโซลูชัน # 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
ขั้นตอนคือ -
แทนที่การนำเข้าที่สัมพันธ์อย่างชัดเจนด้วยการนำเข้าสัมบูรณ์เทียบเท่า
ติดตั้ง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 :
เพิ่มไดเรกทอรีหลักของแพคเกจไป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
แทนที่การนำเข้าแบบสัมพัทธ์ด้วยการนำเข้าแบบสัมบูรณ์:
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แนะนำให้ใช้การนำเข้าแบบสัมบูรณ์ แต่ระบุว่าในบางสถานการณ์การนำเข้าที่สัมพันธ์กันอย่างชัดเจนเป็นที่ยอมรับ:
แนะนำให้ใช้การนำเข้าแบบสัมบูรณ์เนื่องจากสามารถอ่านได้มากขึ้นและมีแนวโน้มที่จะประพฤติตัวดีขึ้น (หรืออย่างน้อยก็ให้ข้อความแสดงข้อผิดพลาดที่ดีกว่า) [... ] อย่างไรก็ตามการนำเข้าแบบสัมพัทธ์ที่ชัดเจนนั้นเป็นทางเลือกที่ยอมรับได้สำหรับการนำเข้าแบบสัมบูรณ์โดยเฉพาะอย่างยิ่งเมื่อต้องรับมือกับรูปแบบแพคเกจที่ซับซ้อน