PyCon APAC 2013参加レポート

第2回Pythonによる開発運用を助けるツールたち ─パッケージシステム、DataDogでモニタリング、mockを使ったテスト

PyCon APAC参加レポート第2回は、初日の日本語セッションを中心にまとめたいと思います。1日目と2日目のキーノートは第1回の記事にまとめてありますのでお読みください。

パッケージの未来

1日目の一番初めのセッションは、@aodag(小田切 篤)さんによるパッケージの未来に関するお話です。このセッションは、Pythonに10年間お世話になっているというaodagさんが、これまでのPythonのパッケージ管理システムの歴史とPython 3のパッケージ管理システムについて語るというもの。セッションではさらにその先に関する発表もありました。

画像

今までのパッケージングシステム

Pythonのパッケージングシステムの歴史は複雑で、これまでパッケージングの補助ツールとして二系統が存在していました。

  • distribute
  • setuptools

またパッケージのインストールには以下のコマンドを使用します。

pip
サードパーティのパッケージインストーラ
easy_install
distribute/setuptoolsに含まれるインストールコマンド

標準で入っているdistutilsもあるのですが、これだけでは機能が足りたいため、パッケージングにはサードパーティによる上記のツールを活用します。もし、これらのツールを使わない場合、以下のようなコマンドを使ってパッケージングする必要があります。

python setup.py sdist    # ソースパッケージの作成
python setup.py install  # パッケージインストール

また、パッケージに関して重要なツール群を管理しているPypa(Python Packaging Authority)団体があり、この団体は以下のツール群を管理しています。

  • virtualenv:仮想環境の構築
  • pip:パッケージインストーラ
  • setuptools:パッケージ作成の補助ツール

これらはGithubとBitbucketにリポジトリが分かれているので、ソースコードを探すには注意が必要です。

easy_installとpip

パッケージのインストール方法について、ライブラリのドキュメントによっては

easy_install [package_name]

と書いてあったり、

pip install [package_name]

と書いてあったりします。

これらの違いは、pipがパッケージのインストールを補助するライブラリで、easy_installはdistribute/setuptoolsについてくる単純なインストールコマンドだという点にあります。pipのほうが高機能でさまざまなことができます。

pip install [package_name]
# パッケージのインストール(デフォルトではPyPIからダウンロードしてきてインストール)

pip install [package_url]
# 特定URLから直接インストール

pip install [vcs{git,hg,...}+[repository_url]@[change_set]]
# GitやMercurialのリモートリポジトリから直接インストール

pip install -r requirements.txt
# requirements.txtに書かれたパッケージを全てインストール

pip uninstall [package_name]
# パッケージをアンインストール

easy_installとpipでパッケージがインストールされるディレクトリ等が異なってくるので、どちらか片方のみを使うほうが無難です。aodagさんからは「パッケージインストールには高機能なpipを使ってください。」との発言がありました。

virtualenvを使えば、環境構築時に自動でpipがインストールされます。

distributeとsetuptools

distributeとsetuptoolsは同じ役割を果たすライブラリですが、複雑な経緯から二系統存在しています。distributeはsetuptoolsからフォークされたプロジェクトで、 "今まで" はdistributeがpython3対応などを積極的に行っていました。しかし、PyCon US 2013でマージ宣言され、distributeがsetuptoolsに取り込まれました。

マージ宣言されるまで、aodagさんをはじめとするパッケージング関連の話をする開発者は、

setuptoolsのことは忘れてあげてください

と話していましたが、これからは、

distributeのことは忘れてあげてください

となるとおっしゃっていました。

setuptoolsはマージ後に頻繁に更新されており、0.7から1.1.6にバージョンが大きく上がっています。setuptoolsを使うと、setup.pyに便利な機能が拡張されます。

setup.py test                    # ユニットテスト実行
setup.py register                # PyPIへの登録
setup.py sdist bdist_egg upload  # ソース配布物とegg形式の配布物をPyPIにアップロード
setup.py upload_docs             # ドキュメンテーションをPyPIにアップロード

その他、pkg_resourcesというモジュールが使え、パッケージのメタデータを読み込むことができます。以下のようにsetup.pyにentry_pointを指定し、コマンドの作成などに活用することができます。ここでは、find_packages()も活用し、自動でパッケージの読み出し(__init__.pyが含まれるディレクトリを検索して一覧化⁠⁠、設定を行っています。

from setuptools import setup, find_packages

setup(name="yourproject",
      packages=find_packages(),
      entry_point={
          "console_scripts": [
              "hello=hello:greeting",
          ],
      })

これからのパッケージングシステム

画像

PythonはPEPという規約に従って仕様変更が行われていきます。パッケージングシステムについてPEPで議論されている部分とそうでない部分(あまり進んでいない部分)があり、上図のようにユーティリティモジュール、パッケージフォーマット、インストーラーについては議論が行われ、収束しつつあるようです。

パッケージに関連するPEPとして以下の8つが挙げられます。

  • PEP 345 -- Metadata 1.2
  • PEP 376 -- Database
  • PEP 386 -- Version
  • PEP 420 -- Namespace Package
  • PEP 426 -- Metadata 2.0
  • PEP 427 -- Wheel
  • PEP 440 -- Version
  • PEP 453 -- Bundle PIP Installer

pipは先ほどの説明の通りで大きな変更はないようです。ここでは以下のWheel、distlib、pydist.jsonに関してまとめます。

パッケージフォーマット Wheel
新しいメタデータを含むzip形式のパッケージフォーマットです。電子署名も可能になっています。pipもサポートを開始しています。
ユーティリティモジュール distlib
pkg_resourcesからdistlibに変更され、今後はdistlibを使ってパッケージのメタデータにアクセスするようになります。pkg_resourcesとほとんど同じことができるようです。
メタデータ pydist.json
egg-infoでかなり適当に取り決められていた仕様がまとめられ、json形式でメタデータが扱いやすくなります。

今までまとまった情報のなかったパッケージ関連のシステムについて、その経緯から学べるセッションはとても有意義でした。最後は目下議論中の最近の話題にも触れ、トレンドを掴むこともでき、とても内容の濃いお話でした。Youtubeとスライドがアップされているので、このあたりをあまり理解できていない方は、ぜひ確認してみてください。

PythonとDataDogを使って簡単なシステムモニターリング

1日目の午後3番目のセッションは、堀田 直孝(@jhotta)さんによるPythonとDataDogを組み合わせて簡単にモニタリングシステムを構築しよう、というお話です。

DevOps活動を積極的に行われている堀田さんから、DevOpsに絡めてシステムモニタリングの重要性と、構築の手間や海外の企業と日本の企業の違いに関して発表がありました。

システムモニタリングの重要性

海外のIT系の企業では当たり前のように、システムモニタリングの結果をいつでも誰でも見れるよう、モニタを壁に貼り付けてあるそうです。

画像

これはEtsyの社内のワンショットだそうです。海外ではスタートアップからある程度大きくなった企業まで、このようにモニタを設置してあると説明がありました。

システムモニタリングのキーワードは、以下の3つです。

  • Collection:データ収集
  • Correlation:相関性の発見
  • Collaboration:協調作業への結びつけ

これらを実行するために、ビジュアライゼーションと先の写真のようなモニタリングが必要だそうです。

堀田さんは、⁠いろいろな会社がNagiosやZabbix、Muminなどを使ってシステムモニタリングを導入していますが、これらを活用できているのだろうか、重要なのはこれらの構築の時間よりも、指標をチェックし、継続的にビジネスを回すことではないか」と話していました。

以下のように、海外にはすでにさまざまなシステムモニタリングサービスがあり、堀田さんはこれらを活用してシステムモニタリングを早く始めるべきと言います。そしてその中でもDataDogが使いやすく、データのビジュアライズの点で優れているとのことです。

画像

日本国内ではGengoが使っているそうです。

堀田さんは、システムモニタリングの重要性の例として、データベースの最適化とクラウドのパフォーマンス監視を挙げていました。可視化していることで、データベースの最適化したタイミングがグラフから明らかにわかります。これにより、ビジネスサイドの人間にいちいち言葉で説明せずとも、その成果が伝わります。

画像

さらに、クラウド上のシステムを監視していると以下のように、ひと目でパフォーマンス劣化がわかります。このようにモニタリングすることで隠れたリスクを顕在化することができます。

画像

DataDogを使う

DataDogを使えば、以下のようなモニタリングダッシュボードを簡単に作成できます。

画像

このダッシュボードにはメッセージ機能も貼り付けることができ、堀田さんはDataDogはFacebookとシステムモニタリングを掛けあわせたサービスだと話していました。

Windowsをはじめ、Debian、Ubuntu、CentOSなど主要なOSに対応しており、数行でインストールすることができます。

$ DD_API_KEY=c81dc7fbacbfca8c9e4f1c02d439f112 bash -c "$(wget -qO- http://dtdg.co/agent-install-ubuntu)"
$ sudo sh -c "echo 'deb http://apt.datadoghq.com/ unstable main' > /etc/apt/sources.list.d/datadog.list"
$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv- keys C7A7DA52
$ sudo apt-get update
$ sudo apt-get install datadog-agent
$ sudo sh -c "sed 's/api_key:.*/api_key: c81dc7fbacbfca8c9e4f1c02d439f112/' /etc/dd-agent/ datadog.conf.example > /etc/dd-agent/datadog.conf"
$ sudo /etc/init.d/datadog-agent start

Pythonから使うライブラリはPyPIに公開されており、パッケージインストーラ経由でインストールできます。

$ easy_install dogstatsd-python

データを送る際には以下の機能を使用できます。

  • Counters
  • Gauges
  • Histograms
  • Sets
  • Tags
  • Sample Rates

これらの機能を活用したコードがいくつか紹介されていました。

ページのアクセスごとにメトリクスをカウントアップする例
def render_page():
    """ Render a web page. """
    statsd.increment('web.page_views')
    return 'Hello World!'
定期的にメモリの空き容量を取得する例
def get_free_memory():
    ...
    return free_memory

while True:
    statsd.gauge('system.mem.free', get_free_memory())
    time.sleep(10)
クエリの実行時間を取得する例:
@statsd.timed('database.query.time')
def get_data():
    return db.query()

コードをアプリケーションに追加し、データを集めた後は、ダッシュボードをDataDogのページで作成します。作成の方法はDataDogのチュートリアルビデオを見るとわかりやすいようです。

Datadog in 2 minutes

あとは、こうやって作ったダッシュボードをオフィスにモニターを用意して、常時表示するだけとのこと。堀田さんはRaspberry Piを活用して、自宅でシステムモニタリングしているそうです。

発表の半分はDataDogの説明でしたが、そこからDataDogが非常に使いやすいサービスである印象を受けました。

NagiosやMumin、その他、アプリのダウンロード数、PV数などを見ることができる管理画面もそうですが、モニタリング用のダッシュボードのカスタマイズが難しい場合が多いです。DataDogはグラフの見た目もよく、その辺の問題が解決するのが良いですね。

各種指標をクローリングし、モニタリング用にまとめる程度であれば、無料の範囲で運用できるとのことなので、弊社でも一度使ってみようと考えています。

about mock

1日目の最後のセッションはBeProudで働く@podhmoさんによるmockライブラリに関するお話です。Pythonで何かを作るより遊ぶことが多いpodhmoさんは、mockライブラリの使い方はもちろんのこと、どのようにこのライブラリが実装されているのか、自分で実装するにはどうすれば良いのかをコードを示しながら発表していました。

mockとは何か

mockはテスト時に都合の良いように、保存先を置き換えるライブラリです。mockではなく正確にはtest doubleというようです。mockは以下の2つの機能を含みます。

Stub
実行時に好きな値を返すようにします。入力値を置き換える機能のことを言います。
Spy
実行時にどのように使われたか確認する。出力値の調査を行う機能のことを言います。

ただ、いろいろと言葉の定義が揺れる場合があるようなので注意が必要です。この発表では上記の定義で話を進めていました。

mockライブラリの使い方

まずはStubの使い方です。ここではtarget.pyのis_holiday()の中でdatetime.now()を呼んでいます。テストが呼ばれたタイミングで、datetime.now()が実行されると、実行結果が都度変わってしまいます。そこでtests.pyでは、is_holiday()自体をmockライブラリを使ってStubに置き換えています。ちなみに、テスト対象を_callFUT()というメソッドに書くのは慣習らしいですね。

target.py::
    from datetime import datetime

    def is_holiday():
        return datetime.now().weekday() == 0

    def visit_shop(name):
        if is_holiday():
            fmt = "{name} is close."
            return fmt.format(name=name)
        else:
            fmt = "{name} is open."
            return fmt.format(name=name)
tests.py::
     import mock
    import unittest

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import visit_shop #2.x
            return visit_shop(*args, **kwargs)

        @mock.patch("target.is_holiday")
        def test_it(self, m):
            m.return_value = True
            result = self._callFUT("Shop")

            expected = "Shop is close."
            self.assertEqual(result, expected)

続いてSpyの使い方です。ここでは、after_submit()を呼んだ時に、enqueue_data()とnotify_message()が適切な引数で呼ばれたことを確認することが目的です。そこで、これらの関数をmockライブラリを使って、Stubに置き換え、Spyの機能(assert_called_once_with)を使って引数をチェックしています。

target.py::
    def enqueue_data(data):
        # using external resource! (e.g. MQ)
        message = "Enqueue Messaging Queue!"
        raise Exception(data, message)

    def notify_message(name):
        # using external resource! (e.g. mail)
        fmt = "{name} submitted!"
        raise Exception(fmt.format(name=name))

    def after_submit(data, name):
        enqueue_data(data)
        notify_message(name)
tests.py::
    import mock
    import unittest

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import after_submit
            return after_submit(*args, **kwargs)

        @mock.patch("target.notify_message")
        @mock.patch("target.enqueue_data")
        def test_it(self, m0, m1):
            data = mock.sentinel.Data
            self._callFUT(data, "Foo")

            m0.assert_called_once_with(data)
            m1.assert_called_once_with("Foo")

このように、mock.patch()を使っていれば、StubもSpyの機能も簡単に使うことができます。

mockを実装する

ここから、podhmoさんの本領発揮です。mockを自前で実装していきます。mockの実装に必要なのは以下の3つの機能です。

  • 模倣(mimic, fake)
  • 置き換え(patch)
  • 記録(capture)

まずは模倣から実装します。以下のMockクラスを自前のMockクラスに置き換えることを目的とします。

import mock

def complex_query(qs, name):
    qs = qs.where(name=name).where(pemission_id=1)
    return qs.where(deleted_at=None).as_list()


m = mock.Mock()
m.where.return_value.where.return_value.where.return_value.as_list.return_value = ["Foo"]

complex_query(m, "Foo") # => ["Foo"]

これで必要なのは.(ドット)アクセスの模倣と関数呼び出しの模倣です。これらを模倣するために、__dict__と__call__を利用します。Pythonの属性は基本的に__dict__属性にdict型で格納されているだけです。同様に、関数呼び出しは__call__メソッドが実装されているか否かだけで判断されます。これらを利用すれば、以下のようにmockを簡単に実装できます。

class MyMock(object):
    def __init__(self, name='*'):
        self.name = name

    def __getattr__(self, k):
        c = self.__class__(name=k)
        setattr(self, k, c)
        return c

    def __call__(self, *args, **kw):
        if hasattr(self, "return_value"):
            return self.return_value
        raise Exception("not callable")

この他にもisinstance()の模倣や置き換え(patch⁠⁠、記録(capture)に関して実際にコードを示しながら、解説がありました。今回の発表で使用したMyMockはgistに挙がっています。かなりシンプルです。

greeting.py
https://gist.github.com/podhmo/6497807

mockライブラリを使ったテストの欠点

画像

テストに関する簡単なマトリクスがスライドに映され、この左下のTOOOOO-BAD!が最悪なテストだとpodhmoさんは説明していました。

テストが成功しているのに、実際にアプリを動かしたときにエラーで落ちるパターンです。これはmockを使ってテストを書いていると良く起こる現象です。特にリファクタリングの後ですね。以下の例が紹介されていました。

ここではリファクタリングによって、Book.ppメソッドをBook.normalizeメソッドに変更したようですが、テストを変更していなかったようです。

target.py::
    class Book(object):
        def __init__(self, content):
            self.content = content

        def normalize(self): #dont provide pp
            return self.content #do_something()

    class Event(object):
        def __init__(self):
            self.events = []

        def publish(self, v):
            self.events.append(("publish", v))

    def publish_pp(event): #expected pp
        book = Book("user.get_book()")
        event.publish(book.pp())
tests.py::
    import unittest
    import mock

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import publish_pp
            return publish_pp(*args, **kwargs)

        @mock.patch("target.Book")
        def test_it(self, m): #expected pp
            m.return_value.pp.return_value = "mock"
            from target import Event
            ev = Event()
            self._callFUT(ev)
            expected = ("publish", "mock")
            self.assertEquals(ev.events[0], expected)
            m.return_value.pp.assert_called_with()

tests.pyではBookクラスをStubで完全に置き換えてしまっています。単にpatch()を使ってStubに置き換えただけでは、このStubは元のクラスがどのような属性やメソッドを持っているのか、知りません。Stubで置き換えた対象がppメソッドを持っている前提でテストが書かれているために、テストが通ってしまいます。

こういう時はspecを使うと属性のチェックも行ってくれます。patchの引数にspec=[対象のクラス]を渡すだけですね。

tests.py::
    import unittest
    from mock import patch

    class Tests(unittest.TestCase):
        def _callFUT(self, *args, **kwargs):
            from target import publish_pp
            return publish_pp(*args, **kwargs)

        def test_it(self): #expected pp
            from target import Book
            with patch("target.Book",spec=Book) as m:
                m.return_value.pp.return_value = "mk"
                from target import Event
                ev = Event()
                self._callFUT(ev)
                expected = ("publish", "mock")
                self.assertEquals(ev.events[0], expected)
                m.return_value.pp.assert_called_with()

この他にautospecの紹介がありましたが、specとautospecを駆使しても、以下の問題は残るようです。

  • プロパティとメソッドの区別が難しい
  • インスタンス変数を関知しない

podhmoさんは、mockライブラリは頑張っているが、これを使ったテストではオブジェクト間のインターフェースの変更に弱い問題はいくつか残ってしまうと話していました。

この発表はmockライブラリについて詳しく説明されており、ユニットテストを書く際の参考になります。技術的なメリットとデメリットも明確に示してあるので、これを参考にテストの書き方の基準を定められるように思いました。また、mockライブラリを自前で実装したりと、Pythonの内部構造に関しても理解できる、一石二鳥なお話だったと思います。

次回は2日目のセッションとパーティやランチを含め、会場の様子をお伝えしようと思います。

おすすめ記事

記事・ニュース一覧