クリーンなコードの確保:パラメーター化されたPythonの外観
公開: 2022-03-11この投稿では、クリーンなPythonicコードを生成する上で最も重要な手法またはパターン、つまりパラメーター化について説明します。 この投稿は次の場合に役立ちます。
- あなたはデザインパターン全体に比較的慣れておらず、おそらくパターン名とクラス図の長いリストに少し戸惑っています。 幸いなことに、Pythonで絶対に知っておく必要のあるデザインパターンは1つだけです。 さらに良いことに、あなたはおそらくそれをすでに知っていますが、おそらくそれを適用できるすべての方法ではありません。
- JavaやC#などの別のOOP言語からPythonにアクセスし、その言語からPythonにデザインパターンの知識を変換する方法を知りたいと考えています。 Pythonやその他の動的型付け言語では、静的型付けされたOOP言語で一般的な多くのパターンは、著者のPeter Norvigが述べているように、「見えないか、より単純」です。
この記事では、「パラメーター化」のアプリケーションと、依存性注入、戦略、テンプレートメソッド、抽象ファクトリ、ファクトリメソッド、デコレータとして知られる主流のデザインパターンにどのように関連するかについて説明します。 Pythonでは、これらの多くは単純であるか、Pythonのパラメーターが呼び出し可能なオブジェクトまたはクラスである可能性があるという事実によって不要になっています。
パラメータ化は、コードを一般化するために、関数またはメソッド内で定義された値またはオブジェクトを取得し、それらをその関数またはメソッドのパラメータにするプロセスです。 このプロセスは、「抽出パラメーター」リファクタリングとも呼ばれます。 ある意味で、この記事はデザインパターンとリファクタリングについてです。
パラメータ化されたPythonの最も単純なケース
ほとんどの例では、いくつかのグラフィックスを実行するために、教育用の標準ライブラリturtleモジュールを使用します。
turtle
を使用して100x100の正方形を描画するコードを次に示します。
from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)
別のサイズの正方形を描きたいとします。 この時点で非常に若いプログラマーは、このブロックをコピーして貼り付けて変更したくなるでしょう。 明らかに、はるかに優れた方法は、最初に正方形の描画コードを関数に抽出してから、正方形のサイズをこの関数のパラメーターにすることです。
def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)
これで、 draw_square
を使用して任意のサイズの正方形を描画できます。 パラメータ化の基本的な手法はこれですべてです。最初の主な使用法であるコピー&ペーストプログラミングを排除する方法を見てきました。
上記のコードの当面の問題は、 draw_square
がグローバル変数に依存していることです。 これには多くの悪い結果があり、それを修正する簡単な方法が2つあります。 1つ目は、 draw_square
がTurtle
インスタンス自体を作成することです(これについては後で説明します)。 すべての描画に単一のTurtle
を使用する場合、これは望ましくない場合があります。 したがって、今のところ、パラメータ化を再度使用して、 turtle
をdraw_square
のパラメータにします。
from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)
これには、依存性注入という派手な名前が付いています。 これは、 draw_square
がTurtle
を必要とするように、関数がその作業を行うために何らかのオブジェクトを必要とする場合、呼び出し元がそのオブジェクトをパラメーターとして渡す責任があることを意味します。 いいえ、実際、Pythonの依存性注入に興味がある場合は、これがそれです。
これまで、2つの非常に基本的な使用法を扱いました。 この記事の残りの部分で重要なのは、Pythonには、他のいくつかの言語よりもパラメーターになる可能性のあるものがたくさんあることです。これにより、Pythonは非常に強力な手法になります。
オブジェクトであるものすべて
Pythonでは、この手法を使用してオブジェクトであるものをパラメーター化できます。Pythonでは、実際に遭遇するほとんどのものはオブジェクトです。 これも:
- 文字列
"I'm a string"
や整数42
または辞書などの組み込み型のインスタンス - 他のタイプおよびクラスのインスタンス、たとえば、
datetime.datetime
オブジェクト - 関数とメソッド
- 組み込み型とカスタムクラス
最後の2つは、特に他の言語から来ている場合に最も驚くべきものであり、さらに議論が必要です。
パラメータとして機能
Pythonの関数ステートメントは2つのことを行います。
- 関数オブジェクトを作成します。
- そのオブジェクトを指す名前をローカルスコープに作成します。
REPLでこれらのオブジェクトを操作できます。
> >> def foo(): ... return "Hello from foo" > >> > >> foo() 'Hello from foo' > >> print(foo) <function foo at 0x7fc233d706a8> > >> type(foo) <class 'function'> > >> foo.name 'foo'
そして、すべてのオブジェクトと同様に、他の変数に関数を割り当てることができます。
> >> bar = foo > >> bar() 'Hello from foo'
bar
は同じオブジェクトの別名であるため、以前と同じ内部__name__
プロパティを持っていることに注意してください。
> >> bar.name 'foo' > >> bar <function foo at 0x7fc233d706a8>
ただし、重要な点は、関数は単なるオブジェクトであるため、使用されている関数が表示される場所であればどこでも、それがパラメーターになる可能性があるということです。
したがって、上記の正方形の描画関数を拡張し、正方形を描画するときに、各コーナーで一時停止したい場合があります。これは、 time.sleep()
の呼び出しです。
しかし、一時停止したくない場合があるとします。 これを実現する最も簡単な方法は、 pause
パラメータを追加することです。デフォルトでは一時停止しないように、デフォルトはゼロです。
しかし、後で、実際にはコーナーでまったく異なることをしたいことがあることに気付きました。 おそらく、各コーナーに別の形状を描画したり、ペンの色を変更したりしたい場合があります。必要なことごとに1つずつ、さらに多くのパラメーターを追加したくなるかもしれません。 ただし、はるかに優れた解決策は、実行するアクションとして任意の関数を渡すことができるようにすることです。 デフォルトでは、何もしない関数を作成します。 また、必要に応じて、この関数がローカルのturtle
とsize
パラメーターを受け入れるようにします。
def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)
または、各コーナーに小さな正方形を再帰的に描画するなど、少しクールなことを行うこともできます。
def smaller_square(turtle, size): if size < 10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square)
もちろん、これにはバリエーションがあります。 多くの例では、関数の戻り値が使用されます。 ここでは、より命令的なプログラミングスタイルがあり、関数はその副作用のためにのみ呼び出されます。
他の言語で…
Pythonにファーストクラスの関数があると、これは非常に簡単になります。 それらがない言語、またはパラメーターに型署名を必要とする静的に型付けされた言語では、これはより困難になる可能性があります。 第一級関数がない場合、これをどのように行うでしょうか?
1つの解決策は、 SquareDrawer
をクラスdraw_square
に変換することです。
class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass
これで、 SquareDrawer
をサブクラス化し、必要なことを実行するat_corner
メソッドを追加できます。 このPythonパターンは、テンプレートメソッドパターンと呼ばれます。基本クラスは、操作またはアルゴリズム全体の形状を定義し、操作のバリアント部分は、サブクラスで実装する必要のあるメソッドに配置されます。
これはPythonで役立つ場合がありますが、パラメーターとして渡されるだけの関数にバリアントコードを引き出す方が、はるかに簡単になることがよくあります。
ファーストクラス関数のない言語でこの問題に取り組む2つ目の方法は、次のように、関数をクラス内のメソッドとしてラップすることです。
class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())
これは戦略パターンとして知られています。 繰り返しになりますが、これはPythonで使用するのに確かに有効なパターンです。特に、戦略クラスに1つだけではなく、関連する関数のセットが実際に含まれている場合はそうです。 ただし、多くの場合、本当に必要なのは関数だけであり、クラスの記述を停止できます。
その他の呼び出し可能
上記の例では、関数をパラメーターとして他の関数に渡すことについて説明しました。 しかし、私が書いたものはすべて、実際には、呼び出し可能なオブジェクトすべてに当てはまりました。 関数は最も単純な例ですが、メソッドも検討できます。
リストfoo
があるとします:
foo = [1, 2, 3]
foo
には、 .append()
や.count()
)などの多数のメソッドがアタッチされるようになりました。 これらの「バインドされたメソッド」は、関数のように受け渡して使用できます。
> >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]
これらのインスタンスメソッドに加えて、他のタイプの呼び出し可能なオブジェクト( staticmethods
とclassmethods
、__ __call__
を実装するクラスのインスタンス、およびクラス/タイプ自体)があります。

パラメータとしてのクラス
Pythonでは、クラスは「ファーストクラス」です。つまり、dictや文字列などの実行時オブジェクトです。これは、関数がオブジェクトであるよりもさらに奇妙に見えるかもしれませんが、ありがたいことに、関数よりもこの事実を示す方が実際には簡単です。
使い慣れたクラスステートメントはクラスを作成するための優れた方法ですが、それが唯一の方法ではありません。3つの引数バージョンの型を使用することもできます。 次の2つのステートメントは、まったく同じことを行います。
class Foo: pass Foo = type('Foo', (), {})
2番目のバージョンでは、先ほど行った2つのことに注意してください(これらは、クラスステートメントを使用してより便利に実行されます)。
- 等号の右側に、内部名
Foo
の新しいクラスを作成しました。 これは、Foo.__name__
を実行した場合に返される名前です。 - 次に、割り当てを使用して、現在のスコープにFooという名前を作成しました。これは、作成したばかりのクラスオブジェクトを参照しています。
関数ステートメントが行うことについても同じ観察を行いました。
ここでの重要な洞察は、クラスは名前を割り当てることができる(つまり、変数に入れることができる)オブジェクトであるということです。 クラスが使用されているのを見ると、実際には変数が使用されているのがわかります。 そして、それが変数である場合、それはパラメーターである可能性があります。
これをいくつかの使用法に分類できます。
工場としてのクラス
クラスは、それ自体のインスタンスを作成する呼び出し可能なオブジェクトです。
> >> class Foo: ... pass > >> Foo() <__main__.Foo at 0x7f73e0c96780>
また、オブジェクトとして、他の変数に割り当てることができます。
> >> myclass = Foo > >> myclass() <__main__.Foo at 0x7f73e0ca93c8>
上記のカメの例に戻ると、カメを描画に使用する際の問題の1つは、描画の位置と方向がカメの現在の位置と方向に依存し、別の状態のままになる可能性があることです。呼び出し側。 これを解決するために、 draw_square
関数は独自のタートルを作成し、それを目的の位置に移動してから、正方形を描画します。
def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
ただし、現在、カスタマイズの問題があります。 発信者がタートルのいくつかの属性を設定したい、または同じインターフェイスを持つが特別な動作をする別の種類のタートルを使用したいとしますか?
以前と同様に、依存性注入を使用してこれを解決できます。呼び出し元がTurtle
オブジェクトの設定を担当します。 しかし、関数がさまざまな描画目的で多くのカメを作成する必要がある場合や、正方形の片側を描画するためにそれぞれが独自のカメを持つ4つのスレッドを開始したい場合はどうでしょうか。 答えは、Turtleクラスを関数のパラメーターにすることです。 デフォルト値でキーワード引数を使用して、気にしない呼び出し元が物事を単純に保つことができます。
def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
これを使用するには、タートルを作成して変更するmake_turtle
関数を作成できます。 正方形を描くときにカメを隠したいとします。
def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)
または、 Turtle
をサブクラス化して、その動作を組み込み、サブクラスをパラメーターとして渡すこともできます。
class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)
他の言語で…
JavaやC#などの他のいくつかのOOP言語には、ファーストクラスのクラスがありません。 クラスをインスタンス化するには、 new
キーワードの後に実際のクラス名を使用する必要があります。
この制限が、抽象ファクトリ(他のクラスをインスタンス化することだけが目的のクラスのセットを作成する必要がある)やファクトリメソッドパターンなどのパターンの理由です。 ご覧のとおり、Pythonでは、クラスは独自のファクトリであるため、パラメータとしてクラスを引き出すだけです。
基本クラスとしてのクラス
同じ機能を異なるクラスに追加するためにサブクラスを作成していることに気付いたとします。 たとえば、ログが作成されたときにログに書き出すTurtle
サブクラスが必要です。
import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Turtle got created")
しかし、その後、私たちは別のクラスでまったく同じことをしていることに気づきます。
class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Hippo got created")
これら2つの間で異なるのは次のとおりです。
- 基本クラス
- サブクラスの名前。ただし、これについてはあまり気にせず、基本クラスの
__name__
属性から自動的に生成できます。 -
debug
呼び出し内で使用される名前ですが、これも基本クラス名から生成できます。
バリアントが1つしかない、非常によく似た2つのコードに直面した場合、何ができるでしょうか。 最初の例と同じように、関数を作成し、バリアント部分をパラメーターとして引き出します。
def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("{0} got created".format(cls.__name__)) LoggingThing.__name__ = "Logging{0}".format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)
ここでは、ファーストクラスのクラスのデモンストレーションがあります。
- クラスを関数に渡し、キーワード
class
との衝突を避けるためにパラメーターに従来の名前cls
を付けました(この目的で使用されるclass_
とklass
も表示されます)。 - 関数内でクラスを作成しました。この関数を呼び出すたびに新しいクラスが作成されることに注意してください。
- そのクラスを関数の戻り値として返しました。
また、 LoggingThing.__name__
を設定します。これは完全にオプションですが、デバッグに役立ちます。
この手法のもう1つの用途は、クラスに追加したい機能がたくさんあり、これらの機能のさまざまな組み合わせを追加したい場合です。 必要なすべての異なる組み合わせを手動で作成すると、非常に扱いにくくなる可能性があります。
クラスが実行時ではなくコンパイル時に作成される言語では、これは不可能です。 代わりに、デコレータパターンを使用する必要があります。 このパターンはPythonで役立つ場合がありますが、ほとんどの場合、上記の手法を使用できます。
通常、私は実際にカスタマイズのためにたくさんのサブクラスを作成することを避けています。 通常、クラスをまったく含まない、より単純でより多くのPythonicメソッドがあります。 ただし、この手法は必要に応じて利用できます。 BrandonRhodesによるPythonでのデコレータパターンの完全な処理も参照してください。
例外としてのクラス
クラスが使用されていることを確認できるもう1つの場所は、try / exception/finallyステートメントのexcept
句です。 これらのクラスもパラメーター化できると推測しても驚くことではありません。
たとえば、次のコードは、失敗する可能性のあるアクションを試行し、最大試行回数に達するまで指数バックオフで再試行するという非常に一般的な戦略を実装しています。
import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)
実行するアクションと、パラメーターとしてキャッチする例外の両方を引き出しました。 パラメータexceptions_to_catch
は、 IOError
やhttplib.client.HTTPConnectionError
などの単一のクラス、またはそのようなクラスのタプルのいずれかです。 (これは他のプログラミングエラーを隠すことが知られているので、「裸の例外」句、またはexcept Exception
てさえ避けたいです)。
警告と結論
パラメータ化は、コードを再利用し、コードの重複を減らすための強力な手法です。 いくつかの欠点がないわけではありません。 コードの再利用を追求する中で、いくつかの問題がしばしば表面化します。
- 非常に一般的または抽象化されたコードで、理解が非常に困難になります。
- 実際には、パラメーターの特定の組み合わせのみが適切にテストされるため、全体像を覆い隠したり、バグを引き起こしたりするパラメーターが急増しているコード。
- コードベースのさまざまな部分の「共通コード」が1つの場所に分解されているため、役に立たない結合です。 2つの場所のコードが偶然に類似している場合があります。また、2つの場所は独立して変更する必要がある場合があるため、互いに独立している必要があります。
少しの「重複した」コードがこれらの問題よりもはるかに優れている場合があるため、この手法は注意して使用してください。
この投稿では、依存性注入、戦略、テンプレートメソッド、抽象ファクトリ、ファクトリメソッド、デコレータと呼ばれるデザインパターンについて説明しました。 Pythonでは、これらの多くは実際にはパラメーター化の単純なアプリケーションであることが判明するか、Pythonのパラメーターが呼び出し可能なオブジェクトまたはクラスである可能性があるという事実によって明らかに不要になります。 うまくいけば、これは「実際のPython開発者として知っているはずのこと」の概念的な負荷を軽減し、簡潔なPythonコードを記述できるようにするのに役立ちます。
参考文献:
- Pythonデザインパターン:洗練されたファッショナブルなコード用
- Pythonパターン:Pythonデザインパターンの場合
- Pythonロギング:詳細なチュートリアル