2014/09/23

Python Tips:Pythonでクロージャを使いたい

Pythonでクロージャを使う方法をご紹介します。

具体的な説明に入る前に、そもそもクロージャとはなんぞや、というところから行きたいと思います。


クロージャとは

クロージャとは「関数内の変数の名前解決が、その関数が宣言されたときのスコープによって行われるもの」です。

もう少し丁寧にいうなら、クロージャとは、

- 関数内のローカル変数以外の名前解決が、
- 呼び出し時のスコープではなく、
- 宣言時のスコープによって行われるもの。
- またそのような機能を持った関数のこと。

です。

例があるとわかりやすいです。たとえば次の場合。
x = 1
def get_x():
    return x

class MyClass(object):
    x = 10
    print get_x()  # 10 ではなく 1 が返される

MyClass 内の print get_x() は何を出力するでしょうか? 10 ? 1 ?それとも None でしょうか?答えは「 1 」です。

なぜなら、 get_x() の中の x は「呼び出し時のスコープではなく定義時のスコープ」から取得され、また、 Python の名前解決のルールには「LEGB」というものがあるからです。

この仕組みがクロージャです。

ちなみに Wikipedia には次のような説明が載っています。
In programming languages, a closure (also lexical closure or function closure) is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.[1] A closure—unlike a plain function pointer—allows a function to access those non-local variables even when invoked outside its immediate lexical scope.
(意訳)
クロージャ(またはレキシカルクロージャ、関数クロージャ)とは、参照環境を伴ったような関数、あるいはその関数への参照のことを指します。参照環境というのは、特定の関数が持つ、ローカル環境にはない変数(自由変数あるいは upvalue )への参照を保持したテーブルのことです。通常の関数ポインタとは異なり、クロージャによって、関数が別のスコープで呼び出されたときにおいてもそこのローカル変数以外の変数にアクセスすることが可能となります。

わかるようなわからないような・・・概念的な説明はちょっと難しいですね。。でも、実際のコードを見るとその意味は明白です。

クロージャの使い方

では、クロージャの使い方を見てみましょう。

Pythonでは「関数内関数が定義できる」というのと「関数がスコープを分けてくれる」という特徴を利用して、次のようなことができます。
# coding: utf-8

def gen_circle_area_func(pi):
    """円の面積を求める関数を返す"""
    def circle_area(radius):
        # ここの pi は circle_area_func に渡された pi が使われる
        return pi * radius ** 2  
    
    return circle_area

# 円周率を 3.14 として円の面積を計算する関数を作成
circle_area1 = gen_circle_area_func(3.14)
print circle_area1(1)  # => 3.14
print circle_area1(2)  # => 12.56

# 次は、円周率の精度をぐっと上げて
# 円周率 3.141592 で円の面積を計算する関数を作成
circle_area2 = gen_circle_area_func(3.141592)
print circle_area2(1)  # => 3.141592
print circle_area2(2)  # => 12.566370614

circle_area1() circle_area2() はいずれも「円の半径を引数に受け取りその面積を返す関数」です。

ロジックは同じですが、それぞれ参照する pi の値が異なるため、計算の精度が異なっています。

このようにクロージャの特徴を使えば、関数を生成して返す関数をひとつ作っておくだけで「ロジックは同じだけどその内部で使用するパラメータが異なる関数」を動的に作成することができます。この仕組みにより、「変数だけでなく処理(=関数)を部品化し使いまわせる」という特徴(関数型言語的特徴)が言語に加わります。


また別の例を見てみましょう。
def fibonacci_func():
    """フィボナッチ数列を返す関数を返す メモイズ機能つき"""
    table = {}  # 計算済みのフィボナッチ数列を格納するテーブル

    def fibonacci(n):
        # 計算したことのある数値についてはテーブルを参照して返す
        if n in table:
            return table[n]

        # 計算したことのない数値についてはフィボナッチ数列の定義どおり計算
        if n < 2:
            return 1
        table[n] = fibonacci(n - 1) + fibonacci(n - 2)
        return table[n]

    return fibonacci


# 関数を生成してから 50 番までのフィボナッチ数列を計算して表示する
f = fibonacci_func()
for i in range(50):
    print f(i)
こちらはフィボナッチ数列を計算して返す関数です。クロージャのおかげで、グローバル変数を作ることもクラスを作ることもなく、関数だけでメモイズ機能が実現できています。 フィボナッチ数列をメモ化なしで50番目くらいまで求めると結構な時間がかかると思うのですが、このようなメモ化を行うと一瞬で出てきます。 ちなみに、クロージャとなった関数には __closure__ というプロパティがあり、その中身をのぞくと、その関数が持つ「参照」が「セルオブジェクト」として収められていることが確認できます。
print f.__closure__  # => セルオブジェクトのリスト
print f.__closure__[0].cell_contents  # => fibonacci 関数そのものを表すセルオブジェクト
print f.__closure__[1].cell_contents  # => table を表すセルオブジェクト
以上です。 他にもよい例が出てこれば追加していければと思います。 参考 Pythonのクロージャ その1 Closures in Python - Stack Overflow a life of coding: Closures in Python Python Closures Explained | Shut Up and Ship Understanding Python's closures Closure (computer programming) - Wikipedia, the free encyclopedia

2014/09/16

Python の内包表記の使い方まとめ

Python の内包表記についてまとめてみました。


内包表記とは?

内包表記とは、リストや辞書などの iterable オブジェクト( for ループで回せるオブジェクト)のループ処理をかんたん・シンプルに記述できる記法です。

たとえば、 1 から 5 までの数値を 2 乗した値を持つリストを作りたい場合。次のような式を書けばそのリストが得られます。

[x ** 2 for x in [1, 2, 3, 4, 5]]
# => [1, 4, 9, 16, 25]

通常の for ループと同じ for element in collection というブロックを書いて、その前に各要素の値を書いて、 [] で囲みます。この書き方が「内包表記」です。

ちなみに、「内包」は英語で「 comprehension 」というそうです。また、「内包」の逆は「外延」( extension )とのこと。内包と外延は数学の集合論における表現方法のようです。

このあたりの概念について詳しくは Wikipedia などが参考になります。

内包と外延 - Wikipedia


種類

内包表記の種類として次の4つがあります。

  1. リスト内包
  2. ジェネレータ式
  3. セット内包表記
  4. 辞書内包


内包表記といえば、基本は「リスト内包」かと思うのですが、「リスト内包」のほかにも「ジェネレータ式」、「内包表記のセット版や辞書版」というものが存在します。

以下順に見ていきましょう。


リスト内包

リスト内包は [] で定義します。リストを返してくれます。
[x + 2 for x in range(5)]
# => [2, 3, 4, 5, 6]


ジェネレータ式

ジェネレータ式はリスト内包の最初と最後の [] の部分を () に変更したものです。戻り値はリストではなく、要素をひとつずつ生成するイテレータとなります。

(x + 2 for x in range(5))
# => <generator object <genexpr> at 0x106017db0>

PEP 289 -- Generator Expressions | Python.org


セット内包表記

セット内包は {} で定義します。

{x + 2 for x in range(5)}
# => {2, 3, 4, 5, 6}


辞書内法

辞書内法も {} で定義します。辞書を返してくれます。セット内包とのちがいは、各要素の値の部分が key: value という形になっている店です。

li = [("C", 1972), ("Java", 1995), ("JavaScript", 1995)]
{k: v for k, v in li}
# => {'C': 1972, 'Java': 1995, 'JavaScript': 1995}
要素を定義する部分が「k: v」となっている点がセット内包表記とは異なります。

次に、使い方のパターンをいくつか見てみます。


いろいろな使い方

内包表記では基本的なループの他にも多重ループやフィルタ処理なども行うことができます。

内包表記以外の機能をあわせて、いくつかのパターンをあげてみます。

基本のループ:
# 通常の1重ループ  map のようなもの
[x ** 2 for x in range(10)]

基本のループ + フィルタ:
# if 節の条件が成り立つもののみをリストに含める filter のようなもの
[x ** 2 for x in range(10) if x % 2 == 0]

複数要素のループ:
# 複数のリストを同時に回す方法  zip を使う
[x * y for x, y in zip([1,2,3], [11,12,13])]

多重ループ:
# 独立した2つのリストを2重ループとして回す
[x + y for x in range(3) for y in [100, 200, 300]]

ネスト:
# 入れ子になった要素を2重ループとして回す
# 順番は、外側のループを先に書き、内側のループを後に書く
[x for inner_list in [[1, 3], [5], [7, 9]] for x in inner_list]

条件によって値を変える:
# 値の部分に if else 式を使う
[x if x % 3 == 0 else 'fizz' for x in range(10)]

条件によって値を変えて fizzbuzz:
# きれいじゃないけど if else の入れ子も可能
[('fizzbuzz' if x % 15 == 0 else ('fizz' if x % 3 == 0 else ('buzz' if x % 5 == 0 else x))) for x in range(1, 30)]

while ループ:
# ジェネレータ式と itertools.takewhile で while 条件の指定も可能
from itertools import takewhile

takewhile(lambda x: x < 30, (n for n in range(100)))

takewhile を使わずに途中で break:
# while ループと同じことを if else 式で実現
def end_of_loop():
    raise StopIteration

list(x if x < 10 else end_of_loop() for x in range(100))
# 次の書き方でもOK
# list(x if x < 10 else next(iter([])) for x in range(100))


最後の StopIteration 例外を使った break なんかはちょっと飛び道具というかトリック的小技になりますが、こういうこともできる、ということは覚えていてもよいかもしれません。

以上です。


参考
generator - python one-line list comprehension: if-else variants - Stack Overflow
python - break list comprehension - Stack Overflow
List Comprehensions in Python. Python Tutorials.
List Comprehensions — Python公式ドキュメント

2014/09/09

Python Tips:ベクトル演算を扱いたい

Python でベクトル演算を行う方法をご紹介します。

結論からいえば「 numpy 」というライブラリを使う方法が最も一般的かと思います。

以下、 numpy を使った基本的なベクトル演算のやり方を見ていきます。

足し算はこんな感じ。
# ライブラリの読み込み
from numpy import array

# ベクトル演算をサポートした array インスタンスの生成
v1 = array([1, 3])
v2 = array([2, 5])

# ベクトル同士の演算
v3 = v1 + v2
print v3  # => array([3, 8])
# ベクトル同士の足し算

足し算と同様に引き算も可能です。
v4 = v1 - v2  # => array([-1, -2])

かけ算も。
v1 * v2  # => array([2, 15])
# 要素同士の掛け算

* 演算子は内積ではなく「要素同士のかけ算」であることに注意が必要です。他のツールで「 * は内積」に馴染んでいる方は要注意です。

内積を使うには * ではなく dot メソッドを使います。
v1.dot(v2)  # => 17 (1*2 + 3*5 の結果)

割り算もサポートされています。こちらも要素同士。
v2 / v1  # => array([2, 1]) 

スカラー値との演算も可能です。
v1 * 2  # => array([2, 6])
2 * v1  # => array([2, 6])

通常のリストとの演算もできてしまいます。これは個人的には気持ち悪いです。。
v1 * [2, 1]  # => array([2, 6])


以上です。

numpy はベクトルだけでなく行列やその他多くの数学的機能を収めたライブラリなので、もっと詳しく知りたい方は公式ページをご参照ください。


参考
Tentative NumPy Tutorial -


2014/09/02

ライブラリ:doctest

Pythonの「 doctest 」というライブラリをご紹介します。

import doctest

doctest は、名前に doc + test とあるとおり、ドキュメントによってテストを行うためのライブラリです。

具体的には、関数やクラスの内部のいちばん最初にあらわれるコメント、いわゆる docstring の中にテストコードを書いてしまおうというものになります。

具体的に見ていきましょう。

以下、 int 型の数値が偶数かどうかをチェックして返す関数を作っていきながら、 doctest を使ってみることにします。


偶数かどうかをチェックチェックする関数

偶数かどうかをチェックする関数、こんな感じになるでしょうか。
def is_even0(number):
    return number % 2 == 0

ここに doctest でテストケースを追加していきます。


doctest 最初の一歩

is_even1 と名前を変えて、 docstring 内に関数の説明とテストを書いてみます。
def is_even1(number):
    """check if number is even or not
    if even, return true, else false.

    >>> is_even1(2)
    True

    >>> is_even1(5)
    False
    """

    return number % 2 == 0


if __name__ == "__main__":
    import doctest  # ライブラリの読み込み
    doctest.testmod()  # このモジュール内のテストの実行

これが doctest の最小単位になります。

docstring 内の
    >>> is_even1(2)
    True
のようなブロックがひとつのテストケースになります。

書式としてはPythonの対話型モードのように >>> のあとにコードを書いて、期待する結果をその次の行に >>> をつけずに書く形になります。

複数のテストケースを書く場合は、空行をはさんでから書いていきます。

ここでは実際に is_even1(2) と is_even(5) 、ふたつのテストケースが書かれています。

この docstring 内に書かれたテストを走らせるには、上記スクリプトを保存して -v オプションをつけて実行します。
$ python even_module.py -v
Trying:
    is_even1(2)
Expecting:
    True
ok
Trying:
    is_even1(5)
Expecting:
    False
ok
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.is_even1
2 tests in 2 items.
2 passed and 0 failed.

すると、個別のテスト結果とサマリーが返ってきます。

-v オプションをつけなかった場合は、テストに失敗したときにだけ出力が行われ、成功したときには何も表示しない形になります。

テストを実際に走らせているのは、コードの一番下、 if __name__ == ... 以降の部分です。この、 import doctest と doctest.testmod() のペアは doctest を走らせるための基本パターンです。

また、 if __name__ == ... を書かないで、実行時に doctest を import して走らせることも可能です。そうしたい場合には次のように -m オプションで doctest を読み込みます。
$ python -m doctest even_module.py -v

結果は if __name__ == ... と書いた場合と同じです。

つづいて、テストケースを追加してみます。


doctest 二歩目

is_even1 では int 型の値が渡されることを前提としていました。次はそうではなく、他の型が渡される可能性も考慮した関数を作ってみます。

int 以外の型が渡されたときにはエラーをあげるようにしましょう。そのための doctest のテストコードも追加することにします。

def is_even2(number):
    """check if number is even or not
    if even, return true, else false.

    @param
    number : int

    >>> is_even2(2)
    True

    >>> is_even2(5)
    False

    >>> is_even2("%s")
    Traceback (most recent call last):
    ...
    TypeError: not an integer
    """

    if type(number) != int:
        raise TypeError("not an integer")
    return number % 2 == 0


if __name__ == "__main__":
    import doctest  # ライブラリの読み込み
    doctest.testmod()  # このモジュール内のテストの実行

追加されたコードは
    if type(number) != int:
        raise TypeError("not an integer")

で、追加されたテストケースは
    >>> is_even2("%s")
    Traceback (most recent call last):
    ...
    TypeError: not an integer
です。

新たなテストケースでは、何らかの値が返ってくるかどうかではなく、想定した例外があがるかどうかをチェックしています。

特定の例外はあがることを期待する場合は次のように書きます。
    Traceback (most recent call last):
    ...
    TypeError: not an integer

最初の2行は定型文となっています。2行目は「省略」の意味を込めて ... と書く形になっているようです。


以上です。

ほんのちょっとだけですが、 doctest の基本的な使い方を見てみました。


もっと踏み込んで使いたい場合は、もろもろドキュメントや参考情報をご参照ください。


参考
doctest introduction - Python Testing
25.2. doctest — Test interactive Python examples — Python公式ドキュメント
Pythonで簡単な単体テストをはじめよう - doctest - tomoemonの日記
Python でテスト - Qiita [キータ]