我們已經接觸過資料型別和不變性中的函數。如果你還沒讀過那篇文章,我建議你現在回去看看。
讓我們看一個函數的簡單範例,以確保我們在同一維度。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
這裡沒有什麼令人驚訝的。函數cheer()
接受單一參數volume
。如果我們不為volume
傳遞參數,它將預設為None
。
我們這些來自物件導向程式語言的人已經學會了從類別和物件的角度來思考一切。資料被組織成對象,以及負責存取和修改該資料的函數。有時這可行,但有時,類別開始感覺像太多模板。
函數式程式設計幾乎與此相反。我們圍繞著函數進行組織,我們透過這些函數傳遞資料。我們必須遵守一些規則:
函數應該只接受輸入,只產生輸出。
函數不應該有副作用;他們不應該修改任何外部的東西。
函數應該(理想情況下)總是為相同的輸入產生相同的輸出。函數內部不應該有會破壞這種模式的狀態。
現在,在你將所有 Python 程式碼重寫為純函數式之前,停止! 不要忘記 Python 的一大優點是它是一種多範式語言。你不必選擇一種範式並堅持下去;你可以在你的程式碼中混合搭配,這是最好的。
事實上,我們已經這麼做了!迭代器和生成器都是從函數式程式設計中參考而來的,它們與物件一起運作得很好。也可以隨意合併 lambda、裝飾器和閉包。這一切都是為了選擇最適合這項工作的工具。
在實務上,我們很少能同時避免副作用。在將函數式程式設計的概念應用到 Python 程式碼中時,你應該更專注於並謹慎考慮副作用,而不是完全避免它們。將它們限制在沒有更好方法解決問題的情況下。這裡沒有硬性規定可以依靠。你需要培養自己的洞察力。
當一個函數呼叫自身時,這稱為遞歸。 當我們需要重複函數的整個邏輯,但循環不合適(或感覺太混亂)時,這會很有幫助。
注意:我下面的範例被簡化以突出遞歸本身。這其實並不是遞歸是最佳方法的情況。當你需要對不同的資料塊重複呼叫複雜的語言時,例如當你遍歷樹結構時,遞歸 會更好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
與random
相關的東西可能看起來很新。它與這個主題並沒有真正相關,但簡而言之,我們可以透過在程式開始時用隨機數產生器產生隨機數 (random.seed()
),然後呼叫random .randint(min, max)
, 函數裡面的min
和max
定義可能值的包含範圍。
這裡邏輯的重要部分是defeat()
函數。只要villain 沒有被打敗,函數就會呼叫自己,傳遞villain
變量,直到其中一個函數呼叫回傳一個值。在這種情況下,該值在遞歸呼叫堆疊中返回,最終儲存在victory
.
#不管花多長時間*
,我們最終都會打敗那個villain 。
遞迴可以是一個強大的工具,但它也會帶來一個問題:如果我們沒有辦法停下來怎麼辦?
1 2 3 4 5 6 7 8 9 10 11 12 |
|
顯然,這將永遠運行!有些語言沒有提供一種乾淨的方式來處理這個問題——函數只會無限遞歸,直到崩潰。
Python 更優雅地阻止了這種瘋狂。一旦達到設定的遞歸深度(通常為997-1000 次),它就會停止整個程式並引發錯誤:
RecursionError:呼叫Python 物件時超出最大遞歸深度
就像所有錯誤一樣,我們可以在事情失控之前發現它:
1 2 3 4 |
|
值得庆幸的是,由于我编写这段代码的方式,我实际上不需要做任何特别的事情来清理 997 个重复项。递归函数从未返回,因此duplicates
在这种情况下保持未定义。
但是,我们可能希望以另一种方式控制递归,因此我们不必使用 try-except
来防止灾难。在我们的递归函数中,我们可以通过添加一个参数来跟踪它被调用的次数calls
,并在它变得太大时立即中止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
我们仍然需要弄清楚如何在不丢失原始数据的情况下删除 20 个重复项,但至少程序没有崩溃。
注意:你可以使用sys.setrecursionlimit(n)
覆盖最大递归级别,其中n
是你想要的最大值。
有时,我们可能想要在一个函数中重用一段逻辑,但我们不想通过创建另一个函数来弄乱我们的代码。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
当然,这个简单的例子的问题在于它的用处不是很明显。当我们想要将大量逻辑抽象为函数以实现可重用性但又不想在主函数之外定义时,嵌套函数会变得很有帮助。如果use()
函数要复杂得多,并且可能不仅仅是循环调用,那么这种设计将是合理的。
尽管如此,该示例的简单性仍然体现了基本概念。这也带来了另一个困难。你会注意到,每次我们调用它时use()
,我们都在传递target
给内部函数,这感觉毫无意义。我们不能只使用已经在本地范围内的target
变量吗?
事实上,我们可以:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
然而,一旦我们尝试修改该变量,我们就会遇到麻烦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
运行该代码会引发错误:
UnboundLocalError: local variable 'target' referenced before assignment
显然,它不再认识我们的局部变量target
。这是因为默认情况下,分配给变量时会覆盖封闭范围中的任何已有相同的变量。因此,该行target == "Luna"
试图创建一个限制在use()
范围内的新变量,并在use_elements()
的封闭范围内隐藏已有的target
变量。与Python 看到了这一点并假设,因为我们在 use()
函数中定义了target
,所以对该变量的所有引用都与该本地名称相关。这不是我们想要的!
关键字nonlocal
允许我们告诉内部函数我们正在使用来自封闭局部范围的target
变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
现在,运行结果,我们看到了打印出来的值Luna
。我们在这里的工作完成了!
注意:如果你希望函数能够修改定义为全局范围(在所有函数之外)的变量,请使用global
关键字而不是nonlocal
。
基于嵌套函数的思想,我们可以创建一个函数,该函数实际上构建并返回另一个函数(称为闭包)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
在此示例中,applebucking()
就是闭包,因为它关闭了非局部变量pony
和total_trees
。即使在外部函数终止后,闭包仍保留对这些变量的引用。
闭包从harvester()
函数返回,并且可以像任何其他对象一样存储在变量中。正是因为它“关闭”了一个非局部变量,这使得它本身就是一个闭包。否则,它只是一个函数。
在这个例子中,我使用闭包来有效地创建带有状态的对象。换句话说,每个收割机都记得他或她从多少棵树上收割过。这种特殊用法并不严格符合函数式编程,但如果你不想创建一个完整的类来存储一个函数的状态,它会非常有用!
apple_jack
, big_macintosh
, 和apple_bloom
现在是三个不同的函数,每个函数都有自己独立的状态;他们每个人都有不同的名字,并记住他们收获了多少棵树。在一个闭包状态中发生的事情对其他闭包状态没有影响,他们都是独立的个体。
当我们运行代码时,我们看到了这个状态:
1 2 3 4 5 |
|
闭包本质上是“隐式类”,因为它们将功能及其持久信息(状态)放在同一个对象中。然而,闭包有几个独特的缺点:
你不能按原样访问“成员变量”。在我们的示例中,我永远无法访问闭包apple_jack
上的变量total_trees
!我只能在闭包自己的代码的上下文中使用该变量。
关闭状态是完全不透明的。除非你知道闭包是如何编写的,否则你不知道它记录了哪些信息。
由于前面两点,根本不可能直接知道闭包何时具有任何状态。
使用闭包时,你需要准备好处理这些问题,以及它们带来的所有调试困难。我建议仅在你需要单个函数来存储调用之间的少量私有状态时才使用它们,并且仅在代码中如此有限的时间段内使用它们,以至于编写整个类并不合理。(另外,不要忘记生成器和协程,它们可能更适合许多此类场景。)
闭包仍然可以成为 Python中有用的部分,只要你非常小心地使用它们。
lambda是由单个表达式组成的匿名函数(无名称)。
仅此定义就是许多程序员无法想象他们为什么需要一个定义的原因。编写一个没有名称的函数有什么意义,基本上使重用完全不切实际?当然,你可以将lambda 分配给一个变量,但此时,你不应该刚刚编写了一个函数吗?
为了理解这一点,让我们先看一个没有lambdas 的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
我希望你注意的主要事情是sort_by_color()
函数,我必须编写该函数以明确按颜色对列表中的 Element 对象进行排序。实际上,这有点烦人,因为我再也不需要那个功能了。
这就是 lambdas 的用武之地。我可以删除整个函数,并将elements = sorted(...)
行更改为:
1 |
|
使用 lambda 可以让我准确地描述我的逻辑我在哪里使用它,而不是在其他任何地方。(这key=
部分只是表明我将 lambda 传递给sorted()
函数的key
的参数。)
一个 lambda 具有结构lamba <parameters>: <return expression>。它可以收集任意数量的参数,用逗号分隔,但它只能有<em>一个</em>表达式,其值是隐式返回的。<p data-id="p838747a-crqqM7bU"><strong>注意:</strong>与常规函数不同,Lambda 不支持类型注释(类型提示)。</p><p data-id="p838747a-9hXHyGEp">如果我想重写那个 lambda 以按元素的名称而不是颜色排序,我只需要更改表达式部分:</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">elements = sorted(elements, key=lambda e: e.name)</pre><div class="contentsignin">登入後複製</div></div><p data-id="p838747a-g2D2su29">就这么简单。</p><p data-id="p838747a-X33Rgreh">同样,lambda在需要将带有单个表达式的函数传递给另一个函数时非常有用。这是另一个示例,这次在 lambda 上使用了更多参数。</p><p data-id="p838747a-s7QbfBkb">为了设置这个示例,让我们从 Flyer 的类开始,它存储名称和最大速度,并返回 Flyer 的随机速度。</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">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)</pre><div class="contentsignin">登入後複製</div></div><p data-id="p838747a-Wim71oX2">我们希望能够让任何给定的 Flyer 对象执行任何飞行技巧,但是将所有这些逻辑放入类本身是不切实际的……可能有成千上万的飞行技巧和变体!</p><p data-id="p838747a-Un7M8dUT">Lambdas是定义这些技巧的一种方式。我们将首先向该类添加一个函数,该函数可以接受函数作为参数。我们假设这个函数总是有一个参数:执行技巧的速度。</p><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">def perform(self, trick):
performed = trick(self.get_speed())
print(f"{self.name} perfomed a {performed}")</pre><div class="contentsignin">登入後複製</div></div><p data-id="p838747a-UFTfLrZk">要使用它,我们创建一个 Flyer 对象,然后将函数传递给它的<code>perform()
方法。
1 2 3 |
|
因为 lambda 的逻辑在函数调用中,所以更容易看到发生了什么。
回想一下,你可以将 lambdas 存储在变量中。当你希望代码如此简短但需要一些可重用性时,这实际上会很有帮助。例如,假设我们有另一个 Flyer,我们希望他们两个都进行barrelroll。
1 2 3 4 5 |
|
当然,我们可以将barrelroll
写成一个适当的单行函数,但是通过这种方式,我们为自己节省了一些样板文件。而且,由于在这段代码之后我们不会再次使用该逻辑,因此没有必要再使用一个成熟的函数。
再一次,可读性很重要。Lambda 非常适合用于简短、清晰的逻辑片段,但如果你有更复杂的事情,你还是应该编写一个合适的函数。
假设我们想要修改任何函数的行为,而不实际更改函数本身。
让我们从一个相当基本的函数开始:
1 2 3 4 5 6 7 8 9 |
|
运行给我们:
1 2 |
|
很简单,对吧。但是,如果我们想为此添加一些额外的宣传呢?如你所知,我们真的不应该将这种逻辑放在我们的partial_transfiguration
函数中。
这就是装饰器的用武之地。装饰器“包装”了函数周围的附加逻辑,这样我们实际上不会修改原始函数本身。这使得代码更易于维护。
让我们从为大张旗鼓创建一个装饰器开始。这里的语法一开始可能看起来有点过于复杂,但请放心,我会详细介绍。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
你可能已经认识到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()
函数。这样做很简单:
1 2 3 4 5 |
|
第一行,@party_cannon
是我们做出的唯一改变!partial_transfiguration
函数现在已装饰。
注意:你甚至可以将多个装饰器堆叠在一起,一个在下一个之上。只需确保每个装饰器紧接在它所包装的函数或装饰器之前。
我们以前的用法根本没有改变:
1 2 3 |
|
然而输出确实发生了变化:
1 2 3 4 |
|
以上是Python函數、遞歸和閉包怎麼用的詳細內容。更多資訊請關注PHP中文網其他相關文章!