CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: ast (Abstract Syntax Tree: 抽象構文木) モジュールについて

標準ライブラリの ast モジュールを使うと Python で書かれたソースコードを構文解析できる。 それによって得られるオブジェクトは AST (Abstract Syntax Tree: 抽象構文木) と呼ばれる。 その使い道としては、例えば Python の lint ツールなどが考えられる。 つまり、ソースコードの構造を確かめることでまずいところを見つけ出すことができる。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.4
BuildVersion:   15E65
$ python --version
Python 3.5.1

使ってみる

論よりコードということで実際にソースコードを ast モジュールで処理してみよう。

まずは次のようにシンプル極まりないサンプルを用意する。

$ cat << 'EOF' > helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def main():
    print('Hello, World!')


if __name__ == '__main__':
    main()
EOF

ここからは Python の REPL を使って動作確認をする。 先ほどのファイルと同じディレクトリで python コマンドを実行しよう。

$ python

まずは、先ほど用意したソースコードの内容を source という名前の変数に読み込んでおく。

>>> FILENAME = 'helloworld.py'
>>> with open(FILENAME, 'r') as f:
...     source = f.read()
...

読み込んだ内容を ast モジュールの parse() 関数に渡して構文解析しよう。

>>> import ast
>>> ast_object = ast.parse(source, FILENAME)

あるいは代わりに組み込み関数の compile() を使っても構わない。 実際のところ、上記でも内部的には以下を呼び出している。

>>> import ast
>>> ast_object = compile(source, FILENAME, 'exec', ast.PyCF_ONLY_AST)

すると ast モジュールの解析結果として Module オブジェクトが得られる。 これは Python モジュール (ようは *.py ファイル) に対応している。

>>> ast_object
<_ast.Module object at 0x103818e80>

これらのオブジェクトはノードと呼ばれる。

すべてのノードは ast.AST 抽象基底クラスのサブクラスになっている。

>>> isinstance(ast_object, ast.AST)
True

持っているアトリビュートはノードの種類によって異なる。 もし入れ子になった要素があるときは、それらがリストになった body というアトリビュートを持っている。

>>> ast_object.body
[<_ast.FunctionDef object at 0x10561d490>, <_ast.If object at 0x10561d590>]

上記ではモジュールが関数と if ステートメントをひとつずつ持っていることがノードの内容から判断できる。

関数や if ステートメントのノードには、それがソースコードの何行目にあるかという情報が lineno として得られる。

>>> ast_object.body[0].lineno
5
>>> ast_object.body[1].lineno
9

関数については名前の情報も name として得られる。 それに対し if ステートメントは名前がないので name アトリビュートもない。

>>> ast_object.body[0].name
'main'
>>> ast_object.body[1].name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'If' object has no attribute 'name'

ノードがどういったアトリビュートを持っているかは ast.dump() 関数を使うと分かりやすい。

>>> ast.dump(ast_object.body[0])
"FunctionDef(name='main', args=arguments(args=[], vararg=None, kwarg=None, defaults=[]), body=[Print(dest=None, values=[Str(s='Hello, World!')], nl=True)], decorator_list=[])"

全体の構造を出力してみる

先ほどは REPL を使って ast モジュールの基本的な使い方を確認した。 次は試しに AST の全体の構造を出力するプログラムを書いてみる。 先ほどの helloworld.py と同じディレクトリに用意しよう。

$ cat << 'EOF' > tree.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import ast


def walk(node, indent=0):
    # 入れ子構造をインデントで表現する
    print(' ' * indent, end='')

    # クラス名を表示する
    print(node.__class__, end='')

    # 行数の情報があれば表示する
    if hasattr(node, 'lineno'):
        msg = ': {lineno}'.format(lineno=node.lineno)
        print(msg, end='')

    # 改行を入れる
    print()

    # 再帰的に実行する
    for child in ast.iter_child_nodes(node):
        walk(child, indent=indent+4)


def main():
    FILENAME = 'helloworld.py'

    with open(FILENAME, 'r') as f:
        source = f.read()

    tree = ast.parse(source, FILENAME)
    walk(tree)


if __name__ == '__main__':
    main()
EOF

ast.iter_child_nodes() 関数を使ってノードの子要素を取り出して、それを再帰的に呼び出しながら情報を書き出している。 ノードの入れ子構造はインデントで表現した。

実行してみると helloworld.py の構造が表示される。 main 関数を表す FunctionDef の中に Print が入っていて、さらにその中には Str が入っていたり。 見ているだけで Python のソースコードは内部的にこう表現されているのかと面白い。

$ python tree.py
<class '_ast.Module'>
    <class '_ast.FunctionDef'>: 14
        <class '_ast.arguments'>
        <class '_ast.Print'>: 15
            <class '_ast.Str'>: 15
    <class '_ast.If'>: 28
        <class '_ast.Compare'>: 28
            <class '_ast.Name'>: 28
                <class '_ast.Load'>
            <class '_ast.Eq'>
            <class '_ast.Str'>: 28
        <class '_ast.Expr'>: 29
            <class '_ast.Call'>: 29
                <class '_ast.Name'>: 29
                    <class '_ast.Load'>

特定のノードを選択的に処理するには

先ほどの例ではノードを再帰的に処理する関数を自前で用意した。 実は ast にはそれ用のヘルパが用意されているので、そちらを使っても構わない。 具体的には ast.NodeVisitor クラスを継承してコールバックを実装する。 これは、例えば特定のノードだけを選択的に処理したいといったときは特に便利だ。

次のサンプルコードでは FunctionVisitor という関数だけを処理するクラスを定義している。 ast.NodeVisitor クラスを継承した上で、visit_ から始まるメソッドを実装する。 メソッド名の後ろには処理したいノードの名前を入れよう。 今回のように関数であれば FunctionDef になる。

$ cat << 'EOF' > visitor.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function

import ast


class FunctionVisitor(ast.NodeVisitor):

    def visit_FunctionDef(self, node):
        """FunctionDef オブジェクトだけに反応するコールバック"""
        print(node.name, end='')
        print(': ', end='')
        print(node.lineno)


def main():
    FILENAME = 'helloworld.py'

    with open(FILENAME, 'r') as f:
        source = f.read()

    tree = ast.parse(source, FILENAME)
    FunctionVisitor().visit(tree)


if __name__ == '__main__':
    main()
EOF

コールバックの中では関数の名前と、その関数が定義されている行数を出力している。

早速、先ほどのサンプルコードを実行してみる。

$ python visitor.py
main: 14

ばっちり main() 関数と、その場所が出力された。

いじょう。

Python: uptime でシステムの連続稼働時間を得る

システムの uptime (連続稼働時間) に関する情報はプラットフォームごとに取得する方法が異なっている。 今回紹介する Python の uptime パッケージは、それらの差異を吸収して共通の API で連続稼働時間を知ることができる。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.4
BuildVersion:   15E65
$ python --version
Python 3.5.1

インストール

uptime を pip コマンドでインストールする。

$ pip install uptime

使ってみる

使い方はシンプルこの上ないので REPL を使って示す。 python コマンドで起動しよう。

$ python

連続稼働時間は uptime モジュールの uptime() 関数で得られる。 単位は秒。

>>> import uptime
>>> uptime.uptime()
107.211436

ちなみに、もし連続稼働時間が不明なときには None が返るらしい。

複数回呼び出すと、もちろんだんだんと数値 (連続稼働時間) が増えていく。

>>> uptime.uptime()
111.069084
>>> uptime.uptime()
113.490662

また boottime() 関数では起動した時間を得ることができる。

>>> uptime.boottime()
datetime.datetime(2016, 3, 31, 21, 27, 5, 65)

シンプル!

「スマートPythonプログラミング」という本を書きました

表題の通り Python の本を書いたので、そのご紹介です!

どんな本なの?

ひと言で表すと、自信を持って Python を書けるようになるための本です。 スマートPythonプログラミングなので、略してスマパイって呼んでください。

こんな人におすすめ

  • なんとなく書いてなんとなく動いているコードに不安を覚えている方
  • 入門書は読み終わったけど次に何をして良いか分からない方
  • Python の初心者から中級者にステップアップしたい方
  • ベストプラクティスとかアンチパターンって言葉が好きな方

上記に当てはまる方には、本書が少しでも参考になると思います。

どんなことが書いてあるの?

自分が Python を学び始めた頃に知りたかったな〜と思う内容を詰め込みました!

本は 6 つの章に分かれています。 どんなことが書かれているか簡単に紹介します。

  • 「開発環境を整えよう」では Python の開発環境について学びます。

    • pyenv で任意のバージョンの Python をインストールします
    • virtualenv & virtualenvwrapper で独立した実行環境を作ります
    • pip でサードパーティ製のパッケージをインストールします
    • IPython を使ってスニペットの動作確認をしてみます
    • pudb でソースコードをデバッグします
  • 「読みやすいコードを書く」 では Python で読みやすいソースコードを書く方法について学びます。

    • 標準的なコーディング規約 PEP8 と、それをチェックするツールの pep8/autopep8/flake8 を使います
    • PEP8 の命名規則と、名前を使ったアクセスレベルの表現方法について知ります
    • docstring を使ったドキュメンテーションについて知ります
  • 「落とし穴を避ける」では Python でやってしまいがちな誤りについて学びます。

    • Pylint を使ってコードを静的解析して落とし穴を見つけます
    • 新旧スタイルクラスの違いとか非推奨なモジュールがあることなんかを知ります
    • ※ この章にかぎらず「ここに注意!」みたいなのをこの本にはいっぱい書いてます
  • 「Python らしく書く」では Python らしいソースコードを書く方法について学びます。

    • イテレータ
    • ジェネレータ
    • 内包表記 (リスト内包、集合内包、辞書内包、ジェネレータ式)
    • デコレータ
    • コンテキストマネージャ
    • 抽象基底クラス (abc)
    • 特殊メソッド
  • 「テストを書く」では Python のソースコードをテストする方法について学びます。

    • FizzBuzz に標準ライブラリの unittest でテストを書きます
    • テストしにくいところを unittest.mock でモックに入れ替えます
    • coverage でテストのカバレッジ (網羅率) を調べます
    • テストランナー (nose) とデバッガ (pudb) を連携させます
  • 「使いやすいコードを書く」では Python で保守しやすいソースコードを書く方法について学びます。

    • setuptools で自作パッケージをパッケージングする方法を知ります
    • Python 2.x と 3.x の違いと、両方で動作するコードの書き方について知ります
    • 何気に一番ここが濃いかもしれません

この本で使う環境

登場するソースコードは Python 3.5 & 2.7 対応です。 OS には Ubuntu 14.04 LTS を使うことを想定しています。 ※ 付録に仮想マシンを使ったセットアップ方法を載せています

執筆のきっかけ

Python を良い感じに書くための知見が一通り揃っている本が書きたいなーと思ったのがきっかけです。 個人的には「エキスパートPythonプログラミング」をすごくリスペクトしています。 この本は Pythonista になりたいなら絶対に読んでおいた方が良いと思える本です。 ただ、ちょっと難易度が高いのと、発売から日が経って内容が古くなりつつありました。 そんなとき、ちょうど Kindle で電子書籍を出版できることを知ったのもあって書き始めました。

エキスパートPythonプログラミング

エキスパートPythonプログラミング

  • 作者: Tarek Ziade,稲田直哉,渋川よしき,清水川貴之,森本哲也
  • 出版社/メーカー: KADOKAWA/アスキー・メディアワークス
  • 発売日: 2010/05/28
  • メディア: 大型本
  • 購入: 33人 クリック: 791回
  • この商品を含むブログ (90件) を見る

話がちょっと脱線しますけど本書に近い性格を持った本として「Effective Python」の和訳が最近出ました。 こちらは、より純粋に Python の細かな書き方にフォーカスしている感じです。 それに比べると、本書はツールとかを含めた Python の開発全般について書いています。

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

技術的な側面について

この本はすべて Python 製のドキュメンテーションツールの Sphinx を使って書きました。 そこで得られた経験は SphinxCon JP 2015 で発表したりもしてます。

speakerdeck.com

謝辞

Sphinx で書いた本を KDP で販売するという面では @r_rudi さんの「入門Ansible」という先駆者がありました。 これは色々な面で参考にさせて頂きました。

そして、本の表紙を書いてくれた Asano Sonoko さんと本書のレビューをして下さったかわたろ先輩 (@kawatarolab) には感謝してもしきれません。

おわりに

空き時間にチマチマと書き進めていたら 1 年半くらいかかってしまいました。 その分だけ内容は詰められたんじゃないかと思います。 含める内容については何度か見直しを入れて、本当に必要そうなものだけに絞りました。 そういう意味では、何を書くかよりも何を書かないかを重要視した本かもしれません。

スマパイをよろしくお願いします!

追記: ありがとうございます

Python コミュニティで有名な @shimizukawa さんに読んで頂けました! 一点ご指摘頂いた箇所についても、現在は修正した版になっています。 むしろ、それ以外に気になる点が無かったというところにすごく安心しました…。

ありがたいことに id:methane さんのブログで紹介して頂きました! 例として他のプログラミング言語を使ったことのある人が Python のオンラインチュートリアルと本書を組み合わせて学習することをおすすめされています。 これは私の中で他のプログラミング言語に慣れ親しんだ方を読者イメージとして持っていたので、仰る通りだと思います。 そういった方に、業務に耐えるレベルのソースコードを書くための近道を作れたら良いなと考えています。

methane.hatenablog.jp

Python: テストフレームワーク nose のプラグインを書いてみる

nose は Python のテストフレームワークのひとつ。 特徴のひとつとしてプラグイン機構があるためテストランナーの挙動をカスタマイズできる点が挙げられる。

プラグインの書き方は次のページに詳しく書かれている。 Writing Plugins — nose 1.3.7 documentation

今回は上記のページを元に簡単なプラグインをいくつか書いてみることにする。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1

プラグインを書いてみる

プラグインは nose_helloworld という名前のパッケージとして用意する。 まずはプロジェクト用のディレクトリを用意しておこう。

$ mkdir nose_helloworld
$ cd nose_helloworld

ディレクトリの中に移動したら、さらに同じ名前でディレクトリを作る。 ただし、今度のディレクトリは Python のパッケージにするためのものだ。 Python のパッケージは __init__.py というファイルの入ったディレクトリとして表現される。

$ mkdir nose_helloworld

ちなみに Python のモジュールとパッケージまわりの話はこちらの記事に書いたので併せて参照してもらいたい。

blog.amedama.jp

それでは、先ほどの説明通りディレクトリの中に __init__.py を作ろう。 これで nose_helloworld ディレクトリは Python の nose_helloworld パッケージとして認識できるようになる。 今回は、この init.py の中に nose のプラグインを書くことにする。

nose のプラグインは nose.plugins.Plugin のサブクラスとして定義する。 書き方にはいくつかの流儀があるけど、基本的にはスーパークラスにあるメソッドのオーバーライドするだけで良い。 他にはクラスの docstring がコマンドラインのヘルプメッセージになるので書いておいた方が良い。 あとはプラグイン名を name メンバに入れておくのも忘れずに。 ちなみに、オーバーライドしたメソッドの finalize() はテストが完了したときに呼び出されるコールバックだ。 最初のプラグインでは、ここでログを出すことにする。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def finalize(self, result, *args, **kwargs):
        LOG.info('HelloWorld Plugin: finalize')

EOF

プラグインで使えるコールバックは、次の IPluginInterface クラスで確認できる。 この中で <メソッド>._new = True となっているのが使って良いものみたい。 github.com

さて、これでパッケージの中にプラグインが用意できた。 次はパッケージをインストールできるようにする。 なぜなら nose のプラグイン機構は setuptools というパッケージング用の API を使っているためだ。 具体的には pkg_resources というものだけど、これはパッケージをインストールするためのセットアップスクリプトの中に指定する。

次のセットアップスクリプトでは setup() 関数の中の entry_points 引数に 'nose.plugins.0.10' という項目を用意している。 nose のプラグインになるクラスはここで指定することになる。

$ cat << 'EOF' > setup.py
# -*- coding: utf-8 -*-

from setuptools import setup

def main():
    setup(
        name='nose_helloworld',
        version='0.0.1',
        zip_safe=False,
        packages=['nose_helloworld'],
        install_requires=[
            'nose==1.3.7',
        ],
        entry_points = {
            'nose.plugins.0.10': [
                'helloworld = nose_helloworld:HelloWorldPlugin'
            ],
        },
    )


if __name__ == '__main__':
    main()

EOF

パッケージをインストールする

これで nose_helloworld パッケージをインストールする用意ができた。 セットアップスクリプトに install サブコマンドを渡して実行しよう。

$ python setup.py install

これで nose_helloworld パッケージがインストールされた。

$ pip freeze | grep helloworld
nose-helloworld==0.0.1

プラグインを使う

パッケージをインストールすると nose のテストランナーである nosetests コマンドに --with-helloworld というオプションが追加される。 これは先ほどインストールしたパッケージの中に HelloWorldPlugin にある name メンバと docstring から生成されている。 このオプションを有効にすることで作成した HelloWorldPlugin が有効になるという寸法だ。

$ nosetests --help | grep -A 1 helloworld
 --with-helloworld     Enable plugin HelloWorldPlugin: My first plugin for
                       nose [NOSE_WITH_HELLOWORLD]

それでは HelloWorldPlugin を有効にして nosetests を実行してみよう。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: finalize

nose.plugins.helloworld という名前でログが出力されたことがわかる。 ちゃんとテストが完了したときにプラグインのコードが実行された!

ちなみにヘルプメッセージにもあったようにプラグインは環境変数を使って有効にすることもできる。 プラグインを表すクラスの name メンバに 'helloworld' が格納されている場合は NOSE_WITH_HELLOWORLD という環境変数名になる。

$ export NOSE_WITH_HELLOWORLD=1
$ nosetests -vv
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: finalize

プラグインにオプションを追加する

先ほどの例ではこれ以上ないほどシンプルなプラグインを扱った。 次はプラグインに動作を変更するためのオプションを追加してみよう。

先ほどのプラグインを上書きする形で変更していく。 今度は options() というメソッドと configure() というメソッドをプラグインに実装しよう。 options() メソッドは argparse の API を使ってプラグインが有効なときに使えるコマンドラインオプションが追加できる。 同様に configure() メソッドでは、それらのオプションの値を利用してプラグインを設定できる。 次のプラグインでは --message というオプションを追加した。 先ほどは固定だった finalize() メソッドで出力されるログのメッセージを、このオプションで指定されたものに差し替えてみよう。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def options(self, parser, env, *args, **kwargs):
        super(HelloWorldPlugin, self).options(parser, env, *args, **kwargs)
        parser.add_option(
            '--message',
            default='Hello, World!',
            help='Greeting message',
        )

    def configure(self, options, conf, *args, **kwargs):
        super(HelloWorldPlugin, self).configure(options, conf, *args, **kwargs)
        self.message = options.message

    def finalize(self, result, *args, **kwargs):
        LOG.info('HelloWorld Plugin: {msg}'.format(msg=self.message))

EOF

プラグインを上書きしたらパッケージをインストールし直そう。

$ python setup.py install

もう一度 nosetests コマンドをプラグインを有効にして実行してみる。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: Hello, World!

先ほどとは finalize() メソッドで出力されるログのメッセージが変更されていることがわかる。

次にコマンドラインオプションとして --message を追加しよう。 ここに出力してほしいログメッセージを指定する。

$ nosetests -vv --with-helloworld --message 'Hello, Plugin!'
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
nose.plugins.helloworld: INFO: HelloWorld Plugin: Hello, Plugin!

--message オプションで指定したメッセージが出力されていることがわかる。

別のコールバックも実装してみる

次は nose のテストランナーがそれぞれのテストを実行する直前に呼び出されるコールバックをプラグインに実装してみよう。 これには beforeTest() というメソッドをプラグインに実装する。

$ cat << 'EOF' > nose_helloworld/__init__.py
# -*- coding: utf-8 -*-

import logging

from nose.plugins import Plugin

LOG = logging.getLogger('nose.plugins.helloworld')


class HelloWorldPlugin(Plugin):
    """My first plugin"""
    name = 'helloworld'

    def beforeTest(self, test, *args, **kwargs):
        LOG.info('HelloWorld Plugin: {test}'.format(test=test))

EOF

用意できたらパッケージをインストールし直そう。

$ python setup.py install

動作確認のためにシンプルなテストを用意する。 これは、実際には何もテストしていない。

$ cat << 'EOF' > test_helloworld.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from nose.tools import ok_


class Test(object):

    def test(self):
        ok_(True)

EOF

準備ができたら nosetests コマンドを実行する。

$ nosetests -vv --with-helloworld
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']
nose.plugins.helloworld: INFO: HelloWorld Plugin: test_helloworld.Test.test
test_helloworld.Test.test ... ok

----------------------------------------------------------------------
Ran 1 test in 0.002s

OK

今度はテストランナーが見つけたテストの test_helloworld.Test.test が実行される直前にコールバックが実行されていることがわかる。

まとめ

今回は Python のテストフレームワーク nose のプラグインを書く方法について調べてみた。 nose は自作パッケージを用意して、そこにプラグインとなるクラスを定義することで挙動をカスタマイズできる。 この仕組みは nose の中でも使われていて、いくつかの組み込みプラグインが用意されている。 例えばテストのカバレッジを調べるための --with-coverage オプションも、今回調べたプラグインの機構を利用しているようだ。

Python: モジュールとパッケージ

Python のモジュールとパッケージという概念についてまとめておく。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1

モジュールを作ってみる

Python におけるモジュールという概念は、ただの Python ファイル (*.py) にすぎない。 例えば helloworld.py という名前のファイルがあれば、それは Python インタプリタにとって helloworld モジュールになる。

実際に helloworld.py というファイルを用意して確認してみよう。 この中には greet() という関数を定義している。

$ cat << 'EOF' > helloworld.py
# -*- coding: utf-8 -*-


def greet():
    print('Hello, World!')

EOF

この状態では、カレントディレクトリに helloworld.py だけがある。

$ tree
.
└── helloworld.py

0 directories, 1 file

ちょっと脱線するけど tree コマンドは OS X に標準で入っていないのでほしいときは Homebrew で入れよう。

$ brew install tree

helloworld.py がカレントディレクトリにある状態で Python インタプリタを起動しよう。 そして helloworld モジュールをインポートする。 インポートしたモジュールの関数はもちろん使うことができる。

$ python
>>> import helloworld
>>> helloworld.greet()
Hello, World!

ばっちり。 helloworld.py が helloworld モジュールとしてインポートできた。

パッケージを作ってみる

次はパッケージを作ってみよう。 パッケージというのは、複数のモジュールを配下に持つことのできるモジュールをいう。 つまり、ファイルシステムでいえばモジュールがファイルだとしたらパッケージはディレクトリに相当する。

そしてモジュールがファイル (*.py) に対応していたように、パッケージは実際のところディレクトリで表現される。 パッケージというのは __init__.py という名前の Python ファイルの入ったディレクトリに過ぎない。

先ほど作った helloworld モジュールを helloworld パッケージに変えてみよう。 まずは helloworld というディレクトリを作る。 そして、先ほどの helloworld.py を __init__.py という名前に変えてディレクトリの中に移動させよう。

$ mkdir helloworld
$ mv helloworld.py helloworld/__init__.py

ここで __init__.py という Python ファイル (モジュール) は、パッケージ自身を表すモジュールになっている。 つまり、先ほどの greet() 関数は helloworld パッケージの直下に存在することになる。

これでカレントディレクトリの状態は次のようになった。 ちなみにキャッシュファイルはあると分かりにくくなるので削除している。

$ tree
.
└── helloworld
    └── __init__.py

1 directory, 1 file

先ほどと同じように Python インタプリタを起動して helloworld パッケージをインポートしてみよう。 とはいえ、この状態ではモジュールだったときと特に違いはない。

$ python
>>> import helloworld
>>> helloworld.greet()
Hello, World!

パッケージにモジュールを追加してみる

やはりパッケージなので配下にモジュールを追加してみないといけない。 今度は helloworld パッケージの中に spam モジュールを追加してみよう。 spam モジュールの中には ham() 関数を定義している。

$ cat << 'EOF' > helloworld/spam.py
# -*- coding: utf-8 -*-


def ham():
    print('Eggs')

EOF

これでカレントディレクトリの構成は次のようになった。

$ tree
.
└── helloworld
    ├── __init__.py
    └── spam.py

1 directory, 2 files

また Python インタプリタを起動して、今度は helloworld パッケージの spam モジュールをインポートしてみよう。 そして spam モジュールの ham() 関数を実行してみる。

$ python
>>> from helloworld import spam
>>> spam.ham()
Eggs

ばっちり。 helloworld パッケージの配下に spam モジュールが追加できた。

パッケージをインストールできるようにする

先ほど作ったモジュールやパッケージは、そのままでは使いづらい。 なぜならカレントディレクトリにあるか、あるいはどこか決まった場所に置いて PYTHONPATH を手動で通す必要があるためだ。

そんな手間はかけていられないので通常はインストールできるようにする。 これには Python の標準ライブラリにある distutils モジュールや、サードパーティ製ながらデファクトスタンダードになっている setuptools というパッケージを使う。

パッケージをインストールできるようにするにはセットアップスクリプトというファイルを用意する。 これは慣例として setup.py という名前をもった Python モジュールだ。

先ほど作った helloworld パッケージ用のセットアップスクリプトを用意しよう。

$ cat << 'EOF' > setup.py
# -*- coding: utf-8 -*-

from setuptools import setup
from setuptools import find_packages


def main():
    setup(
        name='helloworld',
        version='0.0.1',
        zip_safe=False,
        packages=find_packages(),
    )


if __name__ == '__main__':
    main()

EOF

カレントディレクトリの状態は次の通り。 helloworld パッケージがあるのと同じディレクトリにセットアップスクリプトがある。

$ tree
.
├── helloworld
│   ├── __init__.py
│   └── spam.py
└── setup.py

1 directory, 3 files

自作パッケージをインストールする

それでは、セットアップスクリプトを使って helloworld パッケージを実際にインストールしてみよう。 これにはセットアップスクリプトに install サブコマンドを渡して実行する。

$ python setup.py install

システムの Python 実行環境にインストールするときは適宜 sudo などをつける必要がある。 ただ、個人的には virtualenv などを使って仮想環境を分けるのがおすすめ。 今回もそうしている。

これで pip list サブコマンドから helloworld パッケージが見えるようになった。

$ pip list | grep helloworld
helloworld (0.0.1)

ちなみに pip 自体が入っていないときは次のようにしてインストールしておく必要がある。

$ curl -kL https://bootstrap.pypa.io/get-pip.py | sudo python

さて、インストールされた状態なら今いる場所に関わらずパッケージが使えるようになる。 試しに /tmp とかに移動した上で helloworld パッケージをインポートして使ってみよう。

$ cd /tmp
$ python
>>> import helloworld
>>> helloworld.greet()
Hello, World!

ばっちり。

まとめ

今回は Python のモジュールとパッケージの作り方、そしてパッケージをインストールできるようにする方法について書いた。 Python のモジュールはただの Python ファイル (*.py) に過ぎない。 そしてパッケージは __init__.py という名前のファイルが入っているディレクトリということを知っているのが重要となる。 そして、パッケージをインストールできるようにするには setup.py という名前のセットアップスクリプトを書けば良い。

Python: 古いバージョンを使い続けるということ

例えば標準出力にユニコード文字列を書き込むという単純極まりない処理をやってみる。

Python 2.7 を使うとこんなかんじ。

$ python --version
Python 2.7.10
$ python
>>> import sys
>>> sys.stdout.write(u'うにこーど\n')
うにこーど

何の問題もない。

Python 3.5 ならこう。

$ python --version
Python 3.5.1
$ python
>>> import sys
>>> sys.stdout.write(u'うにこーど\n')
うにこーど
6

write() メソッドが書き込んだ文字数を返すようになってる。

さて、肝心の Python 2.6 を使うとこうなる。

$ python --version
Python 2.6.9
>>> import sys
>>> sys.stdout.write(u'うにこーど\n')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-4: ordinal not in range(128)

何か例外出ましたけど。

原因を調べたら古式ゆかしい Python のバグらしい。 Python 2.6 のサポートは既に終わっているので、この不具合が直ることはない。

stackoverflow.com

一応、解決策がないわけではなくて次のようにすると回避できる。

>>> import sys
>>> import imp
>>> imp.reload(sys)
<module 'sys' (built-in)>
>>> import locale
>>> sys.setdefaultencoding(locale.getpreferredencoding())
>>> sys.stdout.write(u'うにこーど\n')
うにこーど

あるいはファイルライクオブジェクトをラップするやり方もありかな。

>>> import locale
>>> class StdWrapper(object):
...     def __init__(self, stream):
...         self.stream = stream
...     def __getattr__(self, name):
...         return getattr(self.stream, name)
...     def write(self, text):
...         # unicode は 2.x だけにあるシンボルなので注意
...         if isinstance(text, unicode):
...             text = text.encode(locale.getpreferredencoding())
...         return self.stream.write(text)
... 
>>> import sys
>>> wrapper = StdWrapper(sys.stdout)
>>> sys.stdout = wrapper
>>> sys.stdout.write(u'うにこーど\n')
うにこーど

ただ、どちらも LANG=C な環境なんかだと上手くいかないみたいだ。

結論

古いバージョンを使い続けることはリスク。

Python: EAFP スタイルと LBYL スタイル

プログラミングの方針に EAFP スタイルと LBYL スタイルという分類の仕方があることを知った。 これはそれぞれ「Easier to Ask for Forgiveness than Permission」と「Look Before You Leap」の頭文字から取られているらしい。 ざっくり言えば EAFP スタイルは問題が起こってから対処する方針で、反対に LBYL はあらかじめ問題が起こる要因を取り除いておく方針を表しているようだ。

これは別に Python に限った話ではないんだけど、僕は Python の用語集を読んで知った。 そして Python では EAFP スタイルが用いられることが多いらしい。

用語集 — Python 3.5.2 ドキュメント

ちょっとしたサンプルでそれぞれのスタイルの考え方について知ってみよう。 まず、題目には割り算を選んだ。 サンプルコードでは、どちらも mydiv() 関数というふたつの値を取って割り算をする関数を定義している。 ただし、割り算は 0 で割ることができない。 それに対して、それぞれのスタイルではどのように対処するかを考えてみる。

EAFP スタイル

EAFP スタイルでは問題が起こったときにそれを対処する。 すなわち 0 で割り算したときの ZeroDivisionError 例外を拾って、何らかの処理をするということだ。 ここでは 0 で割ったときは結果として None を返すことにしてみよう。

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

from __future__ import division


def mydiv(a, b):
    """割り算をする関数 (EAFP スタイル)"""
    try:
        return a / b
    except ZeroDivisionError:
        # 問題が起こったときに例外などを補足して対処する
        return None


def main():
    print(mydiv(1, 0))


if __name__ == '__main__':
    main()

実行結果は次の通り。

$ python eafp.py 
None

期待通り返り値に None が得られている。

LBYL スタイル

LBYL スタイルでは、あらかじめ問題が起こる要因を取り除いておく。 今回の場合、問題が起きるのは引数 b に 0 が入ったときだ。 なので引数 b に 0 が入ったときを想定して、あらかじめそれに対処するコードを入れておく。

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

from __future__ import division


def mydiv(a, b):
    """割り算をする関数 (LBYL スタイル)"""

    # あらかじめ問題が起こる要因を排除する
    if b == 0:
        return None

    return a / b


def main():
    print(mydiv(1, 0))


if __name__ == '__main__':
    main()

実行結果は次の通り。

$ python lbyl.py 
None

まとめ

今回はプログラミングの方針として EAFP スタイルと LBYL スタイルというふたつのアプローチについて学んだ。 Python では EAFP スタイルが用いられることが多いらしい。 とはいえ、これはどちらのスタイルが優れているとかそういう話ではないだろう。 このプログラミング言語だからどちらかで書かなければいけない、という話でもなくバランスの問題のように感じた。