エキスパートPythonプログラミング読書会04に参加しました

エキスパートPythonプログラミング読書会04 : ATND に参加してきました。

今回は第3章のクラスのお話です。私は小規模開発や普通のアプリケーション開発にしか携わったことがないのですが、wikipedia:オブジェクト指向プログラミング を行うところの、本質的な良さや恩恵を実体験をもってはあまり理解できていません。とはいえ、そういった仕組みを理解して、より良いアプリケーション開発に有用なら使ってみたいですよね。そんな風に考えながら聞いていました。読書会で @ が解説していた内容の中で聞いていて私が印象に残ったことを整理してみます。

特殊メソッドを定義することで構文の振る舞いを簡単に実装できる

組み込み型の str を継承した mystr を定義して文字列の連結と比較を細工してみましょう。以下のサンプルでは、特殊メソッド __add__() で文字列の連結処理を、__eq__() で文字列の比較処理を独自に定義しています。

class mystr(str):
    def __add__(self, s):
        return "%s%s" %(self, s.upper())

    def __eq__(self, s):
        return "%s" % self == s.lower()

def test_mystr():
    """
    >>> test_mystr()
    concatenate: abcDEF
    comparison : True
    """
    my = mystr("abc")
    print "concatenate:", my + "def"
    print "comparison :", my == "ABC"

連結処理では対象文字列を大文字に、比較処理では小文字にして処理しています。標準ドキュメントの 特殊メソッド を見れば、そういった構文の振る舞いに対応する特種メソッドが見つかります。実際にどんな用途があるかまで私は分かりませんが、独自のカスタム型を定義することが本当に簡単にできてしまうというのがおもしろいですね。読書会の中では wikipedia:ワークアラウンド として使ったりできるかも?というやりとりがありました。

多重継承における MRO(メソッド解決順序) と super の役割を理解する

Python 2.2 から新スタイルクラスが導入されて、全ての組み込み型の共通の親クラスとして object という組み込み型が追加されました。新スタイルクラスを使用すると、共通の親クラスになる object が持つメソッドも考慮する必要があり、wikipedia:多重継承 における旧スタイルクラスの MRO では「まずい」ことが起こる可能性があるので Python 2.3 から C3 線形化アルゴリズム という新たな MRO が導入されました。

読書会では、新スタイルクラスで多重継承を行う以下のサンプルコードが紹介されていました。


class Base(object):
    def says(self):
        super(Base, self).says()
        print "Base"

class A(Base):
    def says(self):
        super(A, self).says()
        print "A"

class B(Base):
    def says(self):
        super(B, self).says()
        print "B"

class C(A, B):
    def says(self):
        super(C, self).says()
        print "C"

class Mixin(object):
    def says(self):
        print "Mixin"

class D(C, Mixin):
    def says(self):
        super(D, self).says()
        print "D"

def test_new_style_mro():
    """
    >>> test_new_style_mro()
    (<class 'use_super.D'>,
     <class 'use_super.C'>,
     <class 'use_super.A'>,
     <class 'use_super.B'>,
     <class 'use_super.Base'>,
     <class 'use_super.Mixin'>,
     <type 'object'>)
    Mixin
    Base
    B
    A
    C
    D
    """
    import inspect
    from pprint import pprint
    pprint(inspect.getmro(D))
    d = D()
    d.says()

このサンプルコードの MRO は以下になります。inspect.getmro() を使用すると簡単に調べられます。

says() メソッドを呼び出すときに MRO は D -> C -> A -> B -> Base -> Mixin -> object の順番でメソッドを探します。この順番の理由が知りたい人は C3 線形化アルゴリズムの論文を読んでくださいね。私は「そういうもんだ」と思って、次の実行結果を見ていきます(^ ^;;

D クラスの says() の中で

...
super(D, self).says() 
...

が呼び出されています。ここで重要なことがあります。

super は直接の親クラスの says() メソッドを呼び出すのではなく
MRO を辿っていく次のクラスの says() メソッドを呼び出します

以下の実行結果から

Mixin
Base
B
A
C
D

Base クラスの says() メソッド内での super は

...
super(Base, self).says()
...

Mixin クラスの says() メソッドを呼び出します。

比較検討するために、このサンプルコードを修正して別の動作も確認してみます。例えば D クラスの継承するクラスの順番を入れ換えるように修正してみます。

class D(Mixin, C):
    def says(self):
        super(D, self).says()
        print "D"

inspect.getmro() で調べると、

(<class 'use_super2.D'>,
 <class 'use_super2.Mixin'>,
 <class 'use_super2.C'>,
 <class 'use_super2.A'>,
 <class 'use_super2.B'>,
 <class 'use_super2.Base'>,
 <type 'object'>)

MRO は D -> Mixin -> C -> A -> B -> Base -> object の順番になります。しかし、実行結果は以下のようになります。

Mixin
D

ん? Mixin の次のクラスの says() が呼び出されていない、、、とよく見てみると、Mixin クラスの says() メソッドは super を呼び出していないことに気付きます。もちろん Mixin クラスの says() メソッドの中で super を呼び出すと C クラスの says() メソッドが呼び出されます。

class Mixin(object):
    def says(self):
        print "Mixin"

そのため、この先の MRO の says() メソッドは呼び出されません。つまり、

新スタイルクラスで多重継承するクラスの機能を利用したいときは
MRO を理解した上で必ず super を呼び出す必要があります

ここでサンプルコードの修正前と修正後で MRO の一番最後が object になっていることに気付きます。先に説明した、多重継承における旧スタイルクラスの MRO では「まずい」ことが起こる可能性があるというのは、要は新スタイルクラスの導入によって、C 言語側で実装された object が共通の親になり、その object のメソッドを探す順番が途中になるような MRO だと「まずい」ということなのかなと思います。解釈が間違ってたらツッコミください(> <)

また、ベストプラクティスとしては

  • 多重継承は避けるべき


と本書では推奨されています。参加者からは以下のような質問も出ました。

多重継承は避けるべきと言いながら、言語にその機能があるのはなぜですか?

読書会の中では

機能があってできることと、その機能をどう活用するかはまた別のお話です

といった回答に説得力があったように私は思いました。ここで言う「避けるべき」というのは、闇雲に乱用してはいけませんという注意を呼びかけているのであって、用途に応じてうまい活用方法もあるのだと思います。その流れで wikipedia:オブジェクト指向設計 の流行りとしては、

  • 継承よりも集約(Composition)を使う
  • 継承よりも Mixin を使う
  • 継承よりも委譲(Delegation)を使う


といった議論も出ました。デザインパターンの本とかにこの辺の話題の詳細があると思います*1Python の言語仕様を話してて、そういった実際の開発のお話に発展するのも個人的には興味深くて楽しめました。これが絶対正解というのもありませんしね。

Python における Mixin のサンプル

継承の議論を行っているときに一緒に wikipedia:Mixin についても話題に出ました。wikipedia の Mixin の例として PythonSocketServer モジュール が紹介されています。以下に SocketServer モジュールのコードを紹介します。

...
573 class ForkingUDPServer(ForkingMixIn, UDPServer): pass
574 class ForkingTCPServer(ForkingMixIn, TCPServer): pass
575 
576 class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
577 class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
...

process_request() をオーバーライドする形で ForkingMixIn クラスではリクエスト毎にプロセスの生成処理を、

473 class ForkingMixIn:
474 
475     """Mix-in class to handle each request in a new process."""
...
520     def process_request(self, request, client_address):
521         """Fork a new subprocess to process the request."""
522         self.collect_children()
523         pid = os.fork()
...

ThreadingMixIn ではスレッドの生成処理を実装しています。

544 class ThreadingMixIn:
545     """Mix-in class to handle each request in a new thread."""
...
564     def process_request(self, request, client_address):
565         """Start a new thread to process the request."""
566         t = threading.Thread(target = self.process_request_thread,
567                              args = (request, client_address))
568         if self.daemon_threads:
569             t.setDaemon (1)
570         t.start()
...

標準ライブラリドキュメントのサンプルをちょっと修正して、コマンドラインオプションで TCP サーバの受け付けるリクエスト毎の処理を切り替えるサンプルを作成して動作を確認してみました。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import optparse
import os
import socket
import sys
import threading
from SocketServer import (ForkingTCPServer, ThreadingTCPServer,
                          BaseRequestHandler)

class MyTCPRequestHandler(BaseRequestHandler):

    def handle(self):
        data = self.request.recv(1024)
        print data
        cur_thread = threading.currentThread()
        response = "Parent PID: %d, PID: %d, %s" % (
            os.getppid(), os.getpid(), cur_thread.getName())
        self.request.send(response)

def client(ip, port, message):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    sock.send(message)
    response = sock.recv(1024)
    print "Received: %s" % response
    sock.close()

def get_args():
    usage = "usage: %prog [options]"
    parser = optparse.OptionParser(usage)
    parser.add_option("-p", "--process", dest="process",
                action="store_true", default=False,
                help="forking (each request is handled by a new process)")
    parser.add_option("-t", "--thread", dest="thread",
                action="store_true", default=False,
                help="threading (each request is handled by a new thread)")
    opts, args = parser.parse_args()
    if opts.process or opts.thread:
        return opts
    else:
        parser.print_help()
        sys.exit(0)

def main():
    # Select Forking or Threading with option
    opts = get_args()
    if opts.process:
        _server = ForkingTCPServer
    elif opts.thread:
        _server = ThreadingTCPServer

    # Port 0 means to select an arbitrary unused port
    HOST, PORT = "localhost", 0

    server = _server((HOST, PORT), MyTCPRequestHandler)
    ip, port = server.server_address

    # Start a thread with the server -- that thread will then start one
    # more thread for each request
    server_thread = threading.Thread(target=server.serve_forever)
    # Exit the server thread when the main thread terminates
    server_thread.setDaemon(True)
    server_thread.start()
    print "Server loop running process: %d, thread:%s" % (
            os.getpid(), server_thread.getName())

    for i in range(3):
        client(ip, port, "-- Sending %d ..." % i)

    server.shutdown()

if __name__ == "__main__":
    main()

実行結果。

$ python socketserver_sample01.py -p
Server loop running process: 3781, thread:Thread-1
-- Sending 0 ...
Received: Parent PID: 3781, PID: 3783, Thread-1
-- Sending 1 ...
Received: Parent PID: 3781, PID: 3784, Thread-1
-- Sending 2 ...
Received: Parent PID: 3781, PID: 3785, Thread-1

$ python socketserver_sample01.py -t
Server loop running process: 3786, thread:Thread-1
-- Sending 0 ...
Received: Parent PID: 2145, PID: 3786, Thread-2
-- Sending 1 ...
Received: Parent PID: 2145, PID: 3786, Thread-3
-- Sending 2 ...
Received: Parent PID: 2145, PID: 3786, Thread-4

もしリクエストに対する独自処理を実装したいときは、以下の2つのクラスと必要なメソッドを実装すれば良いです。

class MyProcessingTCPServer(MyProcessingMixin, TCPServer):
    pass

class MyProcessingMixin:
    def process_request(self, request, client_address):
        # something to do

読書会の中では、Mixin を利用すると、モデルの実体やテストのモックを切り替えるときに便利だという話題も出てました。

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

*1:デザインパターンとともに学ぶオブジェクト指向のこころ」が分かり易かったです、私はデザインパターンについてこの本しか読んだことがないのですが(^ ^;;