1. ホーム
  2. スクリプト・コラム
  3. パイソン

Pythonのインターフェースベースのプログラミングアプローチによる実装方法

2022-02-02 06:55:38

ソフトウェア業界において、唯一不変なものは「変化」です。プロダクトマネージャーは変わり、製品要件も変わり、コードもそれに合わせて変わらなければなりません。

要件変更のたびにリファクタリングに近い作業を行うものから、設定ファイルの変更、クラスのコード1行の変更など、変更に伴う作業量はコード設計によって異なります。より良いコード設計は、もちろん、拡張性が良く、結合性が高く、低結合であり、そのためメンテナンスが容易で、すべての変更に対応するために変更回数が少ない。良いコード設計にしたいのであれば、デザインパターンを学ぶ必要があります。今日は、Pythonでインターフェースに基づいたプログラミングをする方法を紹介します。

1994年に出版されたGoFの著書「デザインパターン」の重要な原則の一つは、プログラミングは実装ではなくインターフェースに基づくというもので、英語の原文は "Program to an interface, not an implementaion" となっています。インターフェイスは特定のプログラミング言語における言語に依存しないインターフェイスではなく、開発者がユーザーに提供する機能のリストであることを理解することが重要である。javaでは、インターフェイスはinterfaceというキーワードで実装されています。javaはクラスの多重継承をサポートしていませんが、インターフェイスの多重継承はサポートしており、javaの開発者はインターフェイスに非常に精通しています。

では、実際にインターフェースが何を解決してくれるのか、例で見てみましょう。

例えば、現在SevenNiu Cloudをストレージに使用している画像アップロード機能を実装する場合、クラスは次のようになります。

from django.conf.urls import url
from . import views
app_name = 'main'
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^response/', views.response, name='response'),
]

実際の開発では、何行ものコードと3つ以上の関数があり、それらが何百もの場所で呼び出され、何百ものファイルに散らばっています。しばらくすると、会社が自社のプライベートクラウドを構築し、「もう七尾のクラウドは使えないから、自社のクラウドストレージに切り替えてくれ」と言われ、以下のようなクラスを書き換える必要があります。

from django.shortcuts import render
import urllib
import urllib2
import json
def response(request):
if 'code' in request.GET:
url = 'https://api.instagram.com/oauth/access_token'
values = {
'client_id':'YOUR_CLIENT_ID',
'client_secret':'YOUR_CLIENT_SECRET',
'redirect_uri':'YOUR_REDIRECT_URI',
'code':request.GET.get('code'),
'grant_type':'authorization_code'
}
data = urllib.urlencode(values)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
response_string = response.read()
insta_data = json.loads(response_string)
if 'access_token' in insta_data and 'user' in insta_data:
#authentication success
return render(request, 'main/response.html')
else:
#authentication failure after step 2
return render(request, 'main/auth_error.html')
elif 'error' in req:
#authentication failure after step 1
return render(request, 'main/auth_error.html')

次に、7頭の牛を使用して場所に移動し、交換されますが、また、関数の名前を交換し、最終的に数回、アカウントに取られていない生活をテストする必要があり、エラーを報告するために実行されます。だから、簡単に変更するには、突然ある日、需要が再び、独自のクラウドサービスのためにしばしば秩序のうち、アリ雲を変更するように変更されました。前回の痛みを伴うトスのフラッシーの後、再び変更するには、この時間を参照してください、直接血を吐く。

問題は、十分に汎用的なコードを書いていないことと、抽象的な名前をつけていないことです。もし、クラスの関数名をupload,downloadのようにすれば、クラス名を置き換えるだけで修正するコードの量を半分に減らすことができます。実際、インターフェースを使ってコードの変更量を減らすことができます。インターフェースと実装を分離することで、不安定な実装をカプセル化し、安定したインターフェースを公開することができるのです。開発した機能を上流システムおよび下流システムが利用する際には、インターフェースで宣言された機能一覧を利用すればよいので、実装が変更されても上流システムのコードを変更する必要は基本的になく、カップリングの低減とスケーラビリティの向上を図ることができます。以下に、この問題に対するインターフェースベースのコード実装を示す。

インターフェイスの定義

from abc import ABCMeta, abstractmethod

class ImageStore(metaclass = ABCMeta):
    
    @abstractmethod
    def upload(self,image): 
        pass
        #raise NotImplementedError

    @abstractmethod
    def download(self,image): 
        pass
        #raise NotImplementedError

インターフェースを定義した後、それを継承するクラスはアップロードとダウンロードのメソッドをオーバーライドして正しく使用する必要があり、さもなければ両方とも例外をスローします。

インターフェイスを継承したクラスの定義

つまり、プログラムの堅牢性を確保するために、アップロードとダウンロードのメソッドを実装し、コンパイル時にチェックしなければならないということです。

class OwnImageStore2(ImageStore):

    def upload(self,image):
        print("Own upload.")
        #do something
    
    def download(self,image):
        print("Own download.")
        #do something

class QnyImageStore2(ImageStore):

    def getToken():
        pass

    def upload(self,image):
        print("Qny upload.")

    def download(self,image):
        print("Qny download.")
        #do something

次に、オブジェクトの型に応じて、対応するメソッドを自動的に選択して呼び出すインタフェースを定義する。

class UsedStore(object):

    def __init__(self, store):
        if not isinstance(store, ImageStore): raise Exception('Bad interface')
        self._store = store

    def upload(self):
        self._store.upload('image')

    def download(self):
        self._store.download('image')

最後に、どのインターフェイスを使用するかを設定ファイルに記述します。

# In other files, it should be called like this
img = QnyImageStore2()
# img = OwnImageStore2() Put these in the config file and just update the config file to replace 
store = UsedStore(img)
store.upload()
store.download()

こうしておけば、後から新しい画像ストアを追加する際にも、適切なクラスを追加してインターフェースを継承し、設定ファイルを修正するだけでよく、コードの修正作業が軽減されます。

Python抽象ベースクラス入門 (PEP3119)

Python標準ライブラリabc(正式名称:Abstract Base Classes)は、以下の機能を提供します。

  • isinstance() と issubclass() をオーバーロードするメソッド。
  • 新しいモジュールabcは、"Abstract Base Classes Support Framework"として提供されています。abc のメタクラスと、抽象メソッドを定義するために使用できるデコレータが定義されています。
  • コレクションモジュールに追加される、コンテナとイテレータのための特定の抽象ベースクラス

基本的なこと

オブジェクト指向プログラミングの分野では、オブジェクトと対話するためのデザインパターンは、基本的に「invocation」と「inspection」の2つに分類される。

呼び出しとは、オブジェクトのメソッドを呼び出すことでオブジェクトと対話することです。これはしばしばポリモーフィズムと組み合わせて使われ、あるメソッドを呼び出すと、オブジェクトの種類によって異なるコードが実行されることがあります。

インスペクションとは、(オブジェクトのメソッド以外の)外部コードがオブジェクトの型やプロパティを調べ、その情報に基づいてオブジェクトに対して何をすべきかを決定する機能のことです。

どちらの使用パターンも、多様で潜在的に新しいオブジェクトのセットを統一的な方法で処理することをサポートすると同時に、異なるタイプのオブジェクトごとにカスタム処理の決定を可能にするという、同じ一般的な目的を果たすものである。

古典的なOOP理論では、呼び出しが望ましいデザインパターンであり、検査は以前の手続き的プログラミングのスタイルの遺物と考えられているため、推奨されません。しかし、実際にはこの考え方はあまりにも独断的で硬直的であり、Pythonのような言語の動的な性質とは全く異なる、ある種の設計の硬直性につながっています。

特に、オブジェクトは、オブジェクトクラスの作成者が想定していない方法で処理される必要があることが多いのです。すべてのオブジェクトに、そのオブジェクトに対するあらゆるユーザーのニーズを満たすようなメソッドを組み込むことは、必ずしも最善の解決策ではありません。さらに、ルールやパターンマッチング駆動のロジックなど、オブジェクトに厳密にカプセル化された古典的なOOPの動作要件とは対照的な、強力なディスパッチ哲学が多く存在します。

一方、古典的なOOP理論家によるチェックに対する批判のひとつに、形式主義がなく、チェック対象が特殊であるというものがある。Pythonのように、オブジェクトのほとんどあらゆる側面を反映し、外部コードから直接アクセスできる言語では、オブジェクトが特定のプロトコルに準拠しているかどうかをテストする方法はさまざまにあります。例えば、"このオブジェクトはmutable sequence containerですか"と尋ねたら、"list"という基底クラスを探せばいいし、"getitem"というメソッドを探せばいいのです。しかし、これらのテストは明白に見えるかもしれませんが、どちらかが偽陰性、もう一方が偽陽性となるため、正しいものではないことに注意してください。

一般的に合意されている改善策は、テストを標準化し、正式な形にグループ化することである。これは、継承の仕組みやその他の方法で、各クラスにテスト可能なプロパティの標準セットを関連付けることで最も簡単に行うことができます。各テストには、約束事のセットが付属しています。クラスの一般的な動作に関する約束事と、 その他の利用可能なクラスメソッドに関する約束事が含まれています。

PEP では、これらのテストを整理するための特別な戦略として、ABC (Abstract Base Classes) と呼ばれるものを提案しています。ABC はオブジェクトの継承ツリーに追加される Python のクラスで、オブジェクトの特定の機能を外部のチェッカに送信するためのものです。テストは isinstance() を使って完了し、特定の ABC が存在すればテストに合格したことを意味します。

また、ABCは、その型の機能の動作を確立するための最小限のメソッドセットを定義している。ABCの型に基づいてオブジェクトを区別するコードは、これらのメソッドが常に存在することを信頼することができます。これらのメソッドはそれぞれ、ABCのドキュメントで説明されているように、一般化された抽象的なセマンティック定義が付属しています。これらの標準的なセマンティック定義は必須ではありませんが、強く推奨されます。

Pythonの他のすべてのものと同様に、これらの約束は紳士協定の性質を持っています。この場合、言語はABCで作られた約束の一部を強制しますが、具象クラスの実装者は残りの部分が維持されることを保証しなければならないことを意味します。

以上の説明を読んで、ABCは基底クラスであり、それを継承することで、インターフェイスにあるメソッドが常に存在し、それ以上探りを入れなくても安心して使えるJava的なインターフェイスを書くことができると、簡単に理解することができます。

PEP3119では、サンプルコードも提示していますので、参考にしてください。

from abc import ABCMeta, abstractmethod

class A(metaclass=ABCMeta):
    @abstractmethod
    def foo(self): pass

A() # raises TypeError

class B(A):
    pass

B() # raises TypeError

class C(A):
    def foo(self): print(42)

C() # works



以上、ABCを正しく使っていただければと思います。また、Pythonの公式ドキュメントやPEPプロポーザルで、最も権威ある説明をしていますので、ぜひ読んでみてください。

また、パターンを設定することは、プログラミングやプログラミングの技術にとって非常に重要で、基本中の基本であり、基礎力が十分でないと、戦闘機を目の前にしたときに、どう評価し味わえばいいのかさえもわからないでしょう。

デザインパターンをマスターした上で、他の人のコードを見れば、何がファイターで何がトラクターなのかを見抜く目が養われ、自分自身の学習や改善にとても役立ちますし、より保守性、可読性、拡張性、柔軟性に優れたコードを書くことができるようになるでしょう。

Pythonを使ってインターフェースベースのプログラミングを実現する方法については、今回で終了です。Python でのインターフェースベースプログラミングの詳細については、 Scripting House の過去の記事を検索するか、以下の記事を引き続き参照してください。