深入理解python的導入問題——包,模塊(請勿參考,未完待續。。。)

背景

        在python開發中,經常需要導入不同的內容,在開發大型項目時尤其棘手。稍有不慎就報錯,算下來在這個問題上我浪費了太多時間。與其繼續陷入這種泥潭,還不如掘地三尺掌握這個知識點,一勞永逸地解決問題。

        本文按照“理論->習慣用法->示例”的思路組織語言。如果只是用一下,可以忽略理論部分(不過還是推薦至少把“路徑”這一概念搞清楚)直接快速瀏覽習慣用法,尋找適合自己的方式,然後再照對應的示例去實現。

 

1. 理論補充

注:該部分非專業解釋,以自己理解爲主,沒有權威性,專業解釋詳見相關鏈接並請自行查閱官方資料。

1.1 路徑

工作目錄:當前程序運行的類似於linux中使用pwd查詢當前的工作目錄一樣,可以通過工程配置來決定。務必搞清楚這一點,這個是使用相對路徑的基石(不同IDE,不同工程設置下這裏都會不一樣,最終導致各種問題)

絕對路徑:文件存儲的磁盤路徑,例如:D:\Workspace\python\module_test\main.py。(只要文件的存儲地址不變,則該路徑總是對的)。

相對路徑:文件存儲相對於工作目錄的層級關係(只要工作目錄發生改變,相對路徑的關係就被破壞了)

. 當前文件夾

.. 上一級文件夾

系統路徑(sys.path):就是計算機的環境變量,主要用來處理系統默認路徑,正是因爲有了系統路徑,你導入系統模塊(eg: time, os, 或者安裝的標準第三方模塊)時,纔不用糾結這個系統模塊到底在哪裏。(顯然,你也可以把自定義的模塊添加到系統路徑下)

各種路徑的查詢和修改方法:

import sys
import os

print(sys.argv[0]) #獲得當前待執行的那個模塊(py文件)
print(os.getcwd()) #獲得當前工作目錄
os.chdir("目標目錄")   #修改當前工作目錄爲目標目錄
print(os.path.abspath('.')) #獲得當前工作目錄
print(os.path.abspath('..')) #獲得當前工作目錄的父目錄
print(os.path.abspath(os.curdir)) #獲得當前工作目錄
print(sys.path) #獲得系統路徑

1.2 模塊(module),包(package)

所謂模塊,可以簡單理解爲“就是一個py文件”。

所謂包,可以簡單理解爲“多個想要組織在一起的py文件,放在了一個文件夾裏”,而那個文件夾就是一個包。與普通的文件夾的不同之處在於,該文件夾裏必須帶一個"__init__.py"的文件(可以是空文件)。

一個簡單的比方,試想如下的工程組織結構

package/
        __init__.py
        module1.py
        module2.py
        sub_package/
                    __init__.py
                    module11.py
                    module12.py

package文件夾就是一個包,module1和module2就是package的兩個模塊。其中package還有一個子包叫sub_package,它裏面也有兩個子模塊分別叫module11和module12

1.3 對象,模塊導入

        ——python的世界裏,一切皆對象。不論是類,函數,變量,模塊還是包,其名字只是這個對象的一個引用而已。

按照我的理解就是:我就是一個實體(對象),張三就是我的名字(引用)。你叫張三的時候其實本意是想叫我這個人過去,而不是對張三這個名字感興趣,只不過你通過張三這個名字具體地叫到了我這個人。(也許你有一天也會驚奇地發現,我這個人其實有好多個小名的,這就意味着其他人叫我李四,王五,劉麻子我照樣會答應的。。。)

        好了,我們通過import module 語句導入一個模塊會發生什麼?—— 你通過module這個名字實例化了一個對象(這個對象在成功import之後就存在於內存裏了),一旦你成功實例化了這個module對象,你就可以用上module裏面定義的內容了(變量,函數,類等)。

       導入一個sys模塊時,首先是找到這個模塊,然後會從頭到尾執行這個模塊,遇到def就創建一個函數對象,然後賦上函數名,遇到模塊中被賦值的全局變量,那就創建這個賦值號右側的對象,然後賦給變量。那我怎麼知道這個module裏到底導入了哪些資源呢?你可以這樣查詢

print(dir(module))
有可能得到如下的結果
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b'] 

1.4 命名空間

        接着上面的“我是張三”這個繼續講,如何建立起“實體->引用”的對應關係呢?看着挺眼熟,這不是字典的結構嘛?是的,python裏就是這麼實現的,不過取了一個叫做“命名空間”的術語而已。通過命名空間,就建立了引用和實體之間的映射關係了,所以你不用去操作內存,只要管這個對象叫啥名字就可以了。

        一個命名空間中不能有重名,但是不同的命名空間可以重名而沒有任何影響。python中有3類命名空間

  • local:函數調用時創建(調用返回後即消失),記錄了函數入參,內部變量等
  • global:模塊加載時創建(除非人爲手動卸載該模塊,否則一直存在),記錄了該模塊所包含的資源
  • built-in:系統自帶(一直存在),任何模塊均可訪問(通常放置內置函數和異常)

1.5 導入機制

a. 導入原則

摘抄Python標準庫參考手冊3.6.4中對import語句的一段說明:

The basic import statement (no from clause) is executed in two steps:

  1. find a module, loading and initializing it if necessary
  2. define a name or names in the local namespace for the scope where the import statement occurs.

When the statement contains multiple clauses (separated by commas) the two steps are carried out separately for each clause, just as though the clauses had been separated out into individual import statements.

        以import pandas as pd爲例,模塊導入過程首先是去尋找pandas模塊,找到後將其與本地命名空間的資源進行比對(防止重複導入)。如果沒有,則實例化pandas對象,在內存裏創建相應的資源併爲其初始化,如果有則跳過。最後將pandas這個資源和pd這個變量名綁定到一起,用戶就可以通過pd.xxx來引用pandas的資源了

b. 模塊搜索順序(如果模塊同名的話,此處可能會衝突)

When a module named spam is imported, the interpreter first searches for a built-in module with that name. If not found, it then searches for a file named spam.py in a list of directories given by the variable sys.pathsys.path is initialized from these locations:

  • The directory containing the input script (or the current directory when no file is specified).
  • PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
  • The installation-dependent default.

After initialization, Python programs can modify sys.path. The directory containing the script being run is placed at the beginning of the search path, ahead of the standard library path. This means that scripts in that directory will be loaded instead of modules of the same name in the library directory. This is an error unless the replacement is intended. 

可見,系統在搜索模塊(module.py)時,優先去找built-in的內置模塊,找不到再通過sys.path變量去搜索module.py這個文件。

可以做個實驗,自定義一個os.py的模塊並隨便定義一個函數,你會發現無論如何你自定義的os模塊都不起作用

小結:

  • 模塊導入前首先確定自己的路徑,再考慮用什麼方法去導入。搞不清楚路徑就不要談導入了,一團亂麻
  • 模塊就是一個py文件,包就是一個帶__init__.py的文件夾
  • 成功導入一個對象意味着(有可能)實例化了一個對象並在內存中創建了與該模塊相關的資源
  • 命名空間就是一個字典,它實現了上述從模塊名到實際對象的映射關係,正是因爲這層映射關係你纔可以通過模塊名來引用該資源,而不用關注內存細節
  • 模塊導入的機制是:首先搜索到模塊並對初始化(如果之前沒導入過的話),然後將該模塊的資源與一個名稱綁定到命名空間中。例如import pandas as pd. 你可以利用pd來指代pandas,而pandas這個名字指代了實例化的pandas類所包含的一切資源
  • 模塊搜索的順序是:先在built-in中搜索,如果沒有再找sys.path求助
  • sys.path本質上就是一個系統變量,它存儲了一堆路徑(工作路徑,環境變量,site-package安裝路徑,其他默認路徑等),特別地: sys.path[0] 指代了當前待執行腳本的路徑

2. 常用導入方式彙總

備註:下文如無特別提示,統一main.py爲當前待執行的腳本,且工作路徑與其保持一致

2.1 同級模塊導入,無包的導入(最簡單)

文件層次結構示例:
test/
    main.py
    a.py    -> 定義了hello_a函數, para_a變量

方法1:
import a
a.hello_a()
print(a.para_a)

方法2:
from a import *
hello_a()
print(para_a)

2.2 不同級模塊導入,無包的導入

文件層次結構示例:
test/
    main.py
    sub_module/
            a.py    -> 定義了hello_a函數, para_a變量

方法1:
import sub_module.a
sub_module.a.hello_a()
print(sub_module.a.para_a)

方法2:
from sub_module.a import *
hello_a()
print(para_a)

2.3 包的導入(最基礎的做法)

文件層次結構示例:
test/
    main.py
    package1/
            __init__.py -> 空文件,但必須要有
            m1.py    -> 定義了hello_a和hello_b函數, para_a變量

導入方法:
import package1.m1 as tmp
tmp.hello_a()
print(tmp.para_a)

2.4 控制from module import *的導入內容(不推薦)

2.3 中的導入層次中,如果使用from module import *, 會發現導入失敗。此時__init__.py文件的作用體現出來了。

只需在__init__.py文件中添加 __all__字段就可以實現導入內容的精確控制,例如

 

3. 簡單示例

 

相關鏈接

工作路徑:https://blog.csdn.net/qq_15188017/article/details/53991216

詳解命名空間:https://www.cnblogs.com/zhangxinhe/p/6963462.html

python導入機制詳解:https://www.cnblogs.com/qiaoxg-2018/p/usingimport.html

包/模塊理解:https://www.cnblogs.com/kex1n/p/5977051.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章