高度なPython:関数

高度なPython 関数

Pythonとどのように絡み合うか。写真提供:iam_os on Unsplash

タイトルを読んだ後、おそらく「Pythonの関数は高度な概念ですか?どうして?すべてのコースで関数は言語の基本ブロックとして紹介されています」と自問しているでしょう。そして、同時に正しいと間違っているのです。

Pythonの多くのコースでは、関数を基本的な概念とビルディングブロックとして紹介しています。なぜなら、それらがなければ、機能的なコードを書くことができなくなるからです。これは、関数型プログラミングパラダイムとはまったく異なる概念であり、それについても触れていきます。

Pythonの関数の高度な複雑さに入る前に、基本的な概念とおそらく既に知っていることについて簡単に説明しましょう。

基本的な概念

プログラムを書き始めると、ある時点で同じコードのシーケンスを書くことになります。自分自身とコードブロックを繰り返し書くことになります。これは関数を導入するのに適した時間と場所です。少なくとも、そうです。Pythonでは、関数は次のように定義します:

def shout(name):    print(f'Hey! My name is {name}.')

ソフトウェアエンジニアリングの世界では、関数定義の部分を区別します:

  • def – 関数を定義するために使用されるPythonのキーワード。
  • shout – 関数名。
  • shout(name)– 関数の宣言。
  • name – 関数の引数。
  • print(...)は関数本体の一部、または関数定義と呼ばれるものです。

関数は値を返すことも、何も返さないこともあります。先ほど定義した関数のように、値を返す場合は1つ以上の値を返すことができます:

def break_sentence(sentence):    return sentence.split(' ')

結果として得られるのはタプルであり、タプルの要素を展開したり、任意のタプルの要素を選んで進行することができます。

知らない方のために、Pythonの関数はファーストクラスのオブジェクトです。これはどういう意味でしょうか?これは、関数を他の変数と同じように扱うことができるということです。関数を他の関数の引数として渡すことができたり、関数から返すことができたり、変数に保存することさえできます。以下に例を示します:

def shout(name):    return f'Hey! My name is {name}.'# 上記で定義したbreak_sentenceを使用します# 関数を別の変数に割り当てanother_breaker = break_sentence another_breaker(shout('John'))# ['Hey!', 'My', 'name', 'is', 'John.']# わあ!これは関数を定義するための有効な方法ですname_decorator = lambda x: '-'.join(list(name))name_decorator('John')# 'J-o-h-n'

待ってください、このlambdaは何ですか?これはPythonで関数を定義する別の方法です。これはいわゆる無名または匿名関数です。この例では、変数name_decoratorに割り当てていますが、名前を付ける必要なく、lambda式を他の関数の引数として渡すことができます。これについてはすぐに説明します。

関数を引数として渡したり、他の関数から値として返す方法の例を示します。これは高度な概念に向かって進んでいる部分であり、お付き合いください。

def dash_decorator(name):    return '-'.join(list(name))def no_decorator(name):    return namedef shout(name, decorator=no_decorator):    decorated_name = decorator(name)    return f'Hey! My name is {decorated_name}'shout('John')# 'Hey! My name is John'shout('John', decorator=dash_decorator)# 'Hey! My name is J-o-h-n'

これが、関数を別の関数に引数として渡す方法の例です。では、lambda関数はどうでしょうか?次の例を見てください:

def shout(name, decorator=lambda x: x):    decorated_name = decorator(name)    return f'Hey! My name is {decorated_name}'print(shout('John'))# Hey! My name is Johnprint(shout('John', decorator=dash_decorator))# Hey! My name is J-o-h-n

今、デフォルトの装飾関数はlambdaであり、引数の値をそのまま返します(イデンポテント)。ここでは、名前が付いていないため、これが匿名です。

printも関数であることに注意してください。そして、その中に関数shoutを引数として渡しています。要するに、関数を連鎖させています。そして、これはPythonで選択できる関数型プログラミングパラダイムにつながることがあります。私はこのテーマについて別のブログ記事を書いてみますが、それは非常に興味深いです。今のところ、私たちは手続き型プログラミングパラダイムに従います。つまり、これまでやってきたことを続けます。

前述のように、関数は変数に割り当てることができ、他の関数への引数として渡すことができ、その関数から返すこともできます。最初の2つのケースのいくつかの簡単な例を示しましたが、関数を関数から返すというのはどうでしょうか?最初は本当に簡単にするつもりでしたが、これは高度なPythonですからね!

中級または上級パート

これは決してPythonにおける関数や関数周りの高度な概念のガイドではありません。この記事の最後に素晴らしい資料がたくさんありますので、それを参照してください。ただし、いくつかの興味深い側面について話したいと思います。

Pythonの関数はオブジェクトです。これをどのように理解できるでしょうか?Pythonの各オブジェクトは、最終的にはtypeという特定のクラスから継承するクラスのインスタンスです。これに関する詳細は複雑ですが、関数との関連性を理解するために、次の例を見てみましょう:

type(shout)# 関数type(type(shout))# type

Pythonでクラスを定義すると、自動的にobjectクラスを継承します。そして、objectクラスはどのクラスを継承しているのでしょうか?

type(object)# type

そして、Pythonのクラスもオブジェクトなのだと言ってもいいでしょうか?実際に、これは初心者にとっては驚くべきことです。しかし、Andrew Ngが言うように、これはそれほど重要ではありません。心配しないでください。

では、関数はオブジェクトであるため、何らかのマジックメソッドを持っているはずですよね?

shout.__class__# 関数shout.__name__# shoutshout.__call__# <method-wrapper '__call__' of function object at 0x10d8b69e0># やばい!

マジックメソッド__call__は、呼び出し可能なオブジェクトに対して定義されています。したがって、私たちのshoutオブジェクト(関数)は呼び出し可能です。引数を指定して呼び出すことも呼び出さないこともできます。しかし、これは興味深いです。私たちが以前に行ったのは、shout関数を定義し、関数オブジェクトを取得することであり、この関数オブジェクトには__call__という関数があります。あなたはInceptionという映画を見たことがありますか?

したがって、私たちの関数は本当に関数ではなくオブジェクトです。オブジェクトはクラスのインスタンスであり、メソッドと属性を含んでいますよね?これはOOPから知っているはずのことです。では、オブジェクトの属性をどのように見つけることができるでしょうか?オブジェクトの属性とその値の辞書を返すPythonの関数varsがあります。次の例で何が起こるか見てみましょう:

vars(shout)# {}shout.name = 'Jimmy'vars(shout)# {'name': 'Jimmy'}

これは興味深いです。ただし、すぐにこれを使うユースケースを見つけることができるわけではありません。そして、見つけたとしても、このブラックマジックを行うことは強くお勧めしません。それは追いにくいだけでなく、興味深いフレックスです。私がこれを示した理由は、関数が実際にはオブジェクトであることを証明したかったからです。忘れないでください、Pythonではすべてがオブジェクトです。Pythonではこうやってやっているのですから。

さて、待ちに待った関数の戻り値です。この概念も非常に興味深いです。少しの構文糖衣で非常に表現力が高くなります。では、深入りしてみましょう。

まず、関数の定義には別の関数の定義を含めることができます。それも1つ以上です。以下に完全に有効な例を示します:

def shout(name):    def _upper_case(s):        return s.upper()    return _upper_case(name)

もし、これが単にname.upper()の複雑なバージョンだと思っているなら、正しい判断です。しかし、待ってください、私たちはそこに向かっています。

では、前の例を考えてみましょう。これは完全に機能するPythonコードであり、関数内に定義された複数の関数を試すことができます。この便利なトリックの価値は何でしょうか?まあ、関数が大きく、コードのブロックが繰り返される場合には、サブ関数を定義することで可読性を向上させることができます。実際には、大きな関数はコードのにおいの兆候であり、それをいくつかの小さな関数に分割することが強く推奨されています。このアドバイスに従うと、お互いに複数の関数を定義する必要はほとんどありません。注目すべき一つの点は、_upper_case関数が隠れており、shout関数が定義されて呼び出せるスコープの範囲外にあることです。これにより、簡単にテストすることはできなくなり、このアプローチの別の問題となります。

ただし、関数の内部で関数を定義する必要がある特定のケースがあります。それは関数のデコレータを実装する場合です。これは、以前の例のname文字列をデコレートするために使用した関数とは何の関係もありません。

Pythonにおけるデコレータ関数

デコレータ関数とは何でしょうか?それは、あなたの関数を包む関数と考えてください。これを行う目的は、既存の関数に追加の機能を導入することです。例えば、関数が呼び出されるたびにログを記録したいとします。

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__}が呼び出されています!')    return funmy_function()# 45my_logger(my_function)# my_functionが呼び出されています!# <function my_function at 0x105afbeb0>my_logger(my_function)()# my_functionが呼び出されています!# 45

関数をデコレートする方法に注意してください。デコレータ関数に引数として渡します。しかし、これだけでは不十分です!覚えておいてください、デコレータは関数を返し、この関数は呼び出される必要があります。これが最後の呼び出しの役割です。

さて、実際には、本当に望むのはデコレーションが元の関数の名前の下で持続することです。私たちの場合、コードを解析するときに、my_functionはデコレートされた関数の名前になることを望みます。これにより、コードを追うのが簡単になり、コードのどの部分もデコレートされていないバージョンの関数を呼び出すことができないようになります。例:

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__}が呼び出されています!')    return funmy_function = my_logger(my_function)my_function(10)# my_functionが呼び出されています!# 45

関数の名前をデコレートされたものに再割り当てする部分が問題であることは認めるでしょう。これを心に留めておかなければなりません。ログを記録したい関数の呼び出しがたくさんある場合、繰り返しコードがたくさんあります。ここで構文糖が登場します。デコレータ関数が定義された後、関数定義の前に@とデコレータ関数の名前を付けることで、他の関数をデコレートすることができます。例:

def my_logger(fun):    print(f'{fun.__name__}が呼び出されています!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_functionが呼び出されています!# 45

これがPythonの哲学です。コードの表現力とシンプルさに注目してください。

ここで重要なことを注意してください!出力は理にかなっているように思えますが、期待通りではありません!Pythonコードを読み込む時点で、インタプリタはmy_logger関数を呼び出し、実際に実行します!ログの出力が得られますが、これは最初に望んでいたものではありません。今コードを見てください:

def my_logger(fun):    print(f'{fun.__name__}が呼び出されています!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_functionが呼び出されています!# 45my_function()# 45

オリジナルの関数が呼び出された後にデコレータのコードを実行するためには、それを別の関数で囲む必要があります。ここで事が複雑になる可能性があります。以下に例を示します:

def my_logger(fun):
    def _inner_decorator(*args, **kwargs):
        print(f'{fun.__name__} が呼び出されています!')
        return fun(*args, **kwargs)
    return _inner_decorator

@my_logger
def my_function(n):
    return sum(range(n))

print(my_function(5))
# my_function が呼び出されています!
# 10

この例では、いくつかの更新がありますので、それを見ていきましょう:

  1. my_function に引数を渡すことができるようにしたい。
  2. my_function だけでなく、他の関数にもデコレートできるようにしたい。将来の関数の引数の数が正確にわからないため、*args**kwargs を使用してなるべく一般的な形式を保つ必要があります。
  3. 最も重要なのは、コード内で my_function を呼び出すたびに呼び出される _inner_decorator を定義したことです。この関数は位置引数とキーワード引数を受け取り、それらをデコレートされた関数の引数として渡します。

デコレータ関数は常に、同じ引数(数とその型)を受け入れ、同じ出力(またはその型)を返す関数を返す必要があることを常に念頭に置いてください。つまり、関数をユーザーが混乱しないようにし、コードリーダーが何が起こっているのかを理解しようとしないようにするためです。

たとえば、結果が異なる2つの関数があるとしますが、引数も必要です:

@my_logger
def my_function(n):
    return sum(range(n))

@my_logger
def my_unordinary_function(n, m):
    return sum(range(n)) + m

print(my_function(5))
# my_function が呼び出されています!
# 10

print(my_unordinary_function(5, 1))
# my_unordinary_function が呼び出されています!
# 11

この例では、デコレータ関数はデコレートする関数だけを受け取ります。しかし、もし追加のパラメータを渡してデコレータの振る舞いを動的に変更したい場合はどうなるでしょうか?たとえば、ロガーデコレータの冗長性を調整したいとします。これまでのデコレータ関数は1つの引数(デコレートする関数)を受け入れるようにしてきました。しかし、デコレータ関数自体に引数がある場合、これらは最初に渡されます。その後、デコレータ関数はデコレートされた関数を受け入れる関数を返さなければなりません。基本的に、事柄はより複雑になっています。映画『インセプション』の言及を覚えていますか?

以下は例です:

from enum import IntEnum, auto
from datetime import datetime
from functools import wraps

class LogVerbosity(IntEnum):
    ZERO = auto()
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()

def my_logger(verbosity: LogVerbosity):
    def _inner_logger(fun):
        def _inner_decorator(*args, **kwargs):
            if verbosity >= LogVerbosity.LOW:
                print(f'LOG: 冗長性レベル: {verbosity}')
                print(f'LOG: {fun.__name__} が呼び出されています!')
            if verbosity >= LogVerbosity.MEDIUM:
                print(f'LOG: 呼び出しの日時は {datetime.utcnow()} です。')
            if verbosity == LogVerbosity.HIGH:
                print(f'LOG: 呼び出し元のスコープは {__name__} です。')
                print(f'LOG: 引数は {args}, {kwargs} です。')
            return fun(*args, **kwargs)
        return _inner_decorator
    return _inner_logger

@my_logger(verbosity=LogVerbosity.LOW)
def my_function(n):
    return sum(range(n))

@my_logger(verbosity=LogVerbosity.HIGH)
def my_unordinary_function(n, m):
    return sum(range(n)) + m

print(my_function(10))
# LOG: 冗長性レベル: LOW
# LOG: my_function が呼び出されています!
# 45

print(my_unordinary_function(5, 1))
# LOG: 冗長性レベル: HIGH
# LOG: my_unordinary_function が呼び出されています!
# LOG: 呼び出しの日時は 2023-07-25 19:09:15.954603 です。
# LOG: 呼び出し元のスコープは __main__ です。
# LOG: 引数は (5, 1), {} です。
# 11

デコレータとは関係のないコードの説明には触れませんが、調べて学ぶことをお勧めします。ここでは、異なる冗長性で関数呼び出しを記録するデコレータがあります。すでに説明したように、my_logger デコレータは引数を受け入れて、動的に振る舞いを変えることができます。引数が渡された後、それを受け取る戻り値の関数がデコレータ関数となります。これが _inner_logger 関数です。ここまでの説明で、デコレータコードの残りの部分が理解できるはずです。

結論

この記事の最初のアイデアは、Pythonのデコレータのような高度なトピックについて書くことでした。しかし、おそらく既に知っているように、他の多くの高度なトピックについても言及し、使用しています。将来の記事では、これらのいくつかに一部触れます。それにもかかわらず、ここで言及された事柄について他の情報源からも学ぶことをお勧めします。関数の理解は、どのプログラミング言語で開発している場合でも必要不可欠ですが、選んだプログラミング言語の全ての側面を把握することで、コードの書き方において大きな優位性を得ることができます。

新しい情報を紹介できたこと、そして高度なPythonプログラマーとして関数の書き方に自信を持てるようになったことを願っています。

参考文献

  • Pythonデコレータの入門
  • Pythonの内部関数:何に適していますか?
  • Enum HOWTO

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more