【避坑指北】Python3相對路徑導入方法

2021-12-17 小黑雜說
前情

最近在優化原項目一部分Python代碼,遇到了代碼重複拷貝的問題,一個方法拷貝了n多份,這個「壞味道」當然忍不了,準備將方法寫到utils.py裡,由於Python3已經支持相對路徑導入了,utils放到當前包的common目錄,用到此方法的代碼導入utils使用即可。so easy!

後來?後來我就掉進坑裡。

我以為的相對路徑導入並不是真實的相對路徑導入。

Python導入包或方法

假設我們的工程項目是這樣的:

.
├── a
│   └── callee.py
├── b
│   └── caller.py
├── c
│   └── hello.py
└── main.py

常規操作

hello.py中實現了一個列印「say hi~」方法hi():

# c/hello.py
def hi():
    print("say hi~")

現在想要在main.py中調用,那我們只需要加入一行from c.hello import hi,然後直接調用hi()即可。

# main.py
from c.hello import hi

hi()

我們運行python3 main.py,正常輸出「say hi~」。

python和Java一樣都是用目錄管理包的,運行時會從當前路徑(main.py所在目錄)開始查找匹配的包名對應的c/hello.py文件,然後找到其中名為hi的方法,並調用。

import默認搜索順序

默認情況下,python的import關鍵字會選擇優先查找python的內建模塊,若沒找到,則會去sys.path保存的路徑列表中尋找。

sys.path保存的路徑列表包括幾個部分:

第三方擴展的site-package目錄,也就是pip安裝第三方包的路徑相對路徑導入的那些坑

現在有一個需求就是b目錄下的caller.py希望執行a目錄callee.py中的方法caller_test()方法,這個方法可以對應出調用者的信息。

# a/callee.py
import sys
import os


def caller_test():
    """列印調用者信息"""
    back_frame = sys._getframe().f_back

    if back_frame is None:
        print("back_frame is None, no py caller!")
    else:
        back_filename = os.path.basename(back_frame.f_code.co_filename)
        print("caller: {}".format(back_filename.split('.')[0]))

python3已經可以支持相對路徑導入包了,簡單寫一下:

# b/caller.py
import sys

from ..a import callee

def call():
    print('- caller.py -')
 print("name: {}".format(__name__))
 callee.caller_test()

if __name__ == '__main__':
 call()

這裡可以看到a包名前額外多了兩個點..,按照python手冊中關於相對導入的介紹:兩個點..表示從當前目錄的父目錄開始查找a/callee.py文件,一個點.表示當前目錄,那麼如果我想找父目錄的父目錄中的包呢?那就用三個點...,通常用到三個點的情況並不多。

看上去毫無問題,正常極了,一運行就傻眼了。

錯誤1

執行./b/caller.py,提示錯誤:ImportError: attempted relative import with no known parent package。

嘗試在import前一行加入列印__name__、__package__、sys.path,結果如下:

name: __main__
package: None
sys.path: ['/home/rfw/test/b', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/home/rfw/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

很奇怪,看到sys.path中當前路徑是b目錄所在路徑,按照相對導入的邏輯,..a就應該進入了test/a目錄才對!

錯誤2

StackOverflow上查了下,可以使用python -m b.caller以模塊的方式運行,將包信息告訴python解釋器。

嘗試了下,這次錯誤提示變了,ValueError: attempted relative import beyond top-level package,提示是說相對導入找到的路徑已經超過最頂級的了。

此時再次列印,錯誤日誌如下:

name: __main__
package: b
sys.path: ['/home/rfw/test', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/home/rfw/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']

這時,和上一次列印不一樣的地方在於__package__的值為b,當前運行路徑為test目錄。

由於顯示當前目錄是test,因此,嘗試把導入改成from a.callee import caller_test,運行正常!列印如下:

name: __main__
package: b
sys.path: ['/home/rfw/test', '/usr/lib/python38.zip', '/usr/lib/python3.8', '/usr/lib/python3.8/lib-dynload', '/home/rfw/.local/lib/python3.8/site-packages', '/usr/local/lib/python3.8/dist-packages', '/usr/lib/python3/dist-packages']
caller: caller

但是這就不是相對導入了啊。百思不得其解。

真相只有一個

查了下python官方文檔關於相對導入的說明(https://www.python.org/dev/peps/pep-0328/ ),恍然大明白。

Relative imports use a module's __name__ attribute to determine that module's position in the package hierarchy. If the module's name does not contain any package information (e.g. it is set to '__main__') then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.

翻譯過來就是:

相對導入依賴於一個模塊的__name__屬性,根據這個屬性去決定該模塊在整個包中的層級結構。

當一個模塊的__name__屬性不包含任何包信息時,如直接運行py腳本時,__name__會被設置成__main__,這時,不管這個文件位於包目錄的哪個位置,相對導入機制會把當前腳本視為頂級模塊。

這就意味著,只要是我從終端運行python腳本,都會遇到__name__為__main__的問題,當前被運行的python腳本永遠無法使用相對導入

現在在根目錄下修改main.py,並在b/b1目錄下創建caller_proxy.py。

.
├── a
│   └── callee.py
├── b
│   ├── b1
│   │   └── caller_proxy.py
│   └── caller.py
├── c
│   └── hello.py
└── main.py

main.py的內容如下:

import sys

from c.hello import hi

print("__name__: {}, __package__: {}".format(__name__, __package__))

from b.b1 import caller_proxy

caller_proxy.proxy()

caller_proxy.py的內容如下:

import sys
from .. import caller # 相對導入

print(__package__)

def proxy():
 print("- caller_proxy.py -")
 print("name: {}".format(__name__))
 caller.call()

該文件使用了相對導入,現在運行./main.py,結果如下。

__name__: __main__, __package__: None
say hi~
- caller_proxy.py -
name: b.b1.caller_proxy
- caller.py -
name: b.caller
caller: caller

這時,caller_proxy.py執行時的__name__值是正常的包名結構b.b1.caller_proxy,因此可以使用相對導入..找到b.caller。

而caller.py執行時的包名結構是b.caller,因此,相對導入只能找到b包下的文件,所以,只能使用from a.callee import caller_test。

通常應該怎麼做

為了避免一些奇奇怪怪的問題,還是比較推薦在sys.path數組追加要導入包絕對路徑的方式。

import os
import inspect
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '../common')))

from utils import xxx_func

以之前的caller.py為例,想要調用a/callee.py,可以寫成:

import sys
import os

import inspect
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '../a')))

from callee import caller_test

def call():
 print('- caller.py -')
 print("name: {}".format(__name__))

 caller_test()


if __name__ == '__main__':
 call()

這樣就不用care是直接運行,還是用-m參數以模塊去運行了,直接運行./b/caller.py,輸出結果如下:

$ ./b/caller.py
- caller.py -
name: __main__
caller: caller

以上,就是之前處理Python import導入包時遇到的坑,簡單記錄。

相關焦點

  • 介紹 | Python3和Python2差異 - 拷貝
    Python3採用的是絕對路徑的方式進行import。Python2中相對路徑的import會導致標準庫導入變得困難(想像一下,同一目錄下有file.py,如何同時導入這個文件和標準庫file)。Python3中這一點將被修改,如果還需要導入同一目錄的文件必須使用絕對路徑,否則只能使用相關導入的方式來進行導入。
  • python 淺析模塊的導入和調用
    答案就是:這與導入路徑有關import sysprint(sys.path)  輸出結果入下(這是windows下的目錄):['D:\\pycode\\模塊學習', 'D:\\pycode', 'D:\\python3\\python36.zip', 'D:\\python3\\DLLs', 'D:\\python3
  • RPO 相對路徑覆蓋攻擊
    主要是利用瀏覽器的一些特性和部分服務端的配置差異導致的漏洞,通過一些技巧,我們可以通過引入相對路徑來引入其他資源文件,以達到我們的目的。我們在 index.php 中使用相對路徑引入 rpo.css 文件 <?
  • Python2和Python3差異總結
    Python2 和 Python3 字節和字符對應關係為:Python3 採用的是絕對路徑的方式進行 importPython2 中相對路徑的 import 會導致標準庫導入變得困難(想像一下,同一目錄下有 file.py,如 何同時導入這個文件和標準庫 file)。
  • P02-絕對路徑和相對路徑
    本地文件讀寫依賴文件路徑,本節簡單介紹下相對路徑和絕對路徑,只局限在本地路徑,不涉及網絡路徑。
  • Linux絕對路徑和相對路徑詳解
    說明目錄或文件名位置的方法有兩種,分別使用絕對路徑和相對路徑。絕對路徑指的是從根目錄(/)開始寫起的文件或目錄名稱,而相對路徑則指的是相對於當前路徑的寫法。換句話說,絕對路徑必須以一個正斜線(/),也就是根目錄開始,到查找對象(目錄或文件)所必須經過的每個目錄的名字,它是文件位置的完整路標,因此,在任何情況下都可以使用絕對路徑找到所需的文件。
  • HTML 相對路徑與絕對路徑
    文件路徑文件路徑就是文件在電腦(伺服器)中的位置,表示文件路徑的方式有兩種:相對路徑和絕對路徑。
  • 【技巧】SourceInsight如何設置為相對路徑
    今天主要跟大家分享一下如何把SI設置為相對路徑,方便大家轉移和打包代碼。當初次使用直接默認下一步設置的時候都會設置成絕對路徑,程序原始碼的移動和打包並放到其他目錄下,就會導致軟體找不到對應的源文件從而需要重新進行定位和同步文件,而且當源文件比較龐大重新導入和同步的時間也比較長。
  • code小知識~工作路徑、絕對路徑、相對路徑
    ()獲取的當前工作路徑就是「E:\juzicode」,需要注意如果用當前工作路徑拼接路徑時,當前工作路徑的最後一個斜槓「\」是沒有的,需要手動添加:相對路徑相對路徑有一個「相對」的對象就是當前工作路徑,要搞清楚相對路徑先要明白當前工作路徑。如果在cmd命令行下調試,提示符「>」之前的這段字符就是當前工作路徑。當前工作路徑的基礎上,使用」..\\」(上一層目錄)組合出來的路徑就是一種相對路徑,直接看個例子,在路徑 E:\juzicode\com 下的文件夾結構是這樣的:
  • linux絕對路徑和相對路徑
    在Linux中什麼是一個文件的路徑呢,說白了就是這個文件存在的地方,例如 /root/.ssh/authorized_keys
  • SEO術語之絕對路徑與相對路徑的區別
    最近有很多SEO新手同學問我,,絕對路徑與相對路徑的區別是什麼」,今天為大家詳細解答一番。  什麼是絕對路徑  絕對路徑的指定是從樹型目錄結構頂部的根目錄開始到某個目錄或文件的路徑,由一系列連續的目錄組成,中間用斜線分隔,直到要指定的目錄或文件,路徑中的最後一個名稱即為要指向的目錄或文件。
  • C\C++編程中:相對路徑+絕對路徑
    轉自:http://www.cnblogs.com/vranger/p/3820783.html電腦硬碟E盤下,建文件夾「test」,"test"下建立子文件夾「file」,"file"下建子文件夾「data」,電腦資源管理器顯示目錄  E:\test\file\data當前 路徑
  • 絕對相對路徑,百度網盤小案例
    絕對、相對路徑  在製作這個小案例之前,我們先來了解一下絕對地址和相對地址
  • linux文件路徑怎麼區分絕對路徑和相對路徑
    一、介紹1,文件路徑什麼是文件的路徑?答:這個文件存放的地方,可以聯想為 文件的「家」。
  • 面對相對路徑和絕對路徑的分岔口,你選對了嗎?
    導論我們都知道,如果我們要找到需要的文件,就要知道文件的位置,表示文件位置的方式就是路徑。在程序中,只要涉及文件的地方(如圖片等)都會涉及到相對路徑和絕對路徑的問題。今天就為大家介紹一下相對路徑和絕對路徑的具體寫法及應用。
  • python基礎:range方法在Python2和Python3中的不同
    range()方法是Python中常用的方法, 但是在Python2和Python3中使用方法不同,下面看下它們的不同使用方法。
  • vscode設置python3調試環境
    這個是以前寫的一篇文章,因為太短,所以當時也就沒發最近在寫的另一篇文章需要引用,而微信不能連結其他網站的地址所以就翻新出來了,下面附一個導航吧:匯總系列:https://www.cnblogs.com/dunitian/p/4822808.html#ai獨立安裝python3
  • Q群問答‖Revit連結的相對路徑與絕對路徑的區別
    問:Revit連結的相對路徑與絕對路徑有什麼區別?答:我們著重來理解兩個詞,相對和絕對。
  • python3字體解決大挖掘
    繪圖R vs Python python3字體解決大挖掘雖然當年列了很多用python畫圖比R畫圖好的理由且自己也慢慢轉向python畫圖,但用R畫圖多年的意識總會慣性去收集python畫圖的缺點,以增加下一幅圖選哪個軟體更便捷的評估標準,而差別總是發生在意想不到的地方
  • 以html中插入圖片為例,聊一聊絕對路徑和相對路徑的區別
    src即source(來源)的縮寫,也就是圖片的url(或者說地址、路徑)。通過查閱文檔(例如w3school網站),可以看到這個屬性的語法。如下圖裡面有提到相對路徑和絕對路徑兩種說法。相信還是有一些朋友比如我第一次看到上圖的解釋是懵的。