静かなる名辞

pythonとプログラミングのこと



【python】クラスでデコレータ!

はじめに

 デコレータといえば関数で作るものだと思っている人も大勢いると思いますが、クラスでも__call__メソッドを実装すればクラスインスタンスはcallableになり、呼び出しできるので、デコレータたりえます。

import time

class deco:
    def __init__(self, f):
        self.f = f
    def __call__(self, *args, **kwargs):
        t1 = time.time()
        ret = self.f(*args, **kwargs)
        t2 = time.time()
        print("deco:", t2-t1)
        return ret

@deco
def f(message):
    print("f:", message)

f("hoge")
""" =>
f: hoge
deco: 6.508827209472656e-05
"""

 このことには先に書いた記事で気づいたのですが、もう少し追求してみます。

 目次

デコレータの種類ごとに実装してみる

 こちらの記事によると、デコレータは4種類に分類できます。
qiita.com

 以下の2項目の組み合わせによる

  • 引数の有無
  • ラッパー関数を返すか否か

 ただし、引数なしでラッパー関数を返さないデコレータは簡単すぎて意味がない、としているので3種類考えることにします。

 これらすべてを実装できたらすごいですね。ということで、実装していきます。なお、以下で上記記事からの引用コードを一部示します。

引数なしでラッパー関数を返すデコレータ

def args_logger(f):
    def wrapper(*args, **kwargs):
        f(*args, **kwargs)
        print('args: {}, kwargs: {}'.format(args, kwargs))
    return wrapper

(引用)

 これはクラスのインスタンス自身をラッパー関数として取り扱えば簡単です。冒頭のコードもそれです。もうやったので除外。

引数ありでラッパー関数を返さない場合

funcs = []
def appender(*args, **kwargs):
    def decorator(f):
        # args や kwargsの内容によって処理内容を変えたり変えなかったり
        print(args)
        if kwargs.get('option1'):
            print('option1 is True')

        # 元の関数をfuncsに追加
        funcs.append(f)
    return decorator

(引用)

 簡単そうです。

funcs = []
class deco:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, f):
        print(self.cond)  # 条件によって処理を変えたり
        funcs.append(f)

@deco("aaa")
def hoge(message):
    print("hoge:", message)

@deco("bbb")
def fuga(message):
    print("fuga:", message)

for f in funcs:
    f("called")

""" =>
aaa
bbb
hoge: called
fuga: called
"""

 できた。

引数ありでラッパー関数を返す場合

def args_joiner(*dargs, **dkwargs):
    def decorator(f):
        def wrapper(*args, **kwargs):
            newargs = dargs + args  # リストの結合
            newkwargs = {**kwargs, **dkwargs}  # 辞書の結合 (python3.5以上で動く)
            f(*newargs, **newkwargs)
        return wrapper
    return decorator

(引用)

 ちょっとむずかしそうですかね。デコレータ自身の引数は__init__で受け取ることになります。次に__call__が関数を受け取るしかないのですが、更にwrapperがないと関数そのものの引数が取り扱えません。

 まあ、wrapperもメソッドで作れるので、実は難しいと見せかけて楽勝なのですが。

class deco:
    def __init__(self, decomessage):
        self.decome = decomessage

    def __call__(self, f):
        self.f = f
        return self.wrapper

    def wrapper(self, *args, **kwargs):
        print("deco:", self.decome)
        return self.f(*args, **kwargs)

@deco("hoge")
def func(x):
    print("func:", x)
func("foo")
    
@deco("fuga")
def func(x):
    print("func:", x)
func("bar")

@deco("piyo")
def func(x):
    print("func:", x)
func("baz")

""" =>
deco: hoge
func: foo
deco: fuga
func: bar
deco: piyo
func: baz
"""

 ということで、ぜんぶできました。よって、クラスもデコレータになれますね。

考察

 思いついたことをつらつらと考察します。

どうしてこの書き方が一般的ではないの?

 単純に__call__を使うのがキモいからだと思います。。。

利点は?

 幾つか考えられます。

  • 関数定義のネストが深くならない。
  • 通常の関数によるデコレータでは状態をコールスタック上で管理するが(クロージャ)、クラスで書くと明示的にselfの状態として取り扱える。

 以上2点によって、人によっては可読性が高いと感じる可能性があります。

  • 各種拡張が行いやすい

 通常のデコレータはいかんせんクロージャなので、あまり大規模なものの実装は避けると思います。この方法だと何しろクラスなので、その気になればなんでも作れるでしょう。まあ、クラスを他に定義しておいて普通のデコレータでそれのインスタンスを返しても良い訳ですが。

 まったく利点がない訳ではないということです。

欠点は?

  • とりあえずキモい。
  • 更に一般的でもない。
  • そもそも濫用しすぎ。
  • 決定的な利点がある訳ではないので、上記欠点がむしろ目立つ。

 うーん、苦しいかな。

総評

 面白いけど、これを使ったコードを書いて他人に渡すのは嫌がらせに近いかもしれない。
(いやでも、普通のデコレータもなかなか読みづらいけどなぁ)

まとめ

 とにかくできることはわかりました。「ここが駄目!」とか「こんなふうに活用できるね!」などの意見がありましたら、遠慮なくコメントに書いてください。

【python】使いやすい関数の呼び出し回数カウンタを考える

はじめに

 関数の呼び出し回数を数えたい、というシチュエーションはたまにあります。

 その都度、場当たり的にカウンタ変数を増やしたりして対処するのも、まあ、ありといえばありですが、使いやすいものを作るとしたらどうなるかな? というのを興味本位で書いてみました。

 あくまでも興味本位なので実用的な頑強性までは保証しません。

 なお、ある程度は以前書いた記事の

www.haya-programming.com

 を踏まえた内容になっているので、よろしければこちらも御覧ください。

要件

 「使いやすい」がコンセプトなので、最低限以下のような要件を抑えようと思います。

  • 関数内を書き換えない
  • 追加が容易
  • 関数でもクラスのメソッドなどでも同様に扱える
  • カウント回数を取得する、リセットするなどの機能があると嬉しい

 これは独自クラスのインスタンスを返すデコレータですかね。

作ってみる

 とりあえずカウントするためのクラスを作ります。

class Count:
    def __init__(self, func):
        self.count = 0
        self.func = func

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

    def reset(self):
        self.count = 0

 最低限これだけあれば良いや。カウントは直接count属性を見て取得します。「ゲッターを作れ」「いやそこはプロパティで」といった指摘が飛んできそうですが、python的ではない気がするのと面倒くさいので(こちらが本音)、やりません。また、resetメソッドでカウンタを0にできます。

 クラスなので、必要なら幾らでも処理を追加できます(オーバーヘッドの許す限り)。

 さて、次にデコレータにするために、普通はこんなコードを書いてみるのではないでしょうか。

def count(f):
    return Count(f)

 ……要らないんじゃね? と思ってクラスをそのままデコレータにできないか試したら、いけました。なので、クラス名をcountにしてそのままデコレータとして使うことにしました。

動かしてみる

 最終的にこんなコードになり、望み通りの結果が得られました。

class count:
    """
    カウントのためのクラス
    """
    def __init__(self, func):
        self.count = 0
        self.func = func

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

    def reset(self):
        self.count = 0

@count
def f(x, y=None):
    """
    実験用関数
    """
    print("f:", x, y)

# 以下で確認
for i in range(2):
    for j in range(3+i):
        f(i, y=j)
        print("count:", f.count)
    f.reset()

""" 結果 =>
f: 0 0
count: 1
f: 0 1
count: 2
f: 0 2
count: 3
f: 1 0
count: 1
f: 1 1
count: 2
f: 1 2
count: 3
f: 1 3
count: 4
"""

 問題なさげ。できました。

まとめ

 以前の記事と見比べると、カウンタそのものを実装するのと比べて大した手間はかかりません。それでいて汎用的です。高階関数が扱えてデコレータのある言語は良いですね(ってそれpythonだけじゃん)。

 また、クラスが直接デコレータに使えるのは新たな発見でした。これを使っているものはあるのだろうか?(クラスだけだと引数を取れないデコレータになるので、いろいろと厳しいかもしれません) これについてはもう少し追求してみたいです。

【python】ターミナル上でCUIでライフゲーム

概要

 ANSIエスケープシーケンスを使って複数行を書き換えるテストとして書きました。洗練度は低いですがライフゲームが端末上で動きます。

www.mm2d.net

 これを動かしておくことで、なんとなくかっこいい感じがします。

実装

 基本的には以前作ったときと同じ実装です。

www.haya-programming.com

 ただし、使いやすいようにクラスとモジュールに押し込みました。iterableとして実装しています。

main.py

import sys
import time

from lifegame import LifeGame

def field_to_char(A):
    return "\n".join("".join("#" if char else " " for char in line) 
                     for line in A)

def main():
    seed = int(sys.argv[1])
    lifegame = LifeGame(60, 30, seed)
    for i, x in enumerate(lifegame):
        print(field_to_char(x))
        print("\x1b[30A", end="")
        time.sleep(0.1)

        if i > 1000:
            break

    print("\x1b[30B", end="")
    
if __name__ == "__main__":
    main()

lifegame.py

import numpy as np
import numba as nb
import matplotlib.pyplot as plt

@nb.jit(nopython=True)
def get_ijlst(x, limit):
    ret = []
    if 0 < x:
        ret.append(x-1)
    if x < limit-1:
        ret.append(x+1)
    ret.append(x)
    return ret

@nb.jit(nopython=True)
def update_cell(i, j, field, out, field_w, field_h):
    i_lst = get_ijlst(i, field_h)
    j_lst = get_ijlst(j, field_w)

    s = 0
    for ni in i_lst:
        for nj in j_lst:
            s += field[ni, nj]
    s -= field[i,j]

    if s < 2:
        out[i,j] = 0
    elif s == 2:
        out[i,j] = field[i,j]
    elif s == 3:
        out[i,j] = 1
    elif s >= 4:
        out[i,j] = 0
    else:
        raise Exception

def update_field(pair_lst, field_w, field_h):
    for i in range(field_h):
        for j in range(field_w):
            update_cell(i, j, pair_lst[0], pair_lst[1], field_w, field_h)
    pair_lst.append(pair_lst.pop(0))

class LifeGame:
    def __init__(self, field_w, field_h, seed=0):
        self.field_w = field_w
        self.field_h = field_h
        self.random = np.random.RandomState(seed)
        self.initialize()

    def initialize(self):
        field = (self.random.rand(self.field_h, self.field_w) > 0.9
        ).astype(np.int16)
        out = np.zeros(shape=(self.field_h, self.field_w)).astype(np.int16)
        self.pair_lst = [field, out]
        
    def __iter__(self):
        self.initialize()
        return self
    
    def __next__(self):
        self.update_field()
        return self.pair_lst[0]
        
    def update_field(self):
        update_field(self.pair_lst, self.field_w, self.field_h)

def main():
    lifegame = LifeGame(60, 40)
    img = plt.imshow(lifegame.pair_lst[0])
    for x in lifegame:
        img.set_data(x)
        plt.pause(0.001)
        print(x)

if __name__ == "__main__":
    main()

使い方

 上の二つのファイルを同一ディレクトリに置き、

$ python main.py 127

 のように端末から実行します(コマンドライン引数は乱数seedで省略不可)。ターミナルエミュレータの場合、できるだけ大きい画面(全画面など)にしておいてください。また、フィールドの幅と高さはmain.pyを数箇所書き換えれば調整できます。

 なお、ANSIエスケープシーケンスによる制御を受け付けない端末も中にはあるはずなので、注意が必要です。コマンドプロンプトとかはできないはず。emacsのshellも無理でした。

 うまく実行されれば、同じ位置で画面が書き換わりながらライフゲームが描画されるのを楽しめます。

 実行画面の例を以下に貼ります。動画は編集が大変なので、静止画ですが・・・。

実行例
実行例

いけてないところ

  • いかんせん文字の縦横比が1:1ではないので、表示が縦長な感じになる
  • Ctrl-Cなどで途中で止めると画面にゴミが残ることがある(clearコマンドで消してください)

 縦長になるのはともかく、後者はもう少しなんとかしてあげたいところです。

まとめ

 CUIで動かすのは思いの外簡単にできました。その気になればけっこう遊べるものも作れると思います(簡単なゲームくらいはいける?)。

【python】matplotlibのhistで棒の上にラベルを表示

 plt.histはデータを与えるとそのままヒストグラムをプロットしてくれますが、棒との位置関係に基づいてテキストなどを表示させようとすると、ちょっと困ります。

 しかし、plt.histの返り値を利用して棒の頂点の座標を取得すれば、そのままプロットすることができます。

Returns:
n : array or list of arrays
The values of the histogram bins. See normed or density and weights for a description of the possible semantics. If input x is an array, then this is an array of length nbins. If input is a sequence of arrays [data1, data2,..], then this is a list of arrays with the values of the histograms for each of the arrays in the same order.

bins : array
The edges of the bins. Length nbins + 1 (nbins left edges and right edge of last bin). Always a single array even when multiple data sets are passed in.

patches : list or list of lists
Silent list of individual patches used to create the histogram or list of such list if multiple input datasets.

matplotlib.pyplot.hist — Matplotlib 3.0.2 documentation

 nとbinsがほしい情報です。patchesはとりあえず使いません。

 xy座標でいうと、nがy、binsがxです。ただしbinsは棒の両端の座標で、棒の数+1の要素数を持ちます。平均してやれば真ん中になります。

 以下は各棒の上に棒の高さ(データ数)をプロットするサンプルです。

import numpy as np
import matplotlib.pyplot as plt

n, bins, _ = plt.hist(np.random.uniform(size=1000))
xs = (bins[:-1] + bins[1:])/2
ys = n.astype(int)

for x, y in zip(xs, ys):
    plt.text(x, y, str(y), horizontalalignment="center")
plt.savefig("result.png")

result.png
result.png

 このように、座標さえ取得できれば任意のテキストや図形をプロットすることができます。

【python】自分自身を要素に持つlist・循環参照するlistなどを作ってみる

 これまでやったことがなかったので、試してみたというだけです。データ構造によってはこういうのを作りたくなることもあるでしょう。

>>> lst = [0]
>>> lst.append(lst)
>>> lst
[0, [...]]

 なるほど、こういう形で印字されるのか。特に問題はなさそう。

 次に、互いに互いを要素に持つ2つのlist。

>>> lst1 = [0]
>>> lst2 = [1]
>>> lst1.append(lst2)
>>> lst2.append(lst1)
>>> lst1
[0, [1, [...]]]
>>> lst2
[1, [0, [...]]]

 循環構造がある場合、このように表記が省略される。

 辞書の場合。

>>> d = {}
>>> d["key"] = d
>>> d
{'key': {...}}
>>> d1 = {}
>>> d2 = {}
>>> d1["key1"] = d2
>>> d2["key2"] = d1
>>> d1
{'key1': {'key2': {...}}}
>>> d2
{'key2': {'key1': {...}}}

 set……はunhashableなのでなし。また、tupleはimmutableなので、上のようにオブジェクトを生成してから値を挿入することができない。ただし要素にlistを持つことは可能なため、それを利用して循環構造を作り出すことはできる。

>>> tup = ([],)
>>> tup[0].append(tup)
>>> tup
([(...)],)
>>> tup[0]
[([...],)]
>>> tup[0][0]
([(...)],)

 numpyも大丈夫そう。

>>> import numpy as np
>>> a = np.array([1], dtype=object)
>>> a
array([1], dtype=object)
>>> a[0] = a
>>> a
array([array(..., dtype=object)], dtype=object)

 要するに、組み込み型やちゃんとしたライブラリのコレクション型においては、適切に文字列に変換されるので特に心配要りません。ただオリジナルのコレクション型を定義するような場合、__repr__および__str__の実装次第では事故る可能性があるでしょう(ナイーブに子要素に対してstrやreprを呼ぶような実装にすると、無限に再帰し得る)。

multiprocessing.PoolでAttributeError: Can't get attribute '***' on <module '__main__' from '***.py'>みたいなエラー

概要

 multiprocessing.Poolで並列化じゃ! と調子に乗ってコードを書いていると表題のようなエラーに遭遇することがあります。

 再現するコード。

poolerrortest.py

from multiprocessing import Pool

p = Pool(2)

def f(x):
    print(x)

p.map(f, [1,2])
# 中略
AttributeError: Can't get attribute 'f' on <module '__main__' from 'poolerrortest.py'>

原因と対策

 Poolのインスタンスが作られた時点でプロセスがforkしますが、その時点ではfが定義されていません。よって子プロセスの側でfが見つけられない、というエラーになります。

 Poolのインスタンスを作るタイミングを調整することで回避できます。

from multiprocessing import Pool

def f(x):
    print(x)

p = Pool(2)
p.map(f, [1,2])

""" =>
1
2
"""

 子プロセスの実行に必要なものの定義が終わってからPoolのインスタンスを生成する必要があります。

multiprocessing.Poolがやたらメモリを消費するときの対策

概要

 multiprocessing.Poolは原理的にプロセスをforkさせるので、メインプロセスに大きなデータが残っているとそれが丸々コピーされてメモリ領域を食います。

 グローバル関数限定ですが、initializerを使って必要ないデータを消すことができます。また、Poolを作るタイミングを工夫することでそもそも大きいデータが子プロセスに引き継がれないようにすることができます。

前提状況の説明

 以下のようなコードです。

import subprocess
from multiprocessing import Pool

import numpy as np

a = np.arange(10**7)

def f():
    subprocess.run("ps -aux | grep [m]emory_test", shell=True)

p = Pool(1)
p.apply(f)
p.close()
p.terminate()
print(a.shape)

 見るからにメモリをドカ食いしそうな10**7のnumpy配列を確保しています。実行すると、以下のようになります。

username      7407 44.0  1.2 543952 100056 pts/0   Sl+  20:25   0:00 python memory_test.py
username      7411  0.0  1.1 347344 94640 pts/0    S+   20:25   0:00 python memory_test.py
(10000000,)

 もし子プロセスで走らせたいのがaを使わない処理なら、無駄に大容量のメモリを食っていることになります。

対策1:initializerで消す

実験

 以下のようなコードを書いてみます。

import subprocess
from multiprocessing import Pool

import numpy as np

a = np.arange(10**7)

def f():
    subprocess.run("ps -aux | grep [m]emory_test", shell=True)

def initializer():
    del globals()["a"]

p = Pool(1, initializer=initializer)
p.apply(f)
p.close()
p.terminate()
print(a.shape)  # ちゃんといることの確認
username      7427  0.0  1.2 543948 100112 pts/0   Sl+  20:26   0:00 python memory_test.py
username      7431  0.0  0.2 269212 16548 pts/0    S+   20:26   0:00 python memory_test.py
(10000000,)

 だいぶ改善しました。

本末転倒というか・・・

 まあ、見ての通りエレガントな方法ではありません。また、globals()は書き換えられてもlocals()は書き換えられないので、ローカル変数には効きません。

 そこで2番目の対策を考えます。

対策2:早めにPoolを作る

説明

 上のコードでaが作られる以前にPoolを作れば、その時点でforkするのでメモリどか食い現象は回避できます。

 こんな感じですね。

import subprocess
from multiprocessing import Pool

import numpy as np

def f():
    subprocess.run("ps -aux | grep [m]emory_test", shell=True)

p = Pool(1)
a = np.arange(10**7)

p.apply(f)
p.close()
p.terminate()
print(a.shape)
username      7525  0.0  1.2 543952 100116 pts/0   Sl+  20:31   0:00 python memory_test.py
username      7529  0.0  0.2 269216 16512 pts/0    S+   20:31   0:00 python memory_test.py
(10000000,)

 initializerで消すのと同等の効果がありますが、こちらだとローカル変数でも大丈夫です。また、グローバル変数をdelする方法だと、initializerが走るまでの一瞬の間は無駄なデータがメモリを消費する訳で、そういう面でもこちらの方が有利だと思います。

まとめ

 早めに(重いデータがメモリに読み込まれる前に)forkしておくのが基本ですが、どうしても駄目なときは削除も試してみましょう。