Help us understand the problem. What is going on with this article?

type hinting で複数の Protocol を実装したクラスの型を表現する

Protocol / Interface

作っているプログラムが複雑になってくると、 Interface (python の世界では Protocol) を使って複数のクラスが同じインターフェースを持つことを保証したくなります。
特に mypy など type hinting の仕組みを使い始めるとそのようなものが欲しくなります。

このようなインターフェースは python では Protocol というものを使うことで実装できます。
例えばあるクラスの情報を DB に書き出すために、 serialize というメソッドを実装したクラス群があるとしましょう。この serialize というインターフェースを Serializable と呼ぶことにします。

from typing import Dict, Any
from typing_extensions import Protocol

class Serializable(Protocol):
    def serialize(self) -> Dict[str, Any]:
        ...

class A:
    def __init__(self) -> None:
        self.data = None

    def serialize(self) -> Dict[str, Any]:
        return {'data': self.data}

    def run(self) -> None:
        self.data = 'run result'


def print_serialized(instance: Serializable) -> None:
    print(instance.serialize())


a = A()
print_serialized(a)

print_serialized は何らか serialize メソッドを持ったクラスを受け取り、そのシリアライズ結果をプリントします。
いったいそのインスタンスがどんなクラスなのか、何をシリアライズするのかとかは知らなくてすみます。

複数の Protocol を実装したクラスを作りたい

さて、先程のクラス A には run というメソッドがありました。
このように、何らか run というメソッドをもっているというインターフェースを Runnable と呼ぶことにしましょう。
そして、 Runnable と Serializable の両方を持っているクラスのインスタンスを受け取ってなにか処理を行うことを考えます。

from typing import Dict, Any
from typing_extensions import Protocol


class Serializable(Protocol):
    def serialize(self) -> Dict[str, Any]:
        ...


class Runnable(Protocol):
    def run(self) -> None:
        ...


class A:
    def __init__(self) -> None:
        self.data = None

    def serialize(self) -> Dict[str, Any]:
        return {'data': self.data}

    def run(self) -> None:
        self.data = 'run result'


# ダサいぞ!!!
class RunAndSerializable(Serializable, Runnable, Protocol):
    ...


def print_result(instance: RunAndSerializable) -> None:
    instance.run()
    print(instance.serialize())

こういう書き方をすることになります。

print_result ですが以下のように書きたいですね。。

def print_result(instance: Intersection[Serializable, Runnable]) -> None:  # Serializable/Runnable 両方のインターフェースを実装しているクラス
    instance.run()
    print(instance.serialize())

このような Intersection 型ですが、2019/03現在検討はされているようですが、まだ実装には入っていないようです。
https://github.com/python/typing/issues/213

書かれていることを見ると

  • Intersection は型一般に対応しようと思うと、 Generic などへの対応が非常に複雑になる
  • Protocol のみに対応なら簡単だけど、それだと Union との対称性が無かったり、ニッチすぎる
  • よって今はまだやる時期じゃない

個人的には Protocol ベースなプログラミングをする上では非常に重要な機能なのでぜひ実装してもらいたい気持ちです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away