簡體   English   中英

Python導入編碼風格

[英]Python import coding style

我發現了一種新的模式。 這種模式是眾所周知的還是對它的看法是什么?

基本上,我很難刷新源文件以找出可用的模塊導入等等,所以現在,而不是

import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

我將所有導入移動到它們實際使用的函數中,如下所示:

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

這做了一些事情。 首先,我很少意外地用其他模塊的內容污染我的模塊。 我可以為模塊設置__all__變量,但隨后我必須在模塊發展時更新它,這對於實際存在於模塊中的代碼無法幫助實現名稱空間污染。

其次,我很少在我的模塊頂部進行一連串的進口,其中一半或更多我不再需要,因為我已經重構了它。 最后,我發現這個模式更容易閱讀,因為每個引用的名稱都在函數體中。

這個問題的(先前) 最高投票答案格式很好,但性能絕對錯誤。 讓我來證明一下

性能

頂級導入

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

在函數體中導入

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

如您所見,在函數中導入模塊可能有效。 這樣做的原因是簡單的。 它將引用從全局引用移動到本地引用。 這意味着,至少對於CPython,編譯器將發出LOAD_FAST指令而不是LOAD_GLOBAL指令。 顧名思義,這些更快。 另一個回答者通過在循環的每個迭代中導入來人為地誇大了查看sys.modules的性能。

通常,最好在頂部導入,但如果您多次訪問模塊,性能不是原因。 原因是人們可以更容易地跟蹤模塊所依賴的內容,並且這樣做與Python Universe的其余部分一致。

這確實有一些缺點。

測試

如果您想通過運行時修改來測試模塊,可能會使其變得更加困難。 而不是做

import mymodule
mymodule.othermodule = module_stub

你必須這樣做

import othermodule
othermodule.foo = foo_stub

這意味着您必須全局修補othermodule,而不是僅僅更改mymodule中的引用指向的內容。

依賴性跟蹤

這使得模塊所依賴的模塊不明顯。 如果您使用許多第三方庫或重新組織代碼,這尤其令人惱火。

我不得不維護一些遺留代碼,這些代碼在整個地方使用了內聯導入,這使得代碼極難重構或重新打包。

關於表現的說明

由於python緩存模塊的方式,沒有性能損失。 實際上,由於模塊位於本地名稱空間中,因此在函數中導入模塊會有一些性能上的好處。

頂級導入

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py 

real   0m1.569s
user   0m1.560s
sys    0m0.010s

在函數體中導入

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s

這種方法存在一些問題:

  • 打開文件所依賴的模塊並不是很明顯。
  • 它會混淆必須分析依賴關系的程序,例如py2exepy2app等。
  • 您在許多功能中使用的模塊怎么樣? 您最終會得到大量的冗余導入,或者您必須在文件的頂部和一些內部函數中放置一些。

所以...首選的方法是將所有導入放在文件的頂部。 我發現如果我的導入很難跟蹤,通常意味着我有太多的代碼,我最好把它分成兩個或更多的文件。

在那里發現里面進口的功能是有用的一些情況:

  • 處理循環依賴(如果你真的無法避免它們)
  • 平台特定代碼

另外:在每個函數中放入導入實際上並不比文件頂部慢。 第一次加載每個模塊時,它被放入sys.modules ,每次后續導入只花費查找模塊的時間,這相當快(不會重新加載)。

另一個有用的事情是在Python 3.0中刪除了函數內部的from module import *語法。

這里簡單提一下“刪除的語法”:

http://docs.python.org/3.0/whatsnew/3.0.html

我建議你盡量避免from foo import bar導入。 我只在包內使用它們,其中拆分模塊是一個實現細節,無論如何都不會有很多。

在導入包的所有其他位置,只需使用import foo ,然后使用全名foo.bar引用它。 這樣,您始終可以告訴某個元素來自何處,而不必維護導入元素的列表(實際上,這將永遠過時並導入不再使用的元素)。

如果foo是一個非常長的名字,你可以使用import foo as f來簡化它,然后寫入f.bar 這仍然比保持所有的更為方便和明確from進口。

人們已經很好地解釋了為什么要避免內聯導入,而不是真正的替代工作流來解決你想要它們的原因。

我很難刷新源文件以找出可用的模塊導入等等

要檢查未使用的導入,我使用pylint 它執行靜態(ish) - Python代碼分析,它檢查的(很多)事情之一是未使用的導入。 例如,以下腳本..

import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

..將生成以下消息:

example.py:2 [W0611] Unused import urllib2

至於檢查可用的導入,我通常依賴於TextMate(相當簡單)的完成 - 當你按Esc時,它會在文檔中與其他人完成當前的單詞。 如果我已經完成了import urlliburll[Esc]將擴展為urllib ,如果不是,我跳轉到文件的開頭並添加導入。

從性能的角度來看,您可以看到: Python import語句是否始終位於模塊的頂部?

一般來說,我只使用本地導入來打破依賴循環。

我相信在某些情況/場景中這是一種推薦的方法。 例如,在Google App Engine中建議使用延遲加載大模塊,因為它可以最大限度地降低實例化新Python VM /解釋器的預熱成本。 看一下Google Engineer的演示文稿,描述這一點。 但請記住,這並不意味着您應該延遲加載所有模塊。

您可能想要查看python wiki中的Import 語句開銷 簡而言之:如果模塊已經加載(查看sys.modules ),您的代碼將運行sys.modules慢。 如果您的模塊尚未加載,並且foo只會在需要時加載,這可能是零次,那么整體性能會更好。

兩種變體都有其用途。 但是在大多數情況下,最好在函數之外導入,而不是在函數內部導入。

性能

在幾個答案中已經提到過,但在我看來,他們都缺乏完整的討論。

第一次在python解釋器中導入模塊時,無論它是在頂級還是在函數內部,它都會很慢。 它很慢,因為Python(我專注於CPython,它可能與其他Python實現不同)做了多個步驟:

  • 找到包裹。
  • 檢查包是否已經轉換為字節碼(着名的__pycache__目錄或.pyx文件),如果沒有,則將它們轉換為字節碼。
  • Python加載字節碼。
  • 加載的模塊放在sys.modules

后續導入不必執行所有這些操作,因為Python只能從sys.modules返回模塊。 因此后續進口將更快。

可能是模塊中的函數實際上並未經常使用,但它依賴於import很長的import 然后你可以實際移動函數內的import 這將使您的模塊更快地導入(因為它不必立即導入長加載包)但是當最終使用該函數時,它在第一次調用時會很慢(因為那時必須導入模塊)。 這可能會對感知性能產生影響,因為不會減慢所有用戶的速度,而只會減慢那些使用依賴於慢速加載依賴性的函數的速度。

但是sys.modules的查找不是免費的。 它速度非常快,但並不是免費的。 因此,如果你實際上經常調用一個import sa包的函數,你會注意到性能略有下降:

import random
import itertools

def func_1():
    return random.random()

def func_2():
    import random
    return random.random()

def loopy(func, repeats):
    for _ in itertools.repeat(None, repeats):
        func()

%timeit loopy(func_1, 10000)
# 1.14 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit loopy(func_2, 10000)
# 2.21 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

這幾乎慢了兩倍。

非常重要的是要意識到aaronasterling在答案中“作弊”了一下 他表示,在函數中進行導入實際上會使函數更快。 在某種程度上,這是事實。 那是因為Python如何查找名稱:

  • 它首先檢查本地范圍。
  • 它接下來檢查周圍的范圍。
  • 然后檢查下一個周圍范圍
  • ...
  • 檢查全局范圍。

因此,不是檢查本地范圍然后檢查全局范圍,而是檢查本地范圍,因為模塊的名稱在本地范圍內可用。 這實際上使它更快! 但這是一種稱為“循環不變代碼運動”的技術 它基本上意味着通過在循環(或重復調用)之前將其存儲在變量中來減少循環(或重復)中完成的事情的開銷。 因此,您可以簡單地使用變量並將其分配給全局名稱,而不是在函數中import它:

import random
import itertools

def f1(repeats):
    "Repeated global lookup"
    for _ in itertools.repeat(None, repeats):
        random.random()

def f2(repeats):
    "Import once then repeated local lookup"
    import random
    for _ in itertools.repeat(None, repeats):
        random.random()

def f3(repeats):
    "Assign once then repeated local lookup"
    local_random = random
    for _ in itertools.repeat(None, repeats):
        local_random.random()

%timeit f1(10000)
# 588 µs ± 3.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f2(10000)
# 522 µs ± 1.95 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f3(10000)
# 527 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

雖然您可以清楚地看到對全局random重復查找速度很慢,但在函數內部導入模塊或在函數內部的變量中分配全局模塊之間幾乎沒有區別。

這可以通過避免循環內的函數查找來達到極限:

def f4(repeats):
    from random import random
    for _ in itertools.repeat(None, repeats):
        random()

def f5(repeats):
    r = random.random
    for _ in itertools.repeat(None, repeats):
        r()

%timeit f4(10000)
# 364 µs ± 9.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(10000)
# 357 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

再快得多,但導入和變量之間幾乎沒有差別。

可選的依賴項

有時進行模塊級導入實際上可能是個問題。 例如,如果您不想添加另一個安裝時依賴項,但該模塊對某些其他功能非常有幫助。 決定依賴項是否應該是可選的不應該輕易完成,因為它會影響用戶(如果他們遇到意外的ImportError或者錯過了“酷炫的功能”),它會使安裝包含所有功能的包更復雜,普通依賴項pipconda (僅提及兩個軟件包管理器)開箱即用,但對於可選的依賴項,用戶必須在以后手動安裝軟件包(有一些選項可以自定義需求,但是將“正確”安裝它的負擔放在用戶身上。

但是,這可以通過兩種方式完成:

try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

def function_that_requires_matplotlib():
    plt.plot()

要么:

def function_that_requires_matplotlib():
    import matplotlib.pyplot as plt
    plt.plot()

通過提供替代實現或自定義用戶看到的異常(或消息),可以更加自定義,但這是主要要點。

如果想要為可選依賴項提供替代“解決方案”,那么頂級方法可能會更好一些,但通常人們使用函數內導入。 主要是因為它導致更清晰的堆棧跟蹤並且更短。

循環進口

函數內導入可以非常有助於避免由於循環導入導致的ImportErrors。 在很多情況下,圓形進口是“壞”包裝結構的標志,但如果絕對沒有辦法避免循環導入,則通過將導入圓圈的進口放入“循環”(以及問題)來解決實際使用它的功能。

不要重復自己

如果您實際將所有導入放在函數而不是模塊范圍中,則會引入冗余,因為函數可能需要相同的導入。 這有一些缺點:

  • 您現在有多個地方可以檢查是否有任何導入已過時。
  • 如果你錯誤地導入了一些導入,你只能在運行特定功能時找到,而不是在加載時。 因為你有更多的import語句錯誤的可能性增加(不多),它只是變得更加重要,以測試所有功能。

其他想法:

我很少在我的模塊頂部有一連串的進口,其中一半或更多我不再需要,因為我已經重構了它。

大多數IDE已經有一個未使用導入的檢查器,因此可能只需點擊幾下就可以刪除它們。 即使您不使用IDE,也可以偶爾使用靜態代碼檢查器腳本並手動修復它。 另一個答案提到了pylint,但還有其他答案(例如pyflakes)。

我很少意外地用其他模塊的內容污染我的模塊

這就是為什么你通常使用__all__和/或定義你的函數子模塊,只導入主模塊中的相關類/函數/ ...,例如__init__.py

此外,如果您認為您過多地污染了模塊名稱空間,那么您可能應該考慮將模塊拆分為子模塊,但這只對幾十個導入有意義。

如果要減少命名空間污染,還有一個(非常重要的)要點是避免from module import *導入。 但您可能還想避免from module import a, b, c, d, e, ...導入導入太多名稱的導入,只需導入模塊並使用module.c訪問這些函數。

作為最后的手段,您始終可以使用別名來避免使用“ import random as _random ”導入來污染命名空間: import random as _random 這將使代碼更難理解,但它非常清楚應該公開顯示什么,什么不應該。 這不是我建議的,你應該保持__all__列表是最新的(這是推薦和明智的方法)。

摘要

  • 性能影響是可見的,但幾乎總是微觀優化,所以不要讓決定在哪里進行微觀基准測試。 除非首次import依賴性非常慢,並且它僅用於功能的一小部分。 然后,對於大多數用戶來說,它實際上可以對模塊的感知性能產生明顯的影響。

  • 使用通常理解的工具來定義公共API,我的意思是__all__變量。 保持最新狀態可能有點煩人,但是檢查過時導入的所有功能或添加新功能以添加該功能中的所有相關導入也是如此。 從長遠來看,你可能不得不通過更新__all__來做更少的工作。

  • 你更喜歡哪一個並不重要,兩者都有效。 如果你是獨自工作,你可以推斷出利弊,做一個你認為最好的。 但是,如果你在一個團隊中工作,你可能應該堅持使用已知模式(這將是使用__all__頂級導入),因為它允許他們做他們(可能)一直做的事情。

安全實施

考慮一個環境,其中所有Python代碼都位於只有特權用戶才能訪問的文件夾中。 為了避免以特權用戶身份運行整個程序,您決定在執行期間將權限刪除給非特權用戶。 只要您使用導入另一個模塊的函數,您的程序就會拋出一個ImportError因為由於文件權限,非特權用戶無法導入模塊。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM