Pythonデザインパターン:洗練されたファッショナブルなコード用
公開: 2022-03-11もう一度言いましょう。Pythonは、動的型付けと動的バインディングを備えた高級プログラミング言語です。 私はそれを強力で高レベルの動的言語として説明します。 多くの開発者は、Pythonの明確な構文、適切に構造化されたモジュールとパッケージ、およびその非常に高い柔軟性と最新機能の範囲により、Pythonを愛しています。
Pythonでは、クラスを記述してそれらからオブジェクトをインスタンス化する必要はありません。 プロジェクトに複雑な構造が必要ない場合は、関数を作成するだけです。 さらに良いことに、コードをまったく構造化せずに、単純で迅速なタスクを実行するためのフラットなスクリプトを作成できます。
同時に、Pythonは100パーセントオブジェクト指向言語です。 どのようだ? 簡単に言えば、Pythonのすべてがオブジェクトです。 関数はオブジェクトであり、ファーストクラスのオブジェクトです(それが意味するものは何でも)。 関数がオブジェクトであるというこの事実は重要なので、覚えておいてください。
したがって、Pythonで簡単なスクリプトを作成することも、Pythonターミナルを開いて、そこでステートメントを実行することもできます(これはとても便利です!)。 しかし同時に、複雑なフレームワーク、アプリケーション、ライブラリなどを作成することもできます。 Pythonでは多くのことができます。 もちろん、いくつかの制限がありますが、それはこの記事のトピックではありません。
ただし、Pythonは非常に強力で柔軟性があるため、Pythonでプログラミングする場合は、いくつかのルール(またはパターン)が必要です。 それでは、パターンとは何か、そしてそれらがPythonとどのように関連しているかを見てみましょう。 また、いくつかの重要なPythonデザインパターンの実装に進みます。
Pythonがパターンに適しているのはなぜですか?
どのプログラミング言語もパターンに適しています。 実際、パターンは、特定のプログラミング言語のコンテキストで検討する必要があります。 パターン、言語構文、および性質の両方が、プログラミングに制限を課します。 言語構文と言語の性質(動的、機能、オブジェクト指向など)に起因する制限は、それらの存在の背後にある理由と同様に、異なる可能性があります。 パターンに起因する制限には理由があり、それらは意図的なものです。 それがパターンの基本的な目標です。 何かをする方法としない方法を教えてください。 パターン、特にPythonデザインパターンについては後で説明します。
Pythonの哲学は、よく考えられたベストプラクティスのアイデアに基づいて構築されています。 Pythonは動的言語であり(私はすでに言ったのですか?)、そのため、数行のコードで多くの一般的なデザインパターンをすでに実装しているか、実装を容易にしています。 一部のデザインパターンはPythonに組み込まれているため、知らないうちに使用します。 言語の性質上、他のパターンは必要ありません。
たとえば、 Factoryは、新しいオブジェクトを作成し、インスタンス化ロジックをユーザーから隠すことを目的とした構造的なPythonデザインパターンです。 ただし、Pythonでのオブジェクトの作成は設計上動的であるため、Factoryのような追加は必要ありません。 もちろん、必要に応じて自由に実装できます。 本当に便利な場合もあるかもしれませんが、それは例外であり、標準ではありません。
Pythonの哲学の何がそんなに良いのでしょうか? これから始めましょう(Pythonターミナルで調べてください):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
これらは従来の意味でのパターンではないかもしれませんが、これらはプログラミングへの「Pythonic」アプローチを最もエレガントで便利な方法で定義するルールです。
コードの構造化に役立つPEP-8コードガイドラインもあります。 もちろん、いくつかの適切な例外を除いて、それは私にとって必須です。 ちなみに、これらの例外はPEP-8自体によって推奨されています。
しかし、最も重要なことは、いつ矛盾するかを知ることです。スタイルガイドが適用されない場合もあります。 疑わしい場合は、最善の判断を下してください。 他の例を見て、何が最もよく見えるかを決定します。 そして、遠慮なく質問してください!
PEP-8をZenofPython(PEP-PEP-20)と組み合わせると、読み取り可能で保守可能なコードを作成するための完璧な基盤が得られます。 デザインパターンを追加すれば、一貫性と進化可能性を備えたあらゆる種類のソフトウェアシステムを作成する準備が整います。
Pythonデザインパターン
デザインパターンとは何ですか?
すべてはGangofFour(GOF)から始まります。 GOFに慣れていない場合は、オンラインで簡単に検索してください。
デザインパターンは、よく知られている問題を解決するための一般的な方法です。 2つの主要な原則は、GOFによって定義されたデザインパターンの基礎にあります。
- 実装ではなく、インターフェースへのプログラム。
- 継承よりもオブジェクトコンポジションを優先します。
Pythonプログラマーの観点から、これら2つの原則を詳しく見ていきましょう。
実装ではなくインターフェースへのプログラム
ダックタイピングについて考えてみてください。 Pythonでは、これらのインターフェイスに従ってインターフェイスとプログラムクラスを定義するのは好きではありませんか? しかし、私に耳を傾けてください! これは、インターフェイスについて考えていないという意味ではありません。実際、Duck Typingでは、常にそうしています。
悪名高いダックタイピングアプローチについていくつかの言葉を言って、それがこのパラダイムにどのように適合するかを見てみましょう:インターフェイスへのプログラム。
オブジェクトの性質を気にする必要はありません。オブジェクトが何であるかを気にする必要はありません。 必要なことを実行できるかどうかを知りたいだけです(オブジェクトのインターフェースにのみ関心があります)。
オブジェクトはクワクすることができますか? だから、それをいじめましょう!
try: bird.quack() except AttributeError: self.lol()
アヒルのインターフェースを定義しましたか? 番号! 実装ではなく、インターフェースにプログラムしましたか? はい! そして、これはとてもいいと思います。
Alex MartelliがPythonのデザインパターンに関するよく知られたプレゼンテーションで指摘しているように、 「アヒルにタイプを教えるのには時間がかかりますが、後で多くの作業を節約できます!」
継承よりもオブジェクトコンポジションを優先する
さて、それは私がPythonicの原則と呼んでいるものです! 1つのクラス(または多くの場合、複数のクラス)を別のクラスでラップする場合に比べて、作成するクラス/サブクラスの数は少なくなります。
これを行う代わりに:
class User(DbObject): pass
次のようなことができます。
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
利点は明らかです。 ラップされたクラスのどのメソッドを公開するかを制限できます。 実行時にパーシスターインスタンスを注入できます! たとえば、今日はリレーショナルデータベースですが、明日は必要なインターフェイスを備えたものであれば何でもかまいません(これも厄介なアヒルです)。
構成はPythonにとってエレガントで自然です。
行動パターン
行動パターンには、オブジェクト間のコミュニケーション、オブジェクトがどのように相互作用し、特定のタスクを実行するかが含まれます。 GOFの原則によると、Pythonには、責任の連鎖、コマンド、インタープリター、イテレーター、メディエーター、Memento、オブザーバー、状態、戦略、テンプレート、ビジターの合計11の動作パターンがあります。
これらのパターンは非常に便利だと思いますが、これは他のパターングループがそうではないという意味ではありません。
イテレータ
イテレータはPythonに組み込まれています。 これは、この言語の最も強力な特性の1つです。 数年前、私はイテレータがPythonを素晴らしいものにしていることをどこかで読みましたが、これは今でも当てはまると思います。 Pythonイテレーターとジェネレーターについて十分に学ぶと、この特定のPythonパターンについて必要なすべてを知ることができます。
責任の連鎖
このパターンは、それぞれがリクエストの特定の部分に対応するさまざまなメソッドを使用してリクエストを処理する方法を提供します。 ご存知のとおり、優れたコードの最良の原則の1つは、単一責任の原則です。
すべてのコードは、1つだけのことを実行する必要があります。
この原則は、このデザインパターンに深く統合されています。
たとえば、一部のコンテンツをフィルタリングする場合は、さまざまなフィルターを実装できます。各フィルターは、正確で明確に定義された1つのタイプのフィルタリングを実行します。 これらのフィルタは、不快な言葉、広告、不適切な動画コンテンツなどをフィルタリングするために使用できます。
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
指示
これは、私がプログラマーとして実装した最初のPythonデザインパターンの1つです。 それは私に思い出させます:パターンは発明されていません、それらは発見されています。 それらは存在します。私たちはそれらを見つけて使用する必要があります。 これは、何年も前に実装したすばらしいプロジェクトである、特別な目的のWYSIWYMXMLエディターで発見しました。 このパターンをコードで集中的に使用した後、いくつかのサイトで詳細を読みました。
コマンドパターンは、何らかの理由で、実行するものを準備することから始めて、必要に応じて実行する必要がある場合に便利です。 利点は、このような方法でアクションをカプセル化することで、Python開発者が、実行されたアクションに関連する追加機能(元にたり、やり直しなど)を追加したり、アクションの履歴を保持したりできることです。
単純で頻繁に使用される例がどのように見えるかを見てみましょう。
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
創造的パターン
まず、Pythonでは作成パターンが一般的に使用されていないことを指摘しましょう。 なんで? 言語の動的な性質のため。
FactoryはPythonに組み込まれていると私がかつて言ったよりも賢い人がいます。 これは、言語自体が、十分にエレガントな方法でオブジェクトを作成するために必要なすべての柔軟性を提供することを意味します。 シングルトンやファクトリーなど、何かを上に実装する必要はめったにありません。
あるPythonデザインパターンのチュートリアルで、これらのデザインが「新しい演算子を使用してオブジェクトを直接インスタンス化するのではなく、作成ロジックを非表示にしてオブジェクトを作成する方法を提供する」と述べた作成デザインパターンの説明を見つけました。
これは問題をほぼ要約しています。Pythonには新しい演算子がありません。
それでも、そのようなパターンを使用することで利点が得られると思われる場合は、いくつかを実装する方法を見てみましょう。

シングルトン
シングルトンパターンは、実行時に特定のクラスのインスタンスが1つだけ存在することを保証する場合に使用されます。 Pythonでこのパターンが本当に必要ですか? 私の経験によると、シングルトンパターンを実装する代わりに、意図的に1つのインスタンスを作成し、それを使用する方が簡単です。
しかし、それを実装したい場合は、ここにいくつかの良いニュースがあります。Pythonでは、インスタンス化プロセスを(事実上他のものと一緒に)変更できます。 前に述べた__new__()
メソッドを覚えていますか? どうぞ:
class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
この例では、ロガーはシングルトンです。
これらは、Pythonでシングルトンを使用する代わりの方法です。
- モジュールを使用します。
- アプリケーションのトップレベルのどこかに、おそらく構成ファイルに1つのインスタンスを作成します。
- インスタンスを、それを必要とするすべてのオブジェクトに渡します。 これは依存性注入であり、強力で簡単に習得できるメカニズムです。
依存性注入
依存性注入がデザインパターンであるかどうかについて説明するつもりはありませんが、これはルーズカップリングを実装するための非常に優れたメカニズムであり、アプリケーションの保守と拡張に役立ちます。 ダックタイピングと組み合わせると、フォースがあなたと一緒になります。 いつも。
オブジェクトがいつ(またはさらに良いのはどこで)作成されるかという問題を扱っているので、この投稿の作成パターンのセクションにリストしました。 外で作成されます。 オブジェクトは、それらを使用する場所ではまったく作成されないため、依存関係は、それが消費される場所では作成されないということをお勧めします。 コンシューマーコードは、外部で作成されたオブジェクトを受け取り、それを使用します。 詳細については、このStackoverflowの質問に対する最も賛成の回答をお読みください。
これは依存性注入の良い説明であり、この特定の手法の可能性についての良いアイデアを与えてくれます。 基本的に、答えは次の例の問題を説明しています。自分で冷蔵庫から飲み物を取り出さないで、代わりに必要性を述べてください。 昼食と一緒に何か飲む必要があることを両親に伝えてください。
Pythonは、それを簡単に実装するために必要なすべてを提供してくれます。 JavaやC#などの他の言語での実装の可能性について考えてみてください。そうすれば、Pythonの美しさにすぐに気付くでしょう。
依存性注入の簡単な例を考えてみましょう。
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
Commandクラスにオーセンティケーターメソッドとオーソライザーメソッドを挿入します。 Commandクラスに必要なのは、実装の詳細に煩わされることなく、それらを正常に実行することだけです。 このようにして、実行時に使用することを決定した認証および承認メカニズムでCommandクラスを使用できます。
コンストラクターを介して依存性を注入する方法を示しましたが、オブジェクトのプロパティを直接設定することで簡単に依存性を注入でき、さらに多くの可能性を解き放ちます。
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
依存性注入について学ぶことはもっとたくさんあります。 たとえば、好奇心旺盛な人はIoCを検索します。
ただし、その前に、この質問に対して最も賛成の回答である別のStackoverflowの回答をお読みください。
繰り返しになりますが、Pythonでこの素晴らしいデザインパターンを実装することが、言語の組み込み機能を使用するだけの問題であることを示しました。
これが何を意味するかを忘れないでください。依存性注入手法により、非常に柔軟で簡単な単体テストが可能になります。 データの保存をオンザフライで変更できるアーキテクチャを想像してみてください。 データベースをモックするのは簡単な作業になりますね。 詳細については、ToptalのPythonでのモックの概要を確認してください。
また、プロトタイプ、ビルダー、ファクトリのデザインパターンを調査することもできます。
構造パターン
ファサード
これは、最も有名なPythonデザインパターンである可能性があります。
かなりの数のオブジェクトを含むシステムがあると想像してください。 すべてのオブジェクトは、豊富なAPIメソッドのセットを提供しています。 このシステムで多くのことができますが、インターフェースを単純化してはどうでしょうか。 すべてのAPIメソッドのよく考えられたサブセットを公開するインターフェイスオブジェクトを追加してみませんか? ファサード!
Pythonファサードデザインパターンの例:
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
驚きもトリックもありませんCar
クラスはファサードであり、それだけです。
アダプタ
ファサードがインターフェースの単純化に使用される場合、アダプターはすべてインターフェースを変更することを目的としています。 システムがアヒルを期待しているときに牛を使用するようなものです。
特定の宛先に情報を記録するための実用的な方法があるとしましょう。 このメソッドは、宛先にwrite()
メソッドがあることを想定しています(たとえば、すべてのファイルオブジェクトにあるように)。
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
これは、依存性注入を使用した適切に記述されたメソッドであり、拡張性が大幅に向上します。 ファイルではなくUDPソケットにログを記録したい場合、このUDPソケットを開く方法は知っていますが、唯一の問題は、 socket
オブジェクトにwrite()
メソッドがないことです。 アダプターが必要です!
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
しかし、なぜアダプターがそれほど重要だと思うのですか? まあ、それが依存性注入と効果的に組み合わされるとき、それは私たちに大きな柔軟性を与えます。 新しいインターフェイスをよく知られたインターフェイスに変換するアダプタを実装できるのに、なぜ新しいインターフェイスをサポートするように十分にテストされたコードを変更するのでしょうか。
また、アダプターと類似しているため、ブリッジとプロキシのデザインパターンを確認してマスターする必要があります。 Pythonでの実装がいかに簡単かを考え、プロジェクトでそれらを使用できるさまざまな方法を考えてください。
デコレータ
なんてラッキーなんだ! デコレータは本当に素晴らしく、すでに言語に統合されています。 私がPythonで最も気に入っているのは、Pythonを使用することでベストプラクティスを使用する方法を教えてくれることです。 ベストプラクティス(特にデザインパターン)を意識する必要がないわけではありませんが、Pythonを使用すると、ベストプラクティスに従っているように感じます。 個人的には、Pythonのベストプラクティスは直感的で第二の性質であり、これは初心者とエリート開発者の両方に高く評価されています。
デコレータパターンは、追加機能を導入すること、特に継承を使用せずにそれを行うことです。
それでは、組み込みのPython機能を使用せずにメソッドを装飾する方法を確認しましょう。 これは簡単な例です。
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
ここであまり良くないのは、 execute
関数が何かを実行する以上のことをするということです。 私たちは、この書簡に対する単一責任の原則に従っていません。
次のように書くとよいでしょう。
def execute(action): return action()
次のように、デコレータの別の場所に任意の承認および認証機能を実装できます。
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
これで、 execute()
メソッドは次のようになります。
- 読みやすい
- 1つのことだけを行います(少なくともコードを見るとき)
- 認証で飾られています
- 認可で飾られています
Pythonの統合デコレータ構文を使用して同じように記述します。
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
デコレータとしての機能に限定されないことに注意することが重要です。 デコレータには、クラス全体が含まれる場合があります。 唯一の要件は、それらが呼び出し可能でなければならないということです。 しかし、問題はありません。 __call__(self)
メソッドを定義する必要があります。
Pythonのfunctoolsモジュールを詳しく調べることもできます。 そこには発見することがたくさんあります!
結論
Pythonのデザインパターンを使用するのがいかに自然で簡単かを示しましたが、Pythonでのプログラミングもいかに簡単であるかを示しました。
「複雑なものよりも単純なものの方が優れています」と覚えていますか? おそらく、デザインパターンのどれも完全かつ正式に記述されていないことに気づいたかもしれません。 複雑な本格的な実装は示されていません。 あなたは自分のスタイルとニーズに最も合う方法でそれらを「感じ」そして実行する必要があります。 Pythonは優れた言語であり、柔軟で再利用可能なコードを作成するために必要なすべての機能を提供します。
しかし、それだけではありません。 それはあなたに本当に悪いコードを書くための「自由」を与えます。 しないでください! 自分を繰り返さないでください(DRY)。80文字を超えるコード行を記述しないでください。 また、該当する場合はデザインパターンを使用することを忘れないでください。 これは、他の人から学び、彼らの豊富な経験から無料で得るための最良の方法の1つです。