您好,登錄后才能下訂單哦!
本篇內容介紹了“Python函數、遞歸和閉包怎么用”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
我們已經接觸過數據類型和不變性中的函數。如果你還沒有讀過那篇文章,我建議你現在回去看看。
讓我們看一個函數的簡單示例,以確保我們在同一維度。
def cheer(volume=None): if volume is None: print("yay.") elif volume == "Louder!": print("yay!") elif volume == "LOUDER!": print("*deep breath*") print("yay!") cheer() # prints "yay." cheer("Louder!") # prints "yay!" cheer("LOUDER!") # prints "*deep breath* ...yay!"
這里沒有什么令人驚訝的。函數cheer()
接受單個參數volume
。如果我們不為volume
傳遞參數,它將默認為None
。
我們這些來自面向對象編程語言的人已經學會了從類和對象的角度來思考一切。數據被組織成對象,以及負責訪問和修改該數據的函數。有時這可行,但有時,類開始感覺像太多模板。
函數式編程幾乎與此相反。我們圍繞函數進行組織,我們通過這些函數傳遞數據。我們必須遵守一些規則:
函數應該只接受輸入,只產生輸出。
函數不應該有副作用;他們不應該修改任何外部的東西。
函數應該(理想情況下)總是為相同的輸入產生相同的輸出。函數內部不應該有會破壞這種模式的狀態。
現在,在你將所有 Python 代碼重寫為純函數式之前,停止!不要忘記 Python 的一大優點是它是一種多范式語言。你不必選擇一種范式并堅持下去;你可以在你的代碼中混合搭配,這樣是最好的。
事實上,我們已經這樣做了!迭代器和生成器都是從函數式編程中參考而來的,它們與對象一起工作得很好。也可以隨意合并 lambda、裝飾器和閉包。這一切都是為了選擇最適合這項工作的工具。
在實踐中,我們很少能同時避免副作用。在將函數式編程的概念應用到 Python 代碼中時,你應該更多地關注和慎重考慮副作用,而不是完全避免它們。將它們限制在沒有更好方法解決問題的情況下。這里沒有硬性規定可以依靠。你需要培養自己的洞察力。
當一個函數調用自身時,這稱為遞歸。當我們需要重復函數的整個邏輯,但循環不合適(或者感覺太混亂)時,這會很有幫助。
注意:我下面的示例被簡化以突出遞歸本身。這實際上并不是遞歸是最佳方法的情況。當你需要對不同的數據塊重復調用復雜的語言時,例如當你遍歷樹結構時,遞歸 會更好。
import random random.seed() class Villain: def __init__(self): self.defeated = False def confront(self): # Roll of the dice. if random.randint(0,10) == 10: self.defeated = True def defeat(villain): villain.confront() if villain.defeated: print("Yay!") return True else: print("Keep trying...") return defeat(villain) starlight = Villain() victory = defeat(starlight) if victory: print("YAY!")
與random
相關的東西可能看起來很新。它與這個主題并沒有真正相關,但簡而言之,我們可以通過在程序開始時用隨機數生成器生成隨機數 (random.seed()
),然后調用random.randint(min, max)
, 函數里面的min
和max
定義可能值的包含范圍。
這里邏輯的重要部分是defeat()
函數。只要villain 沒有被打敗,函數就會調用自己,傳遞villain
變量,直到其中一個函數調用返回一個值。在這種情況下,該值在遞歸調用堆棧中返回,最終存儲在victory
.
不管花多長時間*
,我們最終都會打敗那個villain 。
遞歸可以是一個強大的工具,但它也會帶來一個問題:如果我們沒有辦法停下來怎么辦?
def mirror_pool(lookers): reflections = [] for looker in lookers: reflections.append(looker) lookers.append(reflections) print(f"We have {len(lookers) - 1} duplicates.") return mirror_pool(lookers) duplicates = mirror_pool(["Pinkie Pie"])
顯然,這將永遠運行!一些語言沒有提供一種干凈的方式來處理這個問題——函數只會無限遞歸,直到崩潰。
Python 更優雅地阻止了這種瘋狂。一旦達到設定的遞歸深度(通常為 997-1000 次),它就會停止整個程序并引發錯誤:
RecursionError:調用 Python 對象時超出最大遞歸深度
像所有錯誤一樣,我們可以在事情失控之前發現它:
try: duplicates = mirror_pool(["Pinkie Pie"]) except RecursionError: print("Time to watch paint dry.")
值得慶幸的是,由于我編寫這段代碼的方式,我實際上不需要做任何特別的事情來清理 997 個重復項。遞歸函數從未返回,因此duplicates
在這種情況下保持未定義。
但是,我們可能希望以另一種方式控制遞歸,因此我們不必使用 try-except
來防止災難。在我們的遞歸函數中,我們可以通過添加一個參數來跟蹤它被調用的次數calls
,并在它變得太大時立即中止。
def mirror_pool(lookers, calls=0): calls += 1 reflections = [] for looker in lookers: reflections.append(looker) lookers.append(reflections) print(f"We have {len(lookers) - 1} duplicates.") if calls < 20: lookers = mirror_pool(lookers, calls) return lookers duplicates = mirror_pool(["Pinkie Pie"]) print(f"Grand total: {len(duplicates)} Pinkie Pies!")
我們仍然需要弄清楚如何在不丟失原始數據的情況下刪除 20 個重復項,但至少程序沒有崩潰。
注意:你可以使用sys.setrecursionlimit(n)
覆蓋最大遞歸級別,其中n
是你想要的最大值。
有時,我們可能想要在一個函數中重用一段邏輯,但我們不想通過創建另一個函數來弄亂我們的代碼。
def use_elements(target): elements = ["Honesty", "Kindness", "Laughter", "Generosity", "Loyalty", "Magic"] def use(element, target): print(f"Using Element of {element} on {target}.") for element in elements: use(element, target) use_elements("Nightmare Moon")
當然,這個簡單的例子的問題在于它的用處不是很明顯。當我們想要將大量邏輯抽象為函數以實現可重用性但又不想在主函數之外定義時,嵌套函數會變得很有幫助。如果use()
函數要復雜得多,并且可能不僅僅是循環調用,那么這種設計將是合理的。
盡管如此,該示例的簡單性仍然體現了基本概念。這也帶來了另一個困難。你會注意到,每次我們調用它時use()
,我們都在傳遞target
給內部函數,這感覺毫無意義。我們不能只使用已經在本地范圍內的target
變量嗎?
事實上,我們可以:
def use_elements(target): elements = ["Honesty", "Kindness", "Laughter", "Generosity", "Loyalty", "Magic"] def use(element): print(f"Using Element of {element} on {target}.") for element in elements: use(element) use_elements("Nightmare Moon")
然而,一旦我們嘗試修改該變量,我們就會遇到麻煩:
def use_elements(target): elements = ["Honesty", "Kindness", "Laughter", "Generosity", "Loyalty", "Magic"] def use(element): print(f"Using Element of {element} on {target}.") target = "Luna" for element in elements: use(element) print(target) use_elements("Nightmare Moon")
運行該代碼會引發錯誤:
UnboundLocalError: local variable 'target' referenced before assignment
顯然,它不再認識我們的局部變量target
。這是因為默認情況下,分配給變量時會覆蓋封閉范圍中的任何已有相同的變量。因此,該行target == "Luna"
試圖創建一個限制在use()
范圍內的新變量,并在use_elements()
的封閉范圍內隱藏已有的target
變量。與Python 看到了這一點并假設,因為我們在 use()
函數中定義了target
,所以對該變量的所有引用都與該本地名稱相關。這不是我們想要的!
關鍵字nonlocal
允許我們告訴內部函數我們正在使用來自封閉局部范圍的target
變量。
def use_elements(target): elements = ["Honesty", "Kindness", "Laughter", "Generosity", "Loyalty", "Magic"] def use(element): nonlocal target print(f"Using Element of {element} on {target}.") target = "Luna" for element in elements: use(element) print(target) use_elements("Nightmare Moon")
現在,運行結果,我們看到了打印出來的值Luna
。我們在這里的工作完成了!
注意:如果你希望函數能夠修改定義為全局范圍(在所有函數之外)的變量,請使用global
關鍵字而不是nonlocal
。
基于嵌套函數的思想,我們可以創建一個函數,該函數實際上構建并返回另一個函數(稱為閉包)。
def harvester(pony): total_trees = 0 def applebucking(trees): nonlocal pony, total_trees total_trees += trees print(f"{pony} harvested from {total_trees} trees so far.") return applebucking apple_jack = harvester("Apple Jack") big_mac = harvester("Big Macintosh") apple_bloom = harvester("Apple Bloom") north_orchard = 120 west_orchard = 80 # watch out for fruit bats east_orchard = 135 south_orchard = 95 near_house = 20 apple_jack(west_orchard) big_mac(east_orchard) apple_bloom(near_house) big_mac(north_orchard) apple_jack(south_orchard)
在此示例中,applebucking()
就是閉包,因為它關閉了非局部變量pony
和total_trees
。即使在外部函數終止后,閉包仍保留對這些變量的引用。
閉包從harvester()
函數返回,并且可以像任何其他對象一樣存儲在變量中。正是因為它“關閉”了一個非局部變量,這使得它本身就是一個閉包。否則,它只是一個函數。
在這個例子中,我使用閉包來有效地創建帶有狀態的對象。換句話說,每個收割機都記得他或她從多少棵樹上收割過。這種特殊用法并不嚴格符合函數式編程,但如果你不想創建一個完整的類來存儲一個函數的狀態,它會非常有用!
apple_jack
, big_macintosh
, 和apple_bloom
現在是三個不同的函數,每個函數都有自己獨立的狀態;他們每個人都有不同的名字,并記住他們收獲了多少棵樹。在一個閉包狀態中發生的事情對其他閉包狀態沒有影響,他們都是獨立的個體。
當我們運行代碼時,我們看到了這個狀態:
Apple Jack harvested from 80 trees so far. Big Macintosh harvested from 135 trees so far. Apple Bloom harvested from 20 trees so far. Big Macintosh harvested from 255 trees so far. Apple Jack harvested from 175 trees so far.
閉包本質上是“隱式類”,因為它們將功能及其持久信息(狀態)放在同一個對象中。然而,閉包有幾個獨特的缺點:
你不能按原樣訪問“成員變量”。在我們的示例中,我永遠無法訪問閉包apple_jack
上的變量total_trees
!我只能在閉包自己的代碼的上下文中使用該變量。
關閉狀態是完全不透明的。除非你知道閉包是如何編寫的,否則你不知道它記錄了哪些信息。
由于前面兩點,根本不可能直接知道閉包何時具有任何狀態。
使用閉包時,你需要準備好處理這些問題,以及它們帶來的所有調試困難。我建議僅在你需要單個函數來存儲調用之間的少量私有狀態時才使用它們,并且僅在代碼中如此有限的時間段內使用它們,以至于編寫整個類并不合理。(另外,不要忘記生成器和協程,它們可能更適合許多此類場景。)
閉包仍然可以成為 Python中有用的部分,只要你非常小心地使用它們。
lambda是由單個表達式組成的匿名函數(無名稱)。
僅此定義就是許多程序員無法想象他們為什么需要一個定義的原因。編寫一個沒有名稱的函數有什么意義,基本上使重用完全不切實際?當然,你可以將lambda 分配給一個變量,但此時,你不應該剛剛編寫了一個函數嗎?
為了理解這一點,讓我們先看一個沒有lambdas 的例子:
class Element: def __init__(self, element, color, pony): self.element = element self.color = color self.pony = pony def __repr__(self): return f"Element of {self.element} ({self.color}) is attuned to {self.pony}" elements = [ Element("Honesty", "Orange", "Apple Jack"), Element("Kindness", "Pink", "Fluttershy"), Element("Laughter", "Blue", "Pinkie Pie"), Element("Generosity", "Violet", "Rarity"), Element("Loyalty", "Red", "Rainbow Dash"), Element("Magic", "Purple", "Twilight Sparkle") ] def sort_by_color(element): return element.color elements = sorted(elements, key=sort_by_color) print(elements)
我希望你注意的主要事情是sort_by_color()
函數,我必須編寫該函數以明確按顏色對列表中的 Element 對象進行排序。實際上,這有點煩人,因為我再也不需要那個功能了。
這就是 lambdas 的用武之地。我可以刪除整個函數,并將elements = sorted(...)
行更改為:
elements = sorted(elements, key=lambda e: e.color)
使用 lambda 可以讓我準確地描述我的邏輯我在哪里使用它,而不是在其他任何地方。(這key=
部分只是表明我將 lambda 傳遞給sorted()
函數的key
的參數。)
一個 lambda 具有結構lamba <parameters>: <return expression>
。它可以收集任意數量的參數,用逗號分隔,但它只能有一個表達式,其值是隱式返回的。
注意:與常規函數不同,Lambda 不支持類型注釋(類型提示)。
如果我想重寫那個 lambda 以按元素的名稱而不是顏色排序,我只需要更改表達式部分:
elements = sorted(elements, key=lambda e: e.name)
就這么簡單。
同樣,lambda在需要將帶有單個表達式的函數傳遞給另一個函數時非常有用。這是另一個示例,這次在 lambda 上使用了更多參數。
為了設置這個示例,讓我們從 Flyer 的類開始,它存儲名稱和最大速度,并返回 Flyer 的隨機速度。
import random random.seed() class Flyer: def __init__(self, name, top_speed): self.name = name self.top_speed = top_speed def get_speed(self): return random.randint(self.top_speed//2, self.top_speed)
我們希望能夠讓任何給定的 Flyer 對象執行任何飛行技巧,但是將所有這些邏輯放入類本身是不切實際的……可能有成千上萬的飛行技巧和變體!
Lambdas是定義這些技巧的一種方式。我們將首先向該類添加一個函數,該函數可以接受函數作為參數。我們假設這個函數總是有一個參數:執行技巧的速度。
def perform(self, trick): performed = trick(self.get_speed()) print(f"{self.name} perfomed a {performed}")
要使用它,我們創建一個 Flyer 對象,然后將函數傳遞給它的perform()
方法。
rd = Flyer("Rainbow Dash", 780) rd.perform(lambda s: f"barrel-roll at {s} mph.") rd.perform(lambda s: f"flip at {s} mph.")
因為 lambda 的邏輯在函數調用中,所以更容易看到發生了什么。
回想一下,你可以將 lambdas 存儲在變量中。當你希望代碼如此簡短但需要一些可重用性時,這實際上會很有幫助。例如,假設我們有另一個 Flyer,我們希望他們兩個都進行barrelroll。
spitfire = Flyer("Spitfire", 650) barrelroll = lambda s: f"barrel-roll at {s} mph." spitfire.perform(barrelroll) rd.perform(barrelroll)
當然,我們可以將barrelroll
寫成一個適當的單行函數,但是通過這種方式,我們為自己節省了一些樣板文件。而且,由于在這段代碼之后我們不會再次使用該邏輯,因此沒有必要再使用一個成熟的函數。
再一次,可讀性很重要。Lambda 非常適合用于簡短、清晰的邏輯片段,但如果你有更復雜的事情,你還是應該編寫一個合適的函數。
假設我們想要修改任何函數的行為,而不實際更改函數本身。
讓我們從一個相當基本的函數開始:
def partial_transfiguration(target, combine_with): result = f"{target}-{combine_with}" print(f"Transfiguring {target} into {result}.") return result target = "frog" target = partial_transfiguration(target, "orange") print(f"Target is now a {target}.")
運行給我們:
Transfiguring frog into frog-orange. Target is now a frog-orange.
很簡單,對吧。但是,如果我們想為此添加一些額外的宣傳呢?如你所知,我們真的不應該將這種邏輯放在我們的partial_transfiguration
函數中。
這就是裝飾器的用武之地。裝飾器“包裝”了函數周圍的附加邏輯,這樣我們實際上不會修改原始函數本身。這使得代碼更易于維護。
讓我們從為大張旗鼓創建一個裝飾器開始。這里的語法一開始可能看起來有點過于復雜,但請放心,我會詳細介紹。
import functools def party_cannon(func): @functools.wraps(func) def wrapper(*args, **kwargs): print("Waaaaaaaait for it...") r = func(*args, **kwargs) print("YAAY! *Party cannon explosion*") return r return wrapper
你可能已經認識到wrapper()
它實際上是一個閉包,它是由我們的party_cannon()
函數創建并返回的。我們傳遞我們正在“裝飾”的函數,func
。
然而,我們真的對我們裝飾的函數一無所知!它可能有也可能沒有參數。閉包的參數列表(*args, **kwargs)
實際上可以接受任何數量的參數,從零到無窮大。調用func()
時,我們以相同的方式將這些參數傳遞給func()
。
當然,如果func()
上的參數列表與通過裝飾器傳遞給它的參數之間存在某種不匹配,則會引發通常和預期的錯誤(這顯然是一件好事)。
在wrapper()
內部,我們可以隨時隨地調用我們的函數func()
。我選擇在打印我的兩條消息之間這樣做。
我不想丟棄func()
返回的值,所以我將返回的值分配給r
,并確保在裝飾器的末尾用return r
.
請注意,對于在裝飾器中調用函數的方式,或者即使調用它或調用多少次,實際上并沒有硬性規定。你還可以以任何你認為合適的方式處理參數和返回值。關鍵是要確保裝飾器實際上不會以某種意想不到的方式破壞它裝飾的函數。
裝飾器前的奇數行,@functools.wraps(func)
,實際上是一個裝飾器本身。沒有它,被裝飾的函數本質上會混淆它自己的身份,弄亂我們對__doc__
(文檔字符串)和__name__
.。這個特殊的裝飾器確保不會發生這種情況;被包裝的函數保留自己的身份,可以從函數外部以所有常用方式訪問。(要使用那個特殊的裝飾器,我們必須首先import functools
。)
現在我們已經編寫了party_cannon
裝飾器,我們可以使用它來添加我們想要的partial_transfiguration()
函數。這樣做很簡單:
@party_cannon def partial_transfiguration(target, combine_with): result = f"{target}-{combine_with}" print(f"Transfiguring {target} into {result}.") return result
第一行,@party_cannon
是我們做出的唯一改變!partial_transfiguration
函數現在已裝飾。
注意:你甚至可以將多個裝飾器堆疊在一起,一個在下一個之上。只需確保每個裝飾器緊接在它所包裝的函數或裝飾器之前。
我們以前的用法根本沒有改變:
target = "frog" target = partial_transfiguration(target, "orange") print(f"Target is now a {target}.")
然而輸出確實發生了變化:
Waaaaaaaait for it... Transfiguring frog into frog-orange. YAAY! *Party cannon explosion* Target is now a frog-orange.
“Python函數、遞歸和閉包怎么用”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。