本文為CSDN博主「神是念著倒」的原創(chuàng)文章跋选,遵循CC 4.0 BY-SA版權協(xié)議,轉載請附上原文出處鏈接及本聲明系奉。
原文鏈接:https://blog.csdn.net/weixin_38256474/article/details/81228492
一旦使用多層文件架構就很容易遇上import的坑昭抒!哈哈。
一攒磨、理解一些基本概念
1、模塊汤徽、包
模塊 module:一般情況下娩缰,是一個以.py為后綴的文件。其他可作為module的文件類型還有".pyo"谒府、".pyc"漆羔、".pyd"、".so"狱掂、".dll",但Python初學者幾乎用不到亲轨。
module 可看作一個工具類趋惨,可共用或者隱藏代碼細節(jié),將相關代碼放置在一個module以便讓代碼更好用惦蚊、易懂器虾,讓coder重點放在高層邏輯上讯嫂。
module能定義函數、類兆沙、變量欧芽,也能包含可執(zhí)行的代碼。module來源有3種:
①Python內置的模塊(標準庫)葛圃;
②第三方模塊千扔;
③自定義模塊。
包 package: 為避免模塊名沖突库正,Python引入了按目錄組織模塊的方法曲楚,稱之為 包(package)。包 是含有Python模塊的文件夾褥符。
當一個文件夾下有 init .py時龙誊,意為該文件夾是一個包(package),其下的多個模塊(module)構成一個整體喷楣,而這些模塊(module)都可通過同一個包(package)導入其他代碼中趟大。
其中 init .py文件 用于組織包(package),方便管理各個模塊之間的引用铣焊、控制著包的導入行為逊朽。
該文件可以什么內容都不寫,即為空文件(為空時粗截,僅僅用import [該包]形式 是什么也做不了的)惋耙,存在即可,相當于一個標記熊昌。
但若想使用from pacakge_1 import *這種形式的寫法绽榛,需在 init .py中加上: all = [‘file_a’, ‘file_b’] #package_1下有file_a.py和file_b.py,在導入時 init .py文件將被執(zhí)行婿屹。
但不建議在 init .py中寫模塊灭美,以保證該文件簡單。不過可在 init .py導入我們需要的模塊昂利,以便避免一個個導入届腐、方便使用。
其中蜂奸, all 是一個重要的變量犁苏,用來指定此包(package)被import *時,哪些模塊(module)會被import進【當前作用域中】扩所。不在 all 列表中的模塊不會被其他程序引用围详。可以重寫 all ,如 all = [‘當前所屬包模塊1名字’, ‘模塊1名字’]助赞,如果寫了這個买羞,則會按列表中的模塊名進行導入。
在模糊導入時雹食,形如from package import 畜普,是由all定義的。
精確導入群叶,形如 from package import *吃挑、import package.class。
path 也是一個常用變量盖呼,是個列表儒鹿,默認情況下只有一個元素,即當前包(package)的路徑几晤。修改 path 可改變包(package)內的搜索路徑约炎。
當我們在導入一個包(package)時(會先加載 init .py定義的引入模塊,然后再運行其他代碼)蟹瘾,實際上是導入的它的 init .py文件(導入時圾浅,該文件自動運行,助我們一下導入該包中的多個模塊)憾朴。我們可以在 init .py中再導入其他的包(package)或模塊 或自定義類狸捕。
2、sys.modules众雷、命名空間灸拍、模塊內置屬性
2.1 sys.modules
官方解釋:鏈接
sys.modules 是一個 將模塊名稱(module_name)映射到已加載的模塊(modules) 的字典±。可用來強制重新加載modules鸡岗。Python一啟動,它將被加載在內存中编兄。
當我們導入新modules轩性,sys.modules將自動記錄下該module;當第二次再導入該module時狠鸳,Python將直接到字典中查找揣苏,加快運行速度。
它是個字典件舵,故擁有字典的一切方法卸察,如sys.modules.keys()、sys.modules.values()铅祸、sys.modules[‘os’]蛾派。但請不要輕易替換字典、或從字典中刪除某元素,將可能導致Python運行失敗洪乍。
import sys
print(sys.modules)#打印,查看該字典具體內容夜焦。
1
2
2.2 命名空間
如同一個dict壳澳,key 是變量名字,value 是變量的值茫经。
每個函數function 有自己的命名空間巷波,稱local namespace,記錄函數的變量卸伞。
每個模塊module 有自己的命名空間抹镊,稱global namespace,記錄模塊的變量荤傲,包括functions垮耳、classes、導入的modules遂黍、module級別的變量和常量终佛。
build-in命名空間,它包含build-in function和exceptions雾家,可被任意模塊訪問铃彰。
某段Python代碼訪問 變量x 時,Python會所有的命名空間中查找該變量芯咧,順序是:
local namespace 即當前函數或類方法牙捉。若找到,則停止搜索敬飒;
global namespace 即當前模塊邪铲。若找到,則停止搜索驶拱;
build-in namespace Python會假設變量x是build-in的函數函數或變量霜浴。若變量x不是build-in的內置函數或變量,Python將報錯NameError蓝纲。
對于閉包阴孟,若在local namespace找不到該變量,則下一個查找目標是父函數的local namespace税迷。
例:namespace_test.py代碼
def func(a=1):
b = 2
print(locals())#打印當前函數(方法)的局部命名空間
'''
locs = locals()#只讀永丝,不可寫。將報錯箭养!
locs['c'] = 3
print(c)
'''
return a+b
func()
glos = globals()
glos['d'] = 4
print(d)
print(globals())#打印當前模塊namespace_test.py的全局命名空間
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
內置函數locals()慕嚷、globals()返回一個字典。區(qū)別:前者只讀、后者可寫喝检。
命名空間 在from module_name import 嗅辣、import module_name中的體現:from關鍵詞是導入模塊或包中的某個部分。
from module_A import X:會將該模塊的函數/變量導入到當前模塊的命名空間中挠说,無須用module_A.X訪問了澡谭。
import module_A:modules_A本身被導入,但保存它原有的命名空間损俭,故得用module_A.X方式訪問其函數或變量蛙奖。
2.3 模塊內置屬性
name 直接運行本模塊, name 值為 main 杆兵;import module雁仲, name 值為模塊名字。
file 當前 module的絕對路徑
dict
doc
package
path
3琐脏、絕對導入攒砖、相對導入
3.1 絕對導入:所有的模塊import都從“根節(jié)點”開始。根節(jié)點的位置由sys.path中的路徑決定骆膝,項目的根目錄一般自動在sys.path中祭衩。如果希望程序能處處執(zhí)行,需手動修改sys.path阅签。
例1:c.py中導入B包/B1子包/b1.py模塊
import sys,os
BASE_DIR = os.path.dirname(os.path.abspath(file))#存放c.py所在的絕對路徑
sys.path.append(BASE_DIR)
from B.B1 import b1#導入B包中子包B1中的模塊b1
1
2
3
4
5
6
例2:b1.py中導入b2.py模塊
from B.B1 import b2#從B包中的子包B1中導入模塊b2
1
3.2 相對導入:只關心相對自己當前目錄的模塊位置就好掐暮。不能在包(package)的內部直接執(zhí)行(會報錯)。不管根節(jié)點在哪兒政钟,包內的模塊相對位置都是正確的路克。
b1.py代碼
from . import b2 #這種導入方式會報錯。
import b2#正確
b2.print_b2()
1
2
3
b2.py代碼
def print_b2():
print('b2')
1
2
運行b1.py养交,打泳恪:b2。
在使用相對導入時碎连,可能遇到ValueError: Attempted relative import beyond toplevel package
解決方案:參考這篇文章灰羽,鏈接。
3.3 單獨導入包(package):單獨import某個包名稱時鱼辙,不會導入該包中所包含的所有子模塊廉嚼。
c.py導入同級目錄B包的子包B1包的b2模塊,執(zhí)行b2模塊的print_b2()方法:
c.py代碼
import B
B.B1.b2.print_b2()
1
2
運行c.py倒戏,會報錯怠噪。
解決辦法:
B/ init .py代碼
from . import B1#其中的.表示當前目錄
1
B/B1/ init .py代碼
from . import b2
1
此時,執(zhí)行c.py杜跷,成功打影睢:b2矫夷。
3.4 額外
①一個.py文件調用另一個.py文件中的類。
如 a.py(class A)憋槐、b.py(class B)双藕,a.py調用b.py中類B用:from b import B
②一個.py文件中的類 繼承另一個.py文件中的類。如 a.py(class A)阳仔、b.py(class B)蔓彩,a.py中類A繼承b.py類B。
from b import B
class A(B):
pass
1
2
3
二驳概、Python運行機制:理解Python在執(zhí)行import語句(導入內置(Python自個的)或第三方模塊(已在sys.path中))時,進行了啥操作旷赖?
step1:創(chuàng)建一個新的顺又、空的module對象(它可能包含多個module);
step2:將該module對象 插入sys.modules中等孵;
step3:裝載module的代碼(如果需要稚照,需先編譯);
step4:執(zhí)行新的module中對應的代碼俯萌。
在執(zhí)行step3時果录,首先需找到module程序所在的位置,如導入的module名字為mod_1咐熙,則解釋器得找到mod_1.py文件弱恒,搜索順序是:
當前路徑(或當前目錄指定sys.path)----->PYTHONPATH----->Python安裝設置相關的默認路徑。
對于不在sys.path中棋恼,一定要避免用import導入 自定義包(package)的子模塊(module)返弹,而要用from…import… 的絕對導入 或相對導入,且包(package)的相對導入只能用from形式爪飘。
1义起、“標準”import,頂部導入
有上述基礎知識师崎,再理解這個思維導圖默终,就很容易了。在運用模塊的變量或函數時犁罩,就能得心應手了齐蔽。
2、嵌套import
2.1 順序導入-import
PS:各個模塊的Local命名空間的獨立的昼汗。即:
test模塊 import moduleA后肴熏,只能訪問moduleA模塊,不能訪問moduleB模塊顷窒。雖然moduleB已加載到內存中蛙吏,如需訪問源哩,還得明確地在test模塊 import moduleB。實際上打印locals()鸦做,字典中只有moduleA励烦,沒有moduleB。
2.2 循環(huán)導入/嵌套導入-import
形如from moduleB import ClassB語句泼诱,根據Python內部import機制坛掠,執(zhí)行細分步驟:
在sys.modules中查找 符號“moduleB”;
如果符號“moduleB”存在治筒,則獲得符號“moduleB”對應的module對象屉栓;
從的 dict__中獲得 符號“ClassB”對應的對象。如果“ClassB”不存在耸袜,則拋出異秤讯啵“ImportError: cannot import name ‘classB’”
如果符號“moduleB”不存在,則創(chuàng)建一個新的 module對象堤框。不過此時該新module對象的 dict 為空域滥。然后執(zhí)行moduleB.py文件中的語句,填充的 dict 蜈抓。
總結:from moduleB import ClassB有兩個過程启绰,先from module,后import ClassB沟使。
當然將moduleA.py語句 from moduleB import ClassB改為:import moduleB委可,將在第二次執(zhí)行moduleB.py語句from moduleA import ClassA時報錯:ImportError: cannot import name ‘classA’
解決這種circular import循環(huán)導入的方法:
例比:安裝無線網卡時,需上網下載網卡驅動格带;
安裝壓縮軟件時撤缴,從網上下載的壓縮軟件安裝程序是被壓縮的文件。
方法1----->延遲導入(lazy import):把import語句寫在方法/函數里叽唱,將它的作用域限制在局部屈呕。(此法可能導致性能問題)
方法2----->將from x import y改成import x.y形式
方法3----->組織代碼(重構代碼):更改代碼布局,可合并或分離競爭資源棺亭。
合并----->都寫到一個.py文件里虎眨;
分離–>把需要import的資源提取到一個第三方.py文件中。
總之镶摘,將循環(huán)變成單向嗽桩。
3、包(package)import
在一個文件下同時有 init .py文件凄敢、和其他模塊文件時碌冶,該文件夾即看作一個包(package)。包的導入 和模塊導入基本一致涝缝,只是導入包時扑庞,會執(zhí)行這個 init .py譬重,而不是模塊中的語句。
而且罐氨,如果只是單純地導入包【形如:import xxx】臀规,而包的 init .py中有沒有明確地的其他初始化操作,則:此包下的模塊 是不會被自動導入的栅隐。當然該包是會成功導入的塔嬉,并將包名稱放入當前.py的Local命名空間中。
[D:youcaihua\test\PkgDemo\mod.py]文件
[D:youcaihua\test\PkgDemo\pkg1\pkg1_mod.py]文件
[D:youcaihua\test\PkgDemo\pkg2\pkg2_mod.py]文件租悄,三個文件同樣的代碼:
def getName():
print(name)
if name == 'main':
getName()
1
2
3
4
5
[D:youcaihua\test\test.py]文件
import PkgDemo.mod#1
print(locals(),'\n')
import PkgDemo.pkg1#2
print(locals(),'\n')
import PkgDemo.pkg1.pkg1_mod as m1#3
print(locals(),'\n')
import PkgDemo.pkg2.pkg2_mod#4
PkgDemo.mod.getName()#5
print('調用mod.py----', locals(), '\n')
m1.getName()#6
PkgDemo.pkg2.pkg2_mod.getName()#7
1
2
3
4
5
6
7
8
9
10
11
執(zhí)行 #1 后谨究,將PkgDemo、PkgDemo.mod加入sys.modules中泣棋,此時可調用PkgDemo.mod的任何類记盒、或函數。當不能調用包PkgDemo.pkg1或pkg2下任何模塊外傅。但當前test.py文件Local命名空間中只有 PkgDemo。
執(zhí)行 #2 后俩檬,只是將PkgDemo.pkg1載入內存萎胰,sys.modules會有PkgDemo、PkgDemo.mod棚辽、PkgDemo.pkg1 三個模塊技竟。但PkgDemo.pkg1下的任何模塊 都沒有自動載入內存,所以在此時:PkgDemo.pkg1.pkg1_mod.getName()將會出錯屈藐。當前test.py的Local命名空間依然只有PkgDemo榔组。
執(zhí)行 #3 后,會將pkg1_mod載入內存联逻,sys.modules會有PkgDemo搓扯、PkgDemo.mod、PkgDemo.pkg1包归、PkgDemo.pkg1.pkg1_mod四個模塊锨推,此時可執(zhí)行PkgDemo.pkg1.pkg1_mod.getName()。由于使用了as公壤,當前Local命名空間將另外添加m1(作為PkgDemo.pkg1.pkg1_mod的別名)换可、當然還有PkgDemo。
執(zhí)行 #4 后厦幅,會將PkgDemo.pkg2沾鳄、PkgDemo.pkg2.pkg2_mod載入內存,sys.modules中會有PkgDemo确憨、PkgDemo.mod译荞、PkgDemo.pkg1瓤的、PkgDemo.pkg1.pkg1_mod、PkgDemo.pkg2磁椒、PkgDemo.pkg2.pkg2_mod六個模塊堤瘤,當然:當前Local命名空間還是只有PkgDemo、m1浆熔。
5本辐、#6、#7當然都可正確執(zhí)行医增。
三慎皱、How to avoid Python circle import error?如何避免Python的循環(huán)導入問題叶骨?
代碼布局茫多、(架構)設計問題,解決之道是:將循環(huán)變成單向忽刽。采用分層天揖、用時導入、相對導入(層次建議不要超過兩個)
注意:在命令行執(zhí)行Python xx.py跪帝、與IDE中執(zhí)行今膊,結果可能不同。