Python 上級者向けガイド (簡単な上級プログラミング): 10. 効率的な関数を作成する

オリジナル: http://inventwithpython.com/beyond/chapter10.html

関数はプログラム内のミニプログラムのようなもので、コードをより小さな単位に分解することができます。これにより、バグの原因となる重複コードを作成する必要がなくなります。ただし、効率的な関数を作成するには、名前付け、サイズ、パラメーター、および複雑さについて多くの決定を下す必要があります。

この章では、関数を記述するさまざまな方法と、さまざまなトレードオフの長所と短所について説明します。*小さい関数と大きい関数の間のトレードオフ、パラメーターの数が関数の複雑さにどのように影響するか、 and演算子を使用して可変数のパラメーターを持つ関数を記述する方法について詳しく説明します**また、関数型プログラミングのパラダイムと、それに従って関数を作成する利点についても説明します。

関数名

関数名は、第 4 章で説明した識別子の規則に従う必要があります。ただし、関数は通常何らかのアクションを実行するため、通常は動詞を含める必要があります。名詞を使って何が起こっているかを説明することもできます。たとえば、名前refreshConnection()その機能が何をするのか、何をするのかを明確にします。setPassword()extract_version()

クラスまたはモジュールの一部であるメソッドの場合、おそらく名詞は必要ありません。SatelliteConnectionクラス内のメソッドreset()またはwebbrowserモジュール内の関数はopen()、必要なコンテキストを既に提供しています。サテライト接続がリセットされている項目であり、Web ブラウザーが開かれている項目であることがわかります。

省略された名前や短すぎる名前よりも、長くわかりやすい名前を使用することをお勧めします。数学者は、gcd()という名前の関数が 2 つの数値の最大公約数を返すことをすぐに理解するかもしれませんが、他の人はそれがgetGreatestCommonDenominator()より有益であることに気付くでしょう。

allanydate、 、emailfileformathashid、などのPythoninput組み込み関数またはモジュール名は使用ないでくださいlistminmaxobjectopenrandomsetstrsumtesttype

関数サイズのトレードオフ

一部のプログラマーは、関数はできるだけ短くする必要があり、1 つの画面に収まらないようにする必要があります。少なくとも数百行の関数と比較すると、わずか数十行の関数は比較的簡単に理解できます。ただし、コードを複数の小さな関数に分割して関数を短縮することには、マイナス面もあります。小さな関数のいくつかの利点を見てみましょう。

  • この関数のコードは理解しやすいです。
  • 関数に必要な引数が少なくなる場合があります。
  • 172 ページの「関数型プログラミング」で説明されているように、この関数に副作用がある可能性はほとんどありません。
  • この関数は、テストとデバッグが容易です。
  • 関数は、さまざまな種類の例外を発生させる可能性があります。

ただし、短い関数にはいくつかの欠点もあります。

  • 短い関数を書くということは、通常、プログラム内の関数が増えることを意味します。
  • より多くの関数を持つことは、より複雑なプログラムを意味します。
  • より多くの機能を持つということは、より説明的で正確な名前を考え出す必要があることも意味し、これは大変な作業です。
  • より多くの関数を使用すると、より多くのドキュメントを作成する必要があります。
  • 関数間の関係はより複雑になります。

一部の人々は、「短いほど良い」というガイドラインを極端に取り、すべての関数はせいぜい 3 ~ 4 行のコードであると主張します。狂ってる。たとえば、第 14 章のハノイの塔ゲームの関数は次のとおりですgetPlayerMove()これらのコードがどのように機能するかの詳細は重要ではありません。この関数の一般的な構造を見てください。

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # Keep asking player until they enter a valid move.
        print('Enter the letters of "from" and "to" towers, or QUIT.')
        print("(e.g. AB to moves a disk from tower A to tower B.)")
        print()
        response = input("> ").upper().strip()

        if response == "QUIT":
            print("Thanks for playing!")
            sys.exit()

        # Make sure the user entered valid tower letters:
        if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
            print("Enter one of AB, AC, BA, BC, CA, or CB.")
            continue  # Ask player again for their move.

        # Use more descriptive variable names:
        fromTower, toTower = response[0], response[1]

        if len(towers[fromTower]) == 0:
            # The "from" tower cannot be an empty tower:
            print("You selected a tower with no disks.")
            continue  # Ask player again for their move.
        elif len(towers[toTower]) == 0:
            # Any disk can be moved onto an empty "to" tower:
            return fromTower, toTower
        elif towers[toTower][-1] < towers[fromTower][-1]:
            print("Can't put larger disks on top of smaller ones.")
            continue  # Ask player again for their move.
        else:
            # This is a valid move, so return the selected towers:
            return fromTower, toTower

この関数は 34 行の長さです。プレイヤーが手を入力できるようにする、手が有効かどうかを確認する、手が無効な場合はプレイヤーに再度手を入力するように求めるなど、複数のタスクをカバーしますが、これらのタスクはプレイヤーの動く。一方、短い関数を書くことに専念している場合は、getPlayerMove()コードを次のように小さな関数に分割できます。

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # Keep asking player until they enter a valid move.
        response = askForPlayerMove()
 terminateIfResponseIsQuit(response)
        if not isValidTowerLetters(response):
            continue # Ask player again for their move.

        # Use more descriptive variable names:
        fromTower, toTower = response[0], response[1]

        if towerWithNoDisksSelected(towers, fromTower):
            continue  # Ask player again for their move.
        elif len(towers[toTower]) == 0:
            # Any disk can be moved onto an empty "to" tower:
            return fromTower, toTower
        elif largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
            continue  # Ask player again for their move.
        else:
            # This is a valid move, so return the selected towers:
            return fromTower, toTower

def askForPlayerMove():
    """Prompt the player, and return which towers they select."""
    print('Enter the letters of "from" and "to" towers, or QUIT.')
    print("(e.g. AB to moves a disk from tower A to tower B.)")
    print()
    return input("> ").upper().strip()

def terminateIfResponseIsQuit(response):
    """Terminate the program if response is 'QUIT'"""
    if response == "QUIT":
        print("Thanks for playing!")
        sys.exit()

def isValidTowerLetters(towerLetters):
    """Return True if `towerLetters` is valid."""
    if towerLetters not in ("AB", "AC", "BA", "BC", "CA", "CB"):
        print("Enter one of AB, AC, BA, BC, CA, or CB.")
        return False
    return True

def towerWithNoDisksSelected(towers, selectedTower):
    """Return True if `selectedTower` has no disks."""
    if len(towers[selectedTower]) == 0:
        print("You selected a tower with no disks.")
        return True
    return False

def largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
    """Return True if a larger disk would move on a smaller disk."""
    if towers[toTower][-1] < towers[fromTower][-1]:
        print("Can't put larger disks on top of smaller ones.")
        return True
    return False

これらの 6 つの関数の長さは 56 行で、元のコードの行数のほぼ 2 倍ですが、実行するタスクは同じです。各機能は元の よりも理解しやすいものですがgetPlayerMove()、それらを組み合わせると複雑さが増します。コードの読者は、コードがどのように組み合わされているかを理解するのに苦労するかもしれません。getPlayerMove()function は、プログラムの残りの部分から呼び出される唯一の関数です。他の 5 つの関数は、 から 1 回だけ呼び出されますgetPlayerMove()しかし、機能の質はこの事実を伝えていません。

defまた、新しい関数ごとに新しい名前とドキュメント文字列 (各ステートメントの下にあるトリプル クォートで囲まれた文字列。第 11 章で詳しく説明します) を考え出す必要がありました。getPlayerMove()これにより、や などの関数名がわかりにくくなりますaskForPlayerMove()さらに、getPlayerMove()これはまだ 3 行または 4 行よりも長いので、「短い方が良い」というルールに従っている場合は、より小さな関数に分割する必要があります。

この場合、非常に短い関数のみを許可するという戦略は、より単純な関数につながる可能性がありますが、プログラムの全体的な複雑さは劇的に増加します。私の意見では、関数は 30 行未満であることが望ましく、200 行を超えることはありません。関数はできるだけ短くしてください。

関数パラメータと実パラメータ

関数の仮パラメーターはdef関数のステートメントの括弧内の変数名であり、実パラメーターは関数呼び出しの括弧内の値です。関数のパラメーターが多いほど、コードの構成と一般化が容易になります。しかし、パラメータが増えるということは、より複雑になるということでもあります。

従うべき適切なルールは、引数が 0 ~ 3 個であれば問題ありませんが、5 個または 6 個を超えるとおそらく多すぎるということです。関数が複雑になりすぎたら、パラメータを減らしてより小さな関数に分割する方法を検討することをお勧めします。

デフォルトのパラメータ

関数パラメーターの複雑さを軽減する 1 つの方法は、パラメーターの既定のパラメーターを提供することです。デフォルト引数は、関数呼び出しで引数が指定されていない場合に引数として使用される値です。ほとんどの関数呼び出しが特定のパラメーター値を使用する場合、関数呼び出しで繰り返し入力することを避けるために、その値を既定のパラメーターにすることができます。

defパラメータ名と等号に続いて、ステートメントでデフォルト パラメータを指定しましたたとえば、次のintroduction()関数では、関数呼び出しで指定されていない場合、greeting指定されたパラメーターに値があります。'Hello'

>>> def introduction(name, greeting='Hello'):
...    print(greeting + ', ' + name)
...
>>> introduction('Alice')
Hello, Alice
>>> introduction('Hiro', 'Ohiyo gozaimasu')
Ohiyo gozaimasu, Hiro

関数が 2 番目の引数なしで呼び出されるとintroduction()、デフォルトで文字列になります'Hello'デフォルトの引数を持つ引数は、常にデフォルトの引数のない引数の後に続く必要があることに注意してください。

[]空のリストや空の辞書など、変更可能なオブジェクトをデフォルト値として使用することは避けるべきであることを第 8 章で思い出してください{}143 ページの「変更可能な値をデフォルト パラメータとして使用しないでください」では、このアプローチが引き起こす問題とその解決策について説明しています。

関数への引数の使用*と受け渡し**

*and構文を使用して、引数のグループを個別に関数に渡すことができます**(多くの場合、スターダブル スターと発音されます)。*この構文を使用すると、リストやタプルなどの反復可能なオブジェクトでアイテムを渡すことができます。**この構文を使用すると、マップ オブジェクト (辞書など) からキーと値のペアを個別の引数として渡すことができます。

たとえば、print()関数は複数のパラメーターを受け入れることができます。デフォルトでは、以下のコードに示すように、それらの間にスペースが挿入されます。

>>> print('cat', 'dog', 'moose')
cat dog moose

これらのパラメーターは、関数呼び出しでの位置によって、どのパラメーターがどのパラメーターに割り当てられるかが決まるため、位置パラメーターと呼ばれます。ただし、これらの文字列をリストに保存し、そのリストを渡そうとすると、print()関数はリストを単一の値として出力しようとしていると見なします。

>>> args = ['cat', 'dog', 'moose']
>>> print(args)
['cat', 'dog', 'moose']

にリストを渡すと、print()括弧、引用符、コンマ文字を含むリストが表示されます。

リスト内の 1 つの項目を出力する 1 つの方法は、各項目のインデックスを関数に個別に渡すことによって、リストを複数の引数に分割することです。これにより、コードが読みにくくなります。

>>> # An example of less readable code:
>>> args = ['cat', 'dog', 'moose']
>>> print(args[0], args[1], args[2])
cat dog moose

これらのアイテムを に渡す簡単な方法がありますprint()*この構文を使用して、リスト (またはその他の反復可能なデータ型) 内の項目を個々の位置引数として解釈できます。次の例を対話型シェルに入力します。

>>> args = ['cat', 'dog', 'moose']
>>> print(*args)
cat dog moose

*この構文を使用すると、リスト内の項目数に関係なく、リスト項目を個別に関数に渡すことができます。

**構文を使用して、辞書などのマップされたデータ型を個別のキーワード引数として渡すことができます。キーワード引数の前には、引数名と等号が付きます。たとえば、print()関数には、sep表示される引数の間に入れる文字列を指定するキーワード引数があります。デフォルトでは、スペースの文字列に設定されています' '割り当てステートメントまたは**構文を使用して、キーワード引数をさまざまな値に割り当てることができます。これがどのように機能するかを確認するには、対話型シェルに次のように入力します。

>>> print('cat', 'dog', 'moose', sep='-')
cat-dog-moose
>>> kwargsForPrint = {
    
    'sep': '-'}
>>> print('cat', 'dog', 'moose', **kwargsForPrint)
cat-dog-moose

これらの命令は同じ出力を生成することに注意してください。この例では、わずか 1 行のコードで辞書を設定していますkwargsForPrintしかし、より複雑なケースでは、キーワード引数のディクショナリを構築するためにより多くのコードが必要になる場合があります。**この構文を使用すると、関数呼び出しに渡す構成設定のカスタム辞書を作成できます。これは、多数のキーワード引数を受け入れる関数やメソッドに特に役立ちます。

*and構文を使用して、実行時にリストまたはディクショナリを変更することにより、**可変数の引数を関数呼び出しに提供できます。

を使用して*可変関数を作成する

defステートメントでこの構文を使用して、*さまざまな数の位置引数を受け取る可変個引数または可変個引数関数を作成することもできますたとえば、print()任意の数の文字列を渡すことができるため、 は可変引数関数です。たとえば、print('Hello!')またはprint('My name is', name). *前のセクションの関数呼び出しで構文を使用しましたが、*このセクションの関数定義で構文を使用することに注意してください。

product()任意の数の引数を取り、それらを乗算する関数を作成して例を見てみましょう。

>>> def product(*args):
...    result = 1
...    for num in args:
...        result *= num
...    return result
...
>>> product(3, 3)
9
>>> product(2, 1, 2, 3)
12

関数内では、argsすべての位置引数を含む通常の Python タプルだけです。*技術的には、アスタリスク ( ) で始まる限り、このパラメーターに任意の名前を付けることができますが、慣例により、通常はargs.

いつ使用するかを知るには、*少し考える必要があります。結局のところ、可変個引数関数を作成する代わりに、さまざまな数の項目を含むリスト (またはその他の反復可能なデータ型) を受け入れる単一の引数を取ることです。組み込み関数のsum()機能は次のとおりです。

>>> sum([2, 1, 2, 3])
8

sum()この関数は反復可能な引数を想定しているため、複数の引数を渡すと例外が発生します。

>>> sum(2, 1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() takes at most 2 arguments (4 given)

一方、組み込みのmin()summax()関数は、複数の値の最小値または最大値を見つけることができ、単一の反復可能な引数または複数の個別の引数を受け入れます。

>>> min([2, 1, 3, 5, 8])
1
>>> min(2, 1, 3, 5, 8)
1
>>> max([2, 1, 3, 5, 8])
8
>>> max(2, 1, 3, 5, 8)
8

これらの関数はすべて異なる数の引数を取るのに、引数の設計が異なるのはなぜでしょうか? *反復可能な引数または複数の独立した引数を受け入れる関数を設計するために、いつ構文を使用する必要がありますか?

パラメータをどのように設計するかは、プログラマがコードをどのように使用するかを予測する方法によって異なります。print()関数は複数のパラメーターを受け入れます。これは、プログラマーが のように、一連の文字列または文字列を含む変数を関数に渡すことが多いためですprint('My name is', name)これらの文字列をいくつかの手順でリストに集めて、そのリストを に渡すことはprint()一般的ではありません。また、print()リストを に渡すと、関数は完全なリスト値を出力するため、リスト内の個々の値を出力するために使用することはできません。

Python は既にそのために operatorsum()を使用しているため、別の引数でそれを呼び出す理由はありません。+そのようなコードを書くことができるので2 + 4 + 8、そのようなコードを書ける必要はありませんsum(2, 4, 8)さまざまな数の引数を に渡す必要があることは理にかなっていますsum()

min()およびmax()関数は両方のフレーバーを許可します。プログラマーが引数を渡すと、関数はそれがチェックする値のリストまたはタプルであると想定します。プログラマーが複数のパラメーターを渡す場合、これらがチェックする値であると見なされます。これら 2 つの関数は通常、関数呼び出しなど、プログラムの実行時に値のリストを処理しますmin(allExpenses)また、プログラマがコードを記述するときに選択する独立したパラメータも扱いますmax(0, someNumber)したがって、これらの関数は、両方のタイプの引数を受け入れるように設計されています。以下の関数のmyMinFunction()私自身の実装は、これを示しています。min()

def myMinFunction(*args):
    if len(args) == 1:
    1 values = args[0]
    else:
    2 values = args

    if len(values) == 0:
    3 raise ValueError('myMinFunction() args is an empty sequence')

 4 for i, value in enumerate(values):
        if i == 0 or value < smallestValue:
            smallestValue = value
    return smallestValue

myMinFunction()*構文を使用して、さまざまな数の引数をタプルとして受け入れます。このタプルに値が 1 つしか含まれていない場合は、1 をチェックする値のシーケンスであると想定します。それ以外の場合は、argsチェックする値のタプルを想定します 2. いずれにせよ、values変数には、コードの残りの部分がチェックする一連の値が含まれます。実際のmin()関数と同様に、呼び出し元が引数を渡さないか空のシーケンスを渡した場合に発生します3 ValueErrorコードの残りの部分は値をループし、見つかった最小値 4 を返します。この例を単純にするために、myMinFunction()反復可能な値ではなく、リストやタプルなどのシーケンスのみが受け入れられます。

異なる数の引数を渡す 2 つの方法を受け入れる関数を常に作成しないのはなぜかと思うかもしれません。答えは、関数をできるだけシンプルに保つのが最善だということです。関数を呼び出す両方の方法が一般的でない限り、いずれかを選択します。関数が通常、プログラムの実行時に作成されるデータ構造を処理する場合は、単一の引数を受け入れるようにすることをお勧めします。関数が通常、コードを記述するときにプログラマーが指定する引数を処理する場合は、*可変数の引数を受け入れる構文を使用することをお勧めします。

を使用して**可変関数を作成する

可変個引数関数も**この構文を使用できます。defステートメント内の構文は*、位置引数の異なる数を示していますが、**構文はオプションのキーワード引数の異なる数を示しています。

構文を使用せずに多くのオプションのキーワード引数を取ることができる関数を定義すると**defステートメントが扱いにくくなる可能性があります。formMolecule()既知の 118 要素すべてのパラメーターを持つ仮想関数を考えてみましょう。

>>> def formMolecule(hydrogen, helium, lithium, beryllium, boron, `--snip--`

pass as hydrogenparameter 2oxygenpass as parameter 1、 return 'water'、これも面倒で理解しにくいです。無関係な要素をすべてゼロに設定する必要があるためです。

>>> formMolecule(2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 `--snip--`
'water'

関数呼び出しで引数を渡す必要がないように、それぞれが既定の引数を持つ名前付きキーワード引数を使用することで、関数をより管理しやすくすることができます。


ノート

仮パラメーター実パラメーターという用語は明確に定義されていますが、プログラマーはキーワード仮パラメーターキーワード実パラメーター(一般に中国語ではキーワード パラメーターと呼ばれる) を同じ意味で使用する傾向があります。


たとえば、次のdefステートメントには、各キーワード引数の既定の引数があります0

>>> def formMolecule(hydrogen=0, helium=0, lithium=0, beryllium=0, `--snip--`

formMolecule()これにより、デフォルトのパラメーターとは異なるパラメーターのパラメーター値を指定するだけで済むため、呼び出しがはるかに簡単になります。キーワード引数を任意の順序で指定することもできます:

>>> formMolecule(hydrogen=2, oxygen=1)
'water'
>>> formMolecule(oxygen=1, hydrogen=2)
'water'
>>> formMolecule(carbon=8, hydrogen=10, nitrogen=4, oxygen=2)
'caffeine'

しかし、それでもdef118 個のパラメーター名を持つ厄介なステートメントです。新元素が発見されたら?関数のdefステートメントと、関数のパラメーターに関するすべてのドキュメントを更新する必要があります。

代わりに、**キーワード引数の構文を使用して、すべての引数とその引数をキーと値のペアとして辞書に収集できます。**技術的には、パラメーターに好きな名前を付けることができますが、慣例により、通常は次のように名前が付けられますkwargs

>>> def formMolecules(**kwargs):
...    if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and 

kwargs['oxygen'] == 1:

 ...        return 'water'
...    # (rest of code for the function goes here)
...
>>> formMolecules(hydrogen=2, oxygen=1)
'water'

**この構文は、kwargsパラメーターが関数呼び出しで渡されるすべてのキーワード引数を処理できることを示しています。kwargsこれらは、パラメータに割り当てられたディクショナリにキーと値のペアとして格納されます。def新しい化学元素が発見されると、すべてのキーワード引数が に置かれるため、関数のステートメントではなく、関数のコードを更新する必要がありますkwargs

>>> def formMolecules(**kwargs): # 1
...    if len(kwargs) == 1 and kwargs.get('unobtanium') == 12: # 2
...        return 'aether'
...    # (rest of code for the function goes here)
...
>>> formMolecules(unobtanium=12)
'aether'

ご覧のとおり、defステートメント 1 は以前と同じで、関数のコード 2 のみを更新する必要があります。**構文を使用すると、defステートメントと関数呼び出しが書きやすくなり、読みやすいコードが生成されます。

ラッパー関数の使用*と作成**

defステートメント内の*and構文の一般的な使用例は、引数を別の関数に渡し、その関数の戻り値を返すラッパー関数を作成することです。and構文を使用して、すべての引数をラップされた関数に転送**できます。たとえば、組み込み関数をラップする関数を作成できます。実際の作業を行うために依存していますが、最初に文字列引数を小文字に変換します:***printLowercase()print()print()

>>> def printLower(*args, **kwargs): # 1
...    args = list(args) # 2
...    for i, value in enumerate(args):
...        args[i] = str(value).lower()
...    return print(*args, **kwargs) # 3
...
>>> name = 'Albert'
>>> printLower('Hello,', name)
hello, albert
>>> printLower('DOG', 'CAT', 'MOOSE', sep=', ')
dog, cat, moose

printLower()関数 1 は、引数*args割り当てられたタプルでさまざまな数の位置引数を受け入れる構文を使用しますが、任意のキーワード引数を引数のディクショナリ**に割り当てる構文を使用します。kwargs関数が*argsと の両方を使用する場合**kwargs*args引数は**kwargs引数の前になければなりません。これらをラップされた関数に渡しますprint()が、最初に関数がいくつかの引数を変更するため、argsリスト形式で tuple2 を作成します。

args文字列を小文字に変更した後、*構文**3を使用して、args項目とkwargsキーと値のペアを別々の引数として に渡しますprint()print()の戻り値は のprintLower()戻り値としても返されます。これらの手順により、print()関数が効果的にラップされます。

関数型プログラミング

関数型プログラミングは、グローバル変数や、ハード ドライブ上のファイル、インターネット接続、データベースなどの外部状態を変更せずに計算を実行する関数を記述することに重点を置いたプログラミング パラダイムです。Erlang、Lisp、Haskell などの一部のプログラミング言語は、主に関数型プログラミングの概念に基づいて設計されています。パラダイムに縛られることはありませんが、Python にはいくつかの関数型プログラミング機能があります。Python プログラムで使用できる主な関数は、副作用のない関数、高階関数、および Lambda 関数です。

副作用

副作用とは、関数が独自のコードとローカル変数の外部に存在するプログラムの部分に加える変更です。-これを説明するために、Python の減算演算子 ( )を実装するsubtract()関数を作成してみましょう。

>>> def subtract(number1, number2):
...    return number1 - number2
...
>>> subtract(123, 987)
-864

このsubtract()関数には副作用はありません。つまり、コードの一部ではないプログラムには影響しません。プログラムまたはコンピューターの状態から、subtract()関数が以前に 1 回、2 回、または 100 万回呼び出されたかどうかを判断する方法はありません。関数は関数内のローカル変数を変更する場合がありますが、これらの変更はプログラムの残りの部分から分離されています。

次の名前のグローバル変数addToTotal()に数値引数を追加する関数を考えてみましょう。TOTAL

>>> TOTAL = 0
>>> def addToTotal(amount):
...    global TOTAL
...    TOTAL += amount
...    return TOTAL
...
>>> addToTotal(10)
10
>>> addToTotal(10)
20
>>> addToTotal(9999)
10019
>>> TOTAL
10019

addToTotal()関数は、関数の外部に存在する要素であるグローバル変数を変更するため、副作用がありますTOTAL副作用は、グローバル変数への変更だけではありません。これらには、ファイルの更新または削除、画面へのテキストの出力、データベース接続の開始、サーバーへの認証、または関数外でのその他の変更が含まれます。関数呼び出しが戻った後に残るトレースは副作用です。

副作用には、関数の外部で参照される可変オブジェクトへのインプレース変更も含まれます。たとえば、次のremoveLastCatFromList()関数はリスト引数をその場で変更します。

>>> def removeLastCatFromList(petSpecies):
...    if len(petSpecies) > 0 and petSpecies[-1] == 'cat':
...        petSpecies.pop()
...
>>> myPets = ['dog', 'cat', 'bird', 'cat']
>>> removeLastCatFromList(myPets)
>>> myPets
['dog', 'cat', 'bird']

この例では、myPets変数とpetSpeciesパラメーターは同じリストへの参照を保持しています。関数内のリスト オブジェクトへのインプレース変更は、関数の外にも存在するため、そのような変更は副作用になります。

関連する概念であるdeterministic function は、同じ引数を指定すると常に同じ戻り値を返します。subtract(123, 987)関数呼び出しは常に を返します−864Python 組み込み関数は、引数としてround()渡されると常に戻ります非決定論的関数は、同じ引数が渡されたときに常に同じ値を返すとは限りません。たとえば、この呼び出しは~ の間のランダムな整数を返します。この関数にはパラメーターはありませんが、関数が呼び出されたときのコンピューターの時計の設定に応じて、異なる値が返されます。この例では、クロックは外部リソースであり、実際には引数のように関数への入力です。関数の外部リソース (グローバル変数、ハード ディスク上のファイル、データベース、インターネット接続など) に依存する関数は、決定論的とは見なされません。3.143random.randint(1, 10)110time.time()time.time()

決定論的関数の利点の 1 つは、その値をキャッシュできることです。subtract()これらのパラメータで最初に呼び出されたときの戻り値を覚えていれば、合計の差を何度も123計算する必要はありません。したがって、決定論的関数を使用すると、空間と時間のトレードオフを987行うことができ、メモリ空間を使用して以前の結果をキャッシュすることにより、関数の実行時間を短縮できます。

決定論的で副作用のない関数は、純粋な関数と呼ばれます。関数型プログラマーは、プログラム内で純粋な関数のみを作成するよう努めています。すでに述べたものとは別に、純粋関数にはいくつかの利点があります。

  • 外部リソースを設定する必要がないため、単体テストに最適です。
  • 同じ引数で関数を呼び出すことにより、純粋な関数でバグを再現するのは簡単です。
  • 純粋関数は、他の純粋関数を呼び出して、純粋なままにすることができます。
  • マルチスレッド プログラムでは、純粋な関数はスレッド セーフであり、同時に安全に実行できます。(マルチスレッドは、この本の範囲を超えています。)
  • 純粋関数への複数の呼び出しは、特定の順序で実行する必要がある外部リソースに依存しないため、並列 CPU コアまたはマルチスレッド プログラムで実行できます。

可能な限り、Python で純粋な関数を作成できますし、作成する必要があります。Python 関数は慣例により純粋であり、Python インタープリターが純粋性を保証する設定はありません。最も一般的なアプローチは、関数でグローバル変数を使用することを避け、それらがファイル、インターネット、システム クロック、乱数、またはその他の外部リソースとやり取りしないようにすることです。

高階関数

高階関数は、他の関数を引数として受け入れたり、戻り値として関数を返すことができます。たとえば、指定callItTwice()された関数を 2 回呼び出すという関数を定義しましょう。

>>> def callItTwice(func, *args, **kwargs):
...     func(*args, **kwargs)
...     func(*args, **kwargs)
...
>>> callItTwice(print, 'Hello, world!')
Hello, world!
Hello, world!

callItTwice()関数は、渡されたどの関数でも機能します。Python では、関数はファースト クラスのオブジェクトです。つまり、関数は他のオブジェクトとまったく同じです。関数を変数に格納したり、引数として渡したり、戻り値として使用したりできます。

ラムダ関数

匿名関数または名前のない関数とも呼ばれるLambda 関数は、コードがreturn1 つのステートメントのみで構成される名前のない単純化された関数です。関数をパラメーターとして他の関数に渡すときに、Lambda 関数をよく使用します。

たとえば、次のように、4 x 10 の長方形の幅と高さを含むリストを受け取る汎用関数を作成できます。

>>> def rectanglePerimeter(rect):
...    return (rect[0] * 2) + (rect[1] * 2)
...
>>> myRectangle = [4, 10]
>>> rectanglePerimeter(myRectangle)
28

同等の Lambda 関数は次のようになります。

lambda rect: (rect[0] * 2) + (rect[1] * 2)

Python Lambda 関数を定義するには、lambdaキーワードを使用し、引数のコンマ区切りリスト (存在する場合)、コロン、および戻り値として機能する式を続けます。def関数は第一級のオブジェクトであるため、Lambda 関数を変数に割り当てて、ステートメントの機能を効果的に複製できます。

>>> rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2)
>>> rectanglePerimeter([4, 10])
28

この Lambda 関数を という名前の変数に割り当ててrectanglePerimeter、効果的にrectanglePerimeter()関数を提供します。ご覧のとおり、lambdaステートメントによって作成された関数は、defステートメントによって作成された関数と同じです。


ノート

実際のコードでは、defLambda 関数を定数変数に割り当てる代わりにステートメントを使用します。Lambda 関数は、関数に名前が必要ない状況向けに特別に設計されています。


ラムダ関数構文は、小さな関数を他の関数呼び出しへの引数として指定することを容易にします。たとえば、sorted()関数にkeyは という名前のキーワード引数があり、関数を指定できます。値に基づいてリスト内のアイテムを並べ替える代わりに、関数の戻り値に基づいて並べ替えます。以下の例では、sorted()特定の長方形の周囲を返す Lambda 関数を渡します。これにより、リストに直接ではなく、リストsorted()の計算された周囲に基づいて関数[width, height]がソートされます。[width, height]

>>> rects = [[10, 2], [3, 6], [2, 4], [3, 9], [10, 7], [9, 9]]
>>> sorted(rects, key=lambda rect: (rect[0] * 2) + (rect[1] * 2))
[[2, 4], [3, 6], [10, 2], [3, 9], [10, 7], [9, 9]]

たとえば、関数は、値またはでソートするのではなく24返された perimeters の整数合計で18ソートするようになりました。Lambda 関数は便利な構文上のショートカットです。新しい名前付き関数を1 つのステートメントで定義する代わりに、小さな 1 行の Lambda 関数を指定できます。[10, 2][3, 6]def

マッピングとフィルタリングにリスト内包表記を使用する

以前のバージョンの Python では、map()関数filter()は、通常は Lambda 関数の助けを借りて、リストを変換およびフィルタリングできる一般的な高階関数でした。マップは、別のリストの値に基づいて値のリストを作成できます。フィルタリングは、特定の基準を満たす別のリストからの値のみを含むリストを作成します。

たとえば、 integers の代わりに文字列を含む新しいリストを作成する場合は[8, 16, 18, 19, 12, 1, 6, 7]、このリストlambda n: str(n)sum をmap()関数に渡すことができます。

>>> mapObj = map(lambda n: str(n), [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(mapObj)
['8', '16', '18', '19', '12', '1', '6', '7']

map()関数はオブジェクトを返し、mapそれを関数に渡すことでlist()リストとして取得できます。マップされたリストには、元のリストの整数値に基づく文字列値が含まれるようになりました。filter()関数は似ていますが、ここでは、Lambda 関数のパラメーターによって、リスト内のどの項目を保持するか (Lambda 関数が を返す場合True)、またはフィルターで除外するか (が を返す場合False) が決定されます。たとえば、次の方法lambda n: n % 2 == 0で奇数を除外できます。

>>> filterObj = filter(lambda n: n % 2 == 0, [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(filterObj)
[8, 16, 18, 12, 6]

filter()この関数はフィルター オブジェクトを返します。これをlist()再度関数に渡すことができます。フィルタリングされたリストには偶数の整数のみが残ります。

しかしmap()、andfilter()関数は、Python でマップを作成したり、リストをフィルタリングしたりするための時代遅れの方法です。代わりに、リスト内包表記を使用してそれらを作成できるようになりました。リスト内包表記は、Lambda 関数を記述する手間を省くだけでなく、map()andよりも高速ですfilter()

map()ここでは、リスト内包表記を使用して関数の例を複製します。

>>> [str(n) for n in [8, 16, 18, 19, 12, 1, 6, 7]]
['8', '16', '18', '19', '12', '1', '6', '7']

リスト内包表記str(n)部分はlambda n: str(n)に似ていることに注意してください。

filter()ここでは、リスト内包表記を使用して関数の例を複製します。

>>> [n for n in [8, 16, 18, 19, 12, 1, 6, 7] if n % 2 == 0]
[8, 16, 18, 12, 6]

リスト内包表記if n % 2 == 0部分はlambda n: n % 2 == 0に似ていることに注意してください。

多くの言語には、マッピングやフィルタリング関数などの高次関数を可能にする、ファースト クラス オブジェクトとしての関数の概念があります。

戻り値は常に同じデータ型でなければなりません

Python は動的型付け言語です。つまり、Python の関数とメソッドは、任意のデータ型の値を自由に返すことができます。ただし、関数をより予測可能にするためには、単一のデータ型の値のみを返すようにする必要があります。

たとえば、次の関数は乱数に基づいて整数値または文字列値を返します。

>>> import random
>>> def returnsTwoTypes():
...    if random.randint(1, 2) == 1:
...        return 42
...    else:
...        return 'forty two'

関数を呼び出すコードを書いているとき、考えられるいくつかのデータ型を処理しなければならないことを忘れがちです。例を続けて、 を呼び出して、returnsTwoTypes()返される数値を16 進数に変換したいとします。

>>> hexNum = hex(returnsTwoTypes())
>>> hexNum
'0x2a'

Python の組み込みhex()関数は、渡された整数値の 16 進文字列を返します。returnsTwoTypes()このコードは、整数を返す限り正常に機能し、このコードにエラーがないという印象を与えます。ただし、returnsTwoTypes()文字列を返すと、例外がスローされます。

>>> hexNum = hex(returnsTwoTypes())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object cannot be interpreted as an integer

もちろん、戻り値が持つ可能性のあるすべてのデータ型を処理することを常に覚えておく必要があります。しかし、現実の世界では、これを忘れがちです。これらの間違いを防ぐために、常に関数が単一のデータ型の値を返すようにする必要があります。これは厳密な要件ではなく、関数が異なるデータ型の値を返すことができない場合があります。しかし、1 つの型のみを返すことに近づくほど、関数は単純になり、エラーが発生しにくくなります。

特に注意が必要なケースが 1 つあります。関数がNone常に を返す場合を除き、関数から返さないでくださいNoneNone値は、NoneTypeデータ型の唯一の値です。エラーが発生したことを示すために関数を返すことは魅力的ですが (次のセクション「例外のスローとエラー コードの返し」で説明します)、意味のある戻り値Noneを持たない関数の戻り値を予約する必要がありますNone

その理由は、エラーを示すリターンがNoneキャッチされない例外'NoneType' object has no attributeの一般的な原因であるためです。

>>> import random
>>> def sometimesReturnsNone():
...    if random.randint(1, 2) == 1:
...        return 'Hello!'
...    else:
...        return None
...
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
'HELLO!'
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'upper'

エラー メッセージはかなりあいまいで、通常は期待される結果を返す関数にたどり着くのにいくらかの労力がかかる場合がありますが、エラーが発生したときにも返される可能性がありますNone問題が発生するのは、sometimesReturnsNone()返してからそれを変数Noneに代入するためです。しかし、エラー メッセージを見ると、問題はメソッドの呼び出しreturnValにあると思われます。upper()

2009 年の会議で、コンピューター科学者のトニー ホールは、None1965 年の null 参照 (Python と同様の値) の発明について謝罪し、「私はそれを私の 10 億ドルの間違いと呼んでいます。 […] 誘惑に抵抗することはできませんでした。 null 参照を入れるのは簡単だからです. これは、過去 40 年間でおそらく数十億ドルの費用がかかった、語られないバグ、バグ、およびシステム クラッシュにつながりました. 痛みと損失autbor.com/billiondollarmistake.

例外をスローしてエラー コードを返す

Python では、例外エラーという用語はほぼ同じ意味です。通常は問題を示す、プログラム内の異常な状態です。例外は、C++ と Java の出現により、1980 年代と 1990 年代にプログラミング言語の機能として人気を博しました。それらは、問題を示す関数から返される値であるエラー コードの使用を置き換えます。例外の良い点は、戻り値が関数の目的にのみ関連し、エラーがあったことを示すものではないことです。

エラー コードは、プログラムで問題を引き起こす可能性もあります。たとえば、Python のfind()文字列メソッドは通常、部分文字列が見つかったインデックスを返すか、-1部分文字列が見つからない場合はエラー コードとして返します。ただし、 を使用して-1文字列の末尾からインデックスを指定することもできるため、誤って-1エラー コードとして使用するとバグが発生する可能性があります。対話型シェルで次のように入力して、これがどのように機能するかを確認します。

>>> print('Letters after b in "Albert":', 'Albert'['Albert'.find('b') + 1:])
Letters after b in "Albert": ert
>>> print('Letters after x in "Albert":', 'Albert'['Albert'.find('x') + 1:])
Letters after x in "Albert": Albert

コードの一部が'Albert'.find('x')エラー コードに評価されます-1これにより、式は'Albert'['Albert'.find('x') + 1:]に評価され'Albert'[-1 + 1:]、さらに に評価され'Albert'[0:]、さらに に評価されます'Albert'明らかに、これはコードの予想される動作ではありません。のように代わりindex()に を呼び出すと、例外が発生し、問題が明らかになりすぎて無視できなくなります。find()'Albert'['Albert'.index('x') + 1:]

一方、文字列メソッドは、index()部分文字列が見つからない場合に例外を発生させますValueErrorこの例外を処理しないと、プログラムがクラッシュします。通常、この動作は、エラーに気付かないよりはましです。

ValueError例外クラスの名前は通常、 、NameErrorまたは のように、例外が実際のエラーを示す場合、 「エラー」で終わりますSyntaxError必ずしもエラーではない例外的な条件を表す例外クラスにはStopIterationKeyboardInterrupt、または が含まれますSystemExit

要約する

関数は、プログラム コードをまとめる一般的な方法であり、特定の決定を下す必要があります。関数の名前、関数の大きさ、関数のパラメーターの数、これらのパラメーター パラメーターに渡すべきパラメーターの数です。ステートメントのanddef構文を使用すると、関数はさまざまな数の引数を受け取ることができ、可変個引数関数になります。***

Python は関数型プログラミング言語ではありませんが、関数型プログラミング言語で使用される多くの機能を備えています。関数は第一級オブジェクトです。つまり、関数を変数に格納し、引数として他の関数 (このコンテキストでは高階関数と呼びます) に渡すことができます。Lambda 関数は、名前のない匿名関数を高階関数への引数として指定するための短い構文を提供します。Python で最も一般的な高階関数は とmap()ですfilter()が、それらが提供する機能はリスト内包表記を使用してはるかに高速に実行できます。

関数の戻り値は、常に同じデータ型である必要があります。戻り値をエラー コードとして使用しないでください。エラーを示すために例外が使用されます。特に、None値がエラー コードとして誤って使用されることがよくあります。

おすすめ

転載: blog.csdn.net/wizardforcel/article/details/130030501