PyCon APAC参加レポート第2回は、初日の日本語セッションを中心にまとめたいと思います。1日目と2日目のキーノートは第1回の記事にまとめてありますのでお読みください。
パッケージの未来
1日目の一番初めのセッションは、@aodag(小田切 篤)さんによるパッケージの未来に関するお話です。このセッションは、Pythonに10年間お世話になっているというaodagさんが、これまでのPythonのパッケージ管理システムの歴史とPython 3のパッケージ管理システムについて語るというもの。セッションではさらにその先に関する発表もありました。
今までのパッケージングシステム
Pythonのパッケージングシステムの歴史は複雑で、これまでパッケージングの補助ツールとして二系統が存在していました。
またパッケージのインストールには以下のコマンドを使用します。
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日目のセッションとパーティやランチを含め、会場の様子をお伝えしようと思います。