ホームページ > バックエンド開発 > Python チュートリアル > Python 関数、再帰、クロージャの使用方法

Python 関数、再帰、クロージャの使用方法

WBOY
リリース: 2023-05-10 12:19:06
転載
1019 人が閲覧しました

関数の再理解

データ型と不変性の関数についてはすでに説明しました。まだその記事を読んでいない場合は、今すぐ戻って読むことをお勧めします。

同じ次元にあることを確認するために、関数の簡単な例を見てみましょう。

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 の優れた点の 1 つは、Python が マルチパラダイム言語であることです。 1 つのパラダイムを選択してそれに固執する必要はなく、コード内で最も効果的なものを組み合わせて使用​​できます。

実際、これはすでに実行されています。イテレータとジェネレータはどちらも関数型プログラミングから参照され、オブジェクトとうまく連携します。ラムダ、デコレータ、クロージャも自由に組み合わせてください。すべては、仕事に最適なツールを選択することです。

実際には、副作用を同時に回避することはほとんどありません。関数型プログラミングの概念を 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() 関数です。悪役が倒されていない限り、関数呼び出しの 1 つが値を返すまで、関数は villain 変数を渡して自分自身を呼び出します。この場合、値は再帰呼び出しスタックに返され、最終的には victory.

どんなに時間がかかっても、最終的には敗北します。あの悪役。

無限再帰に注意してください

再帰は強力なツールになる可能性がありますが、問題も引き起こします:

停止する方法がない場合はどうすればよいでしょうか?

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()就是闭包,因为它关闭了非局部变量ponytotal_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中有用的部分,只要你非常小心地使用它们。

Lambdas

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>。它可以收集任意数量的参数,用逗号分隔,但它只能有<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&quot;{self.name} perfomed a {performed}&quot;)</pre><div class="contentsignin">ログイン後にコピー</div></div><p data-id="p838747a-UFTfLrZk">要使用它,我们创建一个 Flyer 对象,然后将函数传递给它的<code>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 関数、再帰、クロージャの使用方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:yisu.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート