Giải trình
Từ PEP 328
Nhập khẩu tương đối sử dụng thuộc tính __name__ của mô-đun để xác định vị trí của mô-đun đó trong phân cấp gói. Nếu tên của mô-đun không chứa bất kỳ thông tin gói nào (ví dụ: nó được đặt thành '__main__')
thì việc nhập tương đối được giải quyết như thể mô-đun là mô-đun cấp cao nhất , bất kể mô-đun thực sự nằm ở đâu trên hệ thống tệp.
Tại một số điểm PEP 338 đã xung đột với PEP 328 :
... nhập khẩu tương đối dựa vào __name__ để xác định vị trí của mô-đun hiện tại trong phân cấp gói. Trong một mô-đun chính, giá trị của __name__ luôn là '__main__' , do đó, nhập khẩu tương đối rõ ràng sẽ luôn thất bại (vì chúng chỉ hoạt động cho một mô-đun bên trong một gói)
và để giải quyết vấn đề, PEP 366 đã giới thiệu biến cấp cao nhất __package__
:
Bằng cách thêm thuộc tính cấp mô-đun mới, PEP này cho phép nhập tương đối tự động hoạt động nếu mô-đun được thực thi bằng cách sử dụng công
tắc -m . Một lượng nhỏ mẫu soạn sẵn trong mô-đun sẽ cho phép nhập tương đối hoạt động khi tệp được thực thi theo tên. [...] Khi có [thuộc tính], nhập khẩu tương đối sẽ dựa trên thuộc tính này thay vì thuộc tính __name__ của mô-đun . [...] Khi mô-đun chính được chỉ định bởi tên tệp của nó, thì thuộc tính __package__ sẽ được đặt thành Không có . [...] Khi hệ thống nhập gặp một lần nhập tương đối rõ ràng trong mô-đun không có __package__ được đặt (hoặc được đặt thành Không), nó sẽ tính toán và lưu trữ giá trị chính xác (__name __. rpartition ('.') [0] cho các mô-đun thông thường và __name__ cho các mô-đun khởi tạo gói)
(nhấn mạnh của tôi)
Nếu __name__
là '__main__'
, __name__.rpartition('.')[0]
trả về chuỗi rỗng. Đây là lý do tại sao có chuỗi ký tự trống trong mô tả lỗi:
SystemError: Parent module '' not loaded, cannot perform relative import
Phần có liên quan của PyImport_ImportModuleLevelObject
chức năng của 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 tăng ngoại lệ này nếu không thể tìm thấy package
(tên của gói) trong interp->modules
(có thể truy cập dưới dạng sys.modules
). Vì sys.modules
là "một từ điển ánh xạ tên mô-đun thành các mô-đun đã được tải" , nên giờ đây rõ ràng mô-đun mẹ phải được nhập tuyệt đối rõ ràng trước khi thực hiện nhập tương đối .
Lưu ý: Bản vá từ vấn đề 18018 đã thêm một if
khối khác , sẽ được thực hiện trước mã ở trên:
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) {
...
*/
Nếu package
(giống như trên) là chuỗi rỗng, thông báo lỗi sẽ là
ImportError: attempted relative import with no known parent package
Tuy nhiên, bạn sẽ chỉ thấy điều này trong Python 3.6 hoặc mới hơn.
Giải pháp số 1: Chạy tập lệnh của bạn bằng cách sử dụng -m
Hãy xem xét một thư mục (là một gói Python ):
.
├── package
│ ├── __init__.py
│ ├── module.py
│ └── standalone.py
Tất cả các tệp trong gói bắt đầu với cùng 2 dòng mã:
from pathlib import Path
print('Running' if __name__ == '__main__' else 'Importing', Path(__file__).resolve())
Tôi bao gồm hai dòng sau chỉ để làm cho trật tự của hoạt động rõ ràng. Chúng ta có thể bỏ qua chúng hoàn toàn, vì chúng không ảnh hưởng đến việc thực hiện.
__init__.py và module.py chỉ chứa hai dòng đó (nghĩa là chúng hoàn toàn trống).
độc lập thêm vào các nỗ lực để nhập mô-đun thông qua nhập tương đối:
from . import module # explicit relative import
Chúng tôi nhận thức rõ rằng /path/to/python/interpreter package/standalone.py
sẽ thất bại. Tuy nhiên, chúng ta có thể chạy mô-đun với -m
tùy chọn dòng lệnh sẽ "tìm kiếm sys.path
mô-đun được đặt tên và thực hiện nội dung của nó dưới dạng __main__
mô-đun" :
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
thực hiện tất cả các công cụ nhập cho bạn và tự động thiết lập __package__
, nhưng bạn có thể tự làm điều đó trong
Giải pháp số 2: Đặt __package__ theo cách thủ công
Vui lòng coi nó như một bằng chứng về khái niệm hơn là một giải pháp thực tế. Nó không phù hợp để sử dụng trong mã thế giới thực.
PEP 366 có một cách giải quyết cho vấn đề này, tuy nhiên, nó chưa hoàn chỉnh, vì chỉ thiết lập __package__
một mình là không đủ. Bạn sẽ cần nhập ít nhất N gói trước trong phân cấp mô-đun, trong đó N là số lượng thư mục mẹ (liên quan đến thư mục của tập lệnh) sẽ được tìm kiếm cho mô-đun được nhập.
Như vậy
Thêm thư mục mẹ của tiền thân thứ N của mô-đun hiện tại vàosys.path
Xóa thư mục của tệp hiện tại khỏi sys.path
Nhập mô đun mẹ của mô đun hiện tại bằng tên đủ điều kiện
Đặt __package__
thành tên đủ điều kiện từ 2
Thực hiện nhập tương đối
Tôi sẽ mượn các tệp từ Giải pháp số 1 và thêm một số gói con khác:
package
├── __init__.py
├── module.py
└── subpackage
├── __init__.py
└── subsubpackage
├── __init__.py
└── standalone.py
Lần này, độc lập sẽ nhập mô-đun từ gói gói bằng cách nhập tương đối sau
from ... import module # N = 3
Chúng ta sẽ cần đặt trước dòng đó với mã soạn sẵn để làm cho nó hoạt động.
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
Nó cho phép chúng tôi thực hiện độc lập bằng tên tệp:
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
Một giải pháp tổng quát hơn được bọc trong một chức năng có thể được tìm thấy ở đây . Ví dụ sử dụng:
if __name__ == '__main__' and __package__ is None:
import_parents(level=3) # N = 3
from ... import module
from ...module.submodule import thing
Giải pháp số 3: Sử dụng nhập khẩu tuyệt đối và setuptools
Các bước là -
Thay thế nhập khẩu tương đối rõ ràng bằng nhập khẩu tuyệt đối tương đương
Cài đặt package
để làm cho nó có thể nhập được
Ví dụ, cấu trúc thư mục có thể như sau
.
├── project
│ ├── package
│ │ ├── __init__.py
│ │ ├── module.py
│ │ └── standalone.py
│ └── setup.py
nơi setup.py là
from setuptools import setup, find_packages
setup(
name = 'your_package_name',
packages = find_packages(),
)
Phần còn lại của các tệp được mượn từ Giải pháp số 1 .
Cài đặt sẽ cho phép bạn nhập gói bất kể thư mục làm việc của bạn (giả sử sẽ không có vấn đề về đặt tên).
Chúng tôi có thể sửa đổi độc lập để sử dụng lợi thế này (bước 1):
from package import module # absolute import
Thay đổi thư mục làm việc của bạn thành project
và chạy /path/to/python/interpreter setup.py install --user
( --user
cài đặt gói trong thư mục gói trang web của bạn ) (bước 2):
vaultah@base:~$ cd project
vaultah@base:~/project$ python3 setup.py install --user
Chúng ta hãy xác minh rằng giờ đây có thể chạy độc lập dưới dạng tập lệnh:
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'>
Lưu ý : Nếu bạn quyết định đi theo tuyến đường này, tốt hơn hết bạn nên sử dụng các môi trường ảo để cài đặt các gói riêng lẻ.
Giải pháp số 4: Sử dụng nhập khẩu tuyệt đối và một số mã soạn sẵn
Thành thật mà nói, việc cài đặt là không cần thiết - bạn có thể thêm một số mã soạn sẵn vào tập lệnh của mình để làm cho việc nhập tuyệt đối hoạt động.
Tôi sẽ mượn các tệp từ Giải pháp số 1 và thay đổi độc lập :
Thêm thư mục mẹ của gói vào sys.path
trước khi thử nhập bất cứ thứ gì từ gói bằng cách nhập tuyệt đối:
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
Thay thế nhập tương đối bằng nhập tuyệt đối:
from package import module # absolute import
độc lập chạy mà không có vấn đề:
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'>
Tôi cảm thấy rằng tôi nên cảnh báo bạn: cố gắng không làm điều này, đặc biệt nếu dự án của bạn có cấu trúc phức tạp.
Lưu ý phụ, PEP 8 khuyến nghị sử dụng nhập khẩu tuyệt đối, nhưng nói rằng trong một số trường hợp, nhập khẩu tương đối rõ ràng có thể chấp nhận được:
Nhập khẩu tuyệt đối được khuyến nghị, vì chúng thường dễ đọc hơn và có xu hướng hoạt động tốt hơn (hoặc ít nhất là đưa ra thông báo lỗi tốt hơn). [...] Tuy nhiên, nhập khẩu tương đối rõ ràng là một thay thế chấp nhận được đối với nhập tuyệt đối, đặc biệt là khi xử lý các bố cục gói phức tạp trong đó sử dụng nhập tuyệt đối sẽ không cần thiết.