スポンサーリンク

コードが書ける!数式が書ける!AAが書ける!スタンプが貼れる!

無料の匿名掲示板型SNS「このはちゃんねる

新規会員募集中!

ミックスインってなに?Pythonのコードで見るミックスインのやり方使い方

47, 2019-07-15

目次

ミックスインとは何なのか?

こんにちは、narupoです。

ミックスイン(Mixin)って聞いたことありませんか?
継承の親戚みたいだけど、どうやって使ったらいいのかわかりませんよね。
私もネットで調べてみたんですが

こう使え!

という説得力のある記事は見つけられませんでした。

この記事では今回、私が調べたミックスインについて書いていこうと思います。

ミックスインとは、クラスを拡張するクラスのことである

Wikipediaからの引用ですが、ミックスインとは↓の1行で表すことができます。

mixin とはオブジェクト指向プログラミング言語において、サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである

ふつうのクラスと何が違うのかというと、調べた範囲では単体で動作することを意図しないクラスというのが特徴のようです。
つまり、ほかのクラスに継承されれば機能するけど、ミックスインのクラス単体では使い物にはならない……そういう特徴を持ったクラスです。

普通はクラスといったら、インスタンス化して単独でも使えますよね。
でもミックスインはそうではなくて、最初からほかのクラスで利用されることを前提としているんですね。

どういうことなのか、実際にコードを見ていきたいと思います。

Pythonのコードで見るミックスイン

↓のようなコードがあります。
なかなかファンシーなコードですが、AnimalBaseを継承してLionを定義しているだけですね。
AnimalBaseのインスタンス変数はnameのみです。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class Lion(AnimalBase):
    """
    ライオン・クラス
    """
    pass


lion = Lion('タマ')
print(lion.name)
# タマ

このLionクラスを拡張するには、多重継承やAnimalBaseの拡張などいろいろな方法がありますが、今回はミックスインがテーマなのでミックスインで拡張してみます。

LionMixinで拡張

LionMixinというミックスイン・クラスを定義しました。
このLionMixinは、scratchというメソッドを継承先クラスに提供します。
このscratchメソッド内では、nameというインスタンス変数を参照しています。

LionMixinではnameを定義していないので、LionMixinをインスタンス化してもscratchは機能しません。
しかし、LionMixinがLionクラスに継承されると、インスタンスにはAnimalBaseで定義されたnameがあるのでscratchは機能するようになります。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class LionMixin:
    """
    ライオンの機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        print(self.name + 'がひっかいた!')


class Lion(AnimalBase, LionMixin):
    """
    ミックスインなどで生成される最終的なクラス
    """
    pass


lion = Lion('タマ')
lion.scratch()
# タマがひっかいた!

BirdMixinで拡張

BirdMixinというミックスインを定義してさらにクラスを拡張してみます。
BirdMixinはflyという機能を継承先クラスに提供します。
LionMixinとBirdMixinの特徴を受け継いだGriffinというクラスを宣言しています。
このようにミックスインを加えていくことでクラスを拡張していくことが可能になるのがわかると思います。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class LionMixin:
    """
    ライオンの機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        print(self.name + 'がひっかいた!')


class BirdMixin:
    """
    鳥の機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def fly(self):
        print(self.name + 'が飛んだ!')


class Griffin(AnimalBase, LionMixin, BirdMixin):
    """
    ミックスインなどで生成される最終的なクラス
    """
    pass


griffin = Griffin('タケシ')

griffin.scratch()
# タケシがひっかいた!

griffin.fly()
# タケシが飛んだ!

Mixinを使いまわせば簡単にクラスを作れる

さきほどはLionMixinとBirdMixinを継承してグリフォンを作りましたが、もちろんライオンや鳥も作れます。
このようにミックスインは使いまわせるのも特徴のひとつです。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class LionMixin:
    """
    ライオンの機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        print(self.name + 'がひっかいた!')


class BirdMixin:
    """
    鳥の機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def fly(self):
        print(self.name + 'が飛んだ!')


class Lion(AnimalBase, LionMixin):
    """ ライオン """
    pass


class Bird(AnimalBase, BirdMixin):
    """ 鳥 """
    pass


class Griffin(AnimalBase, LionMixin, BirdMixin):
    """ グリフォン """
    pass

ミックスインの委譲による実装

グリフォンの実装をミックスインではなくて委譲で実装してみるとどうなるでしょうか。
見たところミックスインと似ている気がしますね。
一般に、設計では継承ではなくて委譲を多用するのが良いと言われています。

class AnimalBase:
    def __init__(self, name):
        self.name = name


class Lion:
    def scratch(self, name):
        print(name + 'がひっかいた!')


class Bird:
    def fly(self, name):
        print(name + 'が飛んだ!')


class Griffin:
    def __init__(self, name):
        self.animal_base = AnimalBase(name)
        self.lion = Lion()
        self.bird = Bird()

    def scratch(self):
        self.lion.scratch(self.animal_base.name)

    def fly(self):
        self.bird.fly(self.animal_base.name)


griffin = Griffin('ケン')

griffin.scratch()
# ケンがひっかいた!

griffin.fly()
# ケンが飛んだ!

メソッドの優先順位

LionMixinとBirdMixinに同じscratchメソッドを定義するとどうなるのでしょうか。
Pythonでは継承する順番によって呼ばれるメソッドが変わります。
↓ではGriffin(AnimalBase, LionMixin, BirdMixin)のように、LionMixinを先に取り込んでいます。
こうするとLionMixinのscratchが優先されます。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class LionMixin:
    """
    ライオンの機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        print(self.name + 'が大きな爪でひっかいた!')


class BirdMixin:
    """
    鳥の機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        print(self.name + 'が鋭い爪でひっかいた!')


class Griffin(AnimalBase, LionMixin, BirdMixin):
    """
    ミックスインなどで生成される最終的なクラス
    """
    pass


griffin = Griffin('タケシ')
griffin.scratch()
# タケシが大きな爪でひっかいた!

逆にBirdMixinを先に継承するとBirxMixinのscratchが優先されます。

class Griffin(AnimalBase, BirdMixin, LionMixin):
    """
    ミックスインなどで生成される最終的なクラス
    """
    pass

griffin = Griffin('タケシ')
griffin.scratch()
# タケシが鋭い爪でひっかいた!

バリデーターをミックスインする

グリフォンにバリデーターを追加したい場合は↓のような実装例がありえます。
ミックスインのクラスはクラスを継承しませんが、インスタンスをチェックすることでほかの実装を参照できるようになります。
しかし、可能であることは可能ですが、この設計が果たして一般的、効果的なのかどうかは不明です。

class AnimalBase:
    """
    ベースになる動物クラス
    """
    def __init__(self, name):
        self.name = name


class LionMixin:
    """
    ライオンの機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def scratch(self):
        if hasattr(self, 'validate_name'):
            self.validate_name()
        print(self.name + 'がひっかいた!')


class BirdMixin:
    """
    鳥の機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def fly(self):
        if hasattr(self, 'validate_name'):
            self.validate_name()
        print(self.name + 'が飛んだ!')


class NameValidatorMixin:
    """
    名前をバリデーションする機能を提供するミックスイン
    このクラス単体ではnameが無いためインスタンス化しても機能しない
    """
    def validate_name(self):
        if not isinstance(self.name, str):
            self.name = '???'


class Griffin(AnimalBase, LionMixin, BirdMixin, NameValidatorMixin):
    """
    ミックスインなどで生成される最終的なクラス
    """
    pass


griffin = Griffin(None)

griffin.scratch()
# ???がひっかいた!

griffin.fly()
# ???が飛んだ!

ミックスインはPythonの標準ライブラリではどう使われているのか?

ミックスインの実用的なサンプルを探しましたが、なかなか見つかりませんでした。
しかし、Wikipediaを見ているとどうやらThreadingTCPServerというクラスでミックスインが使用されているようです。
このクラスはPythonの標準ライブラリのsocketserver.py というモジュールに含まれています。

このモジュールでは、↓のようにミックスインによって複数のサーバーを宣言しています。
ForkingTCPServerThreadingTCPServerなどがそうですね。この2つはTCPServerを再利用している所に注目してください。
そしてサーバーの特徴をForkingMixInThreadingMixInで実装している所にも注目してください。

if hasattr(os, "fork"):
    class ForkingUDPServer(ForkingMixIn, UDPServer): pass
    class ForkingTCPServer(ForkingMixIn, TCPServer): pass

class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

どのようにしてミックスインで複数のサーバーを実現しているのでしょうか?

たとえば、ThreadingMixInではserver_closeメソッドを定義しています。
このメソッドは、親クラスのserver_closeを呼び出した後、ThreadingMixIn独自のクローズ処理を行っています。
ThreadingMixInは親クラスを継承していないミックスインなので、このserver_closeメソッドはThreadingMixIn単体では機能しません(単体では親クラスを呼び出せない)。

ThreadingTCPServerの場合、このクラスはThreadingMixInとTCPServerを継承しているので、ThreadingMixInのserver_close内ではTCPServerのserver_closeが呼ばれています。
つまり、ThreadingMixInによってTCPServerのserver_closeを拡張しているわけですね。この拡張によってThreadingTCPServerという名称に似合った機能がこのクラスに備わっていると言えます。

↓がThreadingMixInとTCPServerのコードです。server_closeメソッドに注目してみてください。

class ThreadingMixIn:
    """Mix-in class to handle each request in a new thread."""

    # Decides how threads will act upon termination of the
    # main process
    daemon_threads = False
    # If true, server_close() waits until all non-daemonic threads terminate.
    block_on_close = True
    # For non-daemonic threads, list of threading.Threading objects
    # used by server_close() to wait for all threads completion.
    _threads = None

    def process_request_thread(self, request, client_address):
        """Same as in BaseServer but as a thread.

        In addition, exception handling is done here.

        """
        try:
            self.finish_request(request, client_address)
        except Exception:
            self.handle_error(request, client_address)
        finally:
            self.shutdown_request(request)

    def process_request(self, request, client_address):
        """Start a new thread to process the request."""
        t = threading.Thread(target = self.process_request_thread,
                             args = (request, client_address))
        t.daemon = self.daemon_threads
        if not t.daemon and self.block_on_close:
            if self._threads is None:
                self._threads = []
            self._threads.append(t)
        t.start()

    def server_close(self):
        super().server_close()
        if self.block_on_close:
            threads = self._threads
            self._threads = None
            if threads:
                for thread in threads:
                    thread.join()
class TCPServer(BaseServer):

    """Base class for various socket-based server classes.

    Defaults to synchronous IP stream (i.e., TCP).

    Methods for the caller:

    - __init__(server_address, RequestHandlerClass, bind_and_activate=True)
    - serve_forever(poll_interval=0.5)
    - shutdown()
    - handle_request()  # if you don't use serve_forever()
    - fileno() -> int   # for selector

    Methods that may be overridden:

    - server_bind()
    - server_activate()
    - get_request() -> request, client_address
    - handle_timeout()
    - verify_request(request, client_address)
    - process_request(request, client_address)
    - shutdown_request(request)
    - close_request(request)
    - handle_error()

    Methods for derived classes:

    - finish_request(request, client_address)

    Class variables that may be overridden by derived classes or
    instances:

    - timeout
    - address_family
    - socket_type
    - request_queue_size (only for stream sockets)
    - allow_reuse_address

    Instance variables:

    - server_address
    - RequestHandlerClass
    - socket

    """

    address_family = socket.AF_INET


    socket_type = socket.SOCK_STREAM

    request_queue_size = 5

    allow_reuse_address = False

    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
        """Constructor.  May be extended, do not override."""
        BaseServer.__init__(self, server_address, RequestHandlerClass)
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if bind_and_activate:
            try:
                self.server_bind()
                self.server_activate()
            except:
                self.server_close()
                raise

    def server_bind(self):
        """Called by constructor to bind the socket.

        May be overridden.

        """
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
        self.server_address = self.socket.getsockname()

    def server_activate(self):
        """Called by constructor to activate the server.

        May be overridden.

        """
        self.socket.listen(self.request_queue_size)

    def server_close(self):
        """Called to clean-up the server.

        May be overridden.

        """
        self.socket.close()

    def fileno(self):
        """Return socket file number.

        Interface required by selector.

        """
        return self.socket.fileno()

    def get_request(self):
        """Get the request and client address from the socket.

        May be overridden.

        """
        return self.socket.accept()

    def shutdown_request(self, request):
        """Called to shutdown and close an individual request."""
        try:
            #explicitly shutdown.  socket.close() merely releases
            #the socket and waits for GC to perform the actual close.
            request.shutdown(socket.SHUT_WR)
        except OSError:
            pass #some platforms may raise ENOTCONN here
        self.close_request(request)

    def close_request(self, request):
        """Called to clean up an individual request."""
        request.close()

おわりに

ミックスインは一般に多重継承より安全だと言われています。
たとえば、Rubyでは多重継承を禁止する代わりにミックスインの機能を提供しています。

しかし、それでもちゃんと考察した設計が必要であることに変わりはありません。

設計はむずかしい

以上、narupoでした。

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク