Python 3中的相對導入


Answers

說明

來自PEP 328

相對導入使用模塊的__name__屬性來確定模塊在包層次結構中的位置。 如果模塊的名稱不包含任何包信息(例如,它被設置為'__main__'), 那麼無論模塊實際位於文件系統的哪個位置, 相對導入都會被解析為模塊是頂級模塊

在某些點PEP 338PEP 328衝突:

...相對導入依賴於__name__來確定當前模塊在包層次結構中的位置。 在主模塊中, __name__的值始終'__main__' ,所以顯式相對導入將始終失敗(因為它們僅適用於包內的模塊)

為了解決這個問題, PEP 366引入了頂級變量__package__

通過添加新的模塊級屬性,如果使用-m開關執行模塊,則此PEP允許相關導入自動工作。 當文件按名稱執行時,模塊中的少量樣板文件將允許相關導入工作。 [...]當[屬性]存在時,相對導入將基於此屬性而不是模塊__name__屬性。 [...]當主模塊由其文件名指定時, __package__屬性將被設置為None 。 [...] 當導入系統在沒有設置__package__的模塊中遇到明確的相對導入(或將其設置為無)時,它將計算並存儲正確的值__name __。rpartition('。')[0] for正常模塊和用於程序包初始化模塊的__name__

(強調我的)

如果__name__'__main__' __name__.rpartition('.')[0] '__main__'__name__.rpartition('.')[0]返回空字符串。 這就是為什麼錯誤描述中有空字符串的原因:

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

CPython的PyImport_ImportModuleLevelObject函數的相關部分:

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

如果CPython無法在interp->modules (可以sys.modules訪問)中找到packagepackage的名稱), interp->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 ):

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

包中的所有文件都以相同的兩行代碼開始:

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

包括這兩行,以使操作的順序顯而易見。 我們可以完全忽略它們,因為它們不影響執行。

__init__.pymodule.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__ ,但你可以自己設置__package__

解決方案2:手動設置__package__

請把它當作概念證明而不是實際的解決方案。 它不適合用於真實世界的代碼。

PEP 366解決了這個問題,但是它並不完整,因為單獨設置__package__還不夠。 您將需要在模塊層次結構中導入至少N個前面的軟件包,其中N是將搜索要導入的模塊的父目錄(相對於腳本目錄)的數量。

從而,

  1. 將當前模塊的第N個前輩的父目錄添加到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

here可以找到包含在函數中的更一般的解決方案。 用法示例:

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建議使用絕對進口,但指出在某些情況下明確的相對進口是可以接受的:

建議使用絕對導入,因為它們通常更具可讀性並且往往表現更好(或者至少提供更好的錯誤消息)。 [...]然而,明確的相對進口是絕對進口的可接受替代方案,特別是在處理複雜的包裝佈局時,使用絕對進口的情況會不必要的冗長。

Question

我想從同一個目錄中的另一個文件導入一個函數。

有時它適用於我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

我不明白這裡的邏輯,我找不到任何解釋。 這看起來完全隨機。

有人可以向我解釋這一切背後的邏輯是什麼?




把它放在包的__init__.py文件中

import os, sys
#For relative imports to work in Python 3.6
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和Python 3




為了避免這個問題,我設計了一個解決方案,這個解決方案已經為我工作了一段時間。 它將上層目錄添加到lib路徑中:

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

通過智能策略(檢查調用堆棧),重新打包可以使各種情況下的相關導入工作。






Related