静かなる名辞

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


【python】呼び出し回数カウント関数を色々な方法で作る

はじめに

 関数の呼び出し回数を数える、というのは割とよくあるサンプルプログラムです。

 C言語で言うstaticなローカル変数を使うやつです。

#include <stdio.h>

void f(void) {
  static int i = 0;
  i++;
  printf("%d\n", i);
}

int main(void) {
  f();
  f();
  f();
}

/* result
1
2
3
*/

 こんなの書いて面白いの? といった声もあるかと思いますが、pythonだと何通りも書き方があって面白いのです。たった一つの冴えたやり方? うるせーな。

 何通りもある書き方について、それぞれ説明してみようと思います。

 目次

グローバル変数を使う

 ある意味一番面白みに欠けるかもしれない方法。

i = 0
def f():
    global i
    i += 1
    print(i)

f()
f()
f()

""" =>
1
2
3
"""

 シンプルですが、グローバルの名前空間を汚す欠点があります。

クロージャにする方法

 「pythonでクロージャを使う方法」みたいな記事でとてもよく見かける方法。

def make_f():
    i = 0
    def f():
        nonlocal i
        i += 1
        print(i)
    return f
f = make_f()

f()
f()
f()

""" =>
1
2
3
"""

 こうしてみると、グローバル変数で書くのと大差ないことがわかります。名前空間を作るために関数の中に入れているだけですね。

 ただし、iを内側のスコープから書き換えるために、nonlocal宣言というあまり馴染みのないものが登場します。こうしないと、内側のスコープにローカル変数が新しく作られて意味を為さないのです。

lambdaを使う

 「クロージャで書けるならlambdaでも書けるでしょ」と思った人はpythonのlambdaをよく知らない人です。

 まず代入文が書けないので、普通の方法では「ローカル変数」が作れませんし、更新もできません。つまり超ハードモード。

 が、オブジェクトを名前に束縛するだけなら、lispのletみたくlambdaだけを使って行うことができます(ただしlispのsetqはできない、あくまでも一度束縛するだけ)。mutableなコレクション型オブジェクトを適当な名前に束縛しておけば記憶領域を獲得したのと等価であり、あとはそのメソッド呼び出しでプログラムを書くことができます。

 使うmutableなオブジェクトは何でも良いのですが、今回はlistにします。

www.haya-programming.com

 の記事で書いた通り、appendとpopで操作でき、アンダーバー付きのメソッドを呼ばなくて済みます(辞書だと少なくともdict.__setitem__は呼ぶ必要がある)。

 複数の処理を連結するためには、or演算子を使います(Noneを返す関数/メソッドはこの手でつなげることができます)。

 コードはこちら。

f = (lambda l : lambda : l.append(l.pop()+1) or print(l[-1]))([0])

f()
f()
f()

""" =>
1
2
3
"""

 一瞬でも目を逸らすと、なんで動いているのか、書いた本人ですらよくわからなくなるコードです。

 あと、このコードに関してはもう少しエレガントに書けそうな気がしないでもないので、「こんな別解があるよ」と思いついた方はコメントで教えてください。

関数オブジェクトの属性に押し込む

 出典はここ。
日記/2009/03/16/Pythonの関数でstaticな変数を使いたい時 - Glamenv-Septzen.net

 「python static変数」でググると5件目くらいに表示されるページです(2018/10/22現在)。

def f():
    f.i += 1
    print(f.i)
f.i = 0

f()
f()
f()

""" =>
1
2
3
"""

 シンプルといえばシンプルな発想。グローバル変数の属性はグローバル変数みたいなものである(?)。ただ、アドホックな感じがする。

 ただ、代入が関数の外にあるのは危険な感じがします。コードを書き換えるときにミスったら、挙動が変わります。

 なので、ファクトリ関数を書いてみますか。

def make_f():
    def f():
        f.i += 1
        print(f.i)
    f.i = 0
    return f
f = make_f()

f()
f()
f()

""" =>
1
2
3
"""

 これは自然というか、クロージャより良さげな気すらなんとなくします。それでもアドホックだけど。

オブジェクト指向で作る

 「そもそも、こういう状態を保持する必要があるものはオブジェクトとしてクラスから作って……」と思った方は、立派なオブジェクト指向脳を持っています。その通りです。

 素直に書くとこんな感じ。

class F:
    def __init__(self):
        self.x = 0
    def f(self):
        self.x += 1
        print(self.x)

f = F()
f.f()
f.f()
f.f()

""" =>
1
2
3
"""

 f.f()はさすがにウザいので、__call__()を定義してインスタンスを直接呼ぶことにします。と言っても、やることはfの名前を変えるだけですが。

class F:
    def __init__(self):
        self.x = 0
    def __call__(self):
        self.x += 1
        print(self.x)

f = F()
f()
f()
f()

""" =>
1
2
3
"""

 クロージャと比べると直感的ですね。こっちの方が好きです。

 とか書くと関数型言語(と、JavaScript)のファンに襲われたりするのでしょうか。

まとめ

 けっきょく、状態を保持する変数をどこに置くか? というところで差異が生まれる訳です。

方法 状態を保持する変数をどこに置くか 備考
グローバル変数 グローバルの名前空間そのもの 名前空間が汚れる
クロージャ 外側の関数のローカル変数 若干読みづらいかも
lambda 外側の関数の引数に束縛したコレクション型 確実に難読
関数の属性 関数オブジェクトの属性 アドホック
オブジェクト指向 オブジェクトのインスタンス __call__ に戸惑う可能性

 逆に言えば、状態を置く場所に違いがあるだけで、ロジックそのものはどれも基本的に同じです。まあ、当たり前ですが。

 実用的な方法は、クロージャかオブジェクト指向の2つだと思います。どちらを取るかはけっこう悩ましいですね。使い方はだいたい同じ感じなので、記述そのものの優劣で比較することになりますが、そうすると主観が混ざります。

 私個人の主観で言えば、クロージャはトガってる感じで一見してコードの意図がわかりづらく、オブジェクト指向は角が取れた感じでわかりやすいですが微妙に冗長。甲乙つけがたいです。

 ただし、大規模なものを作ろうとすればするほどクロージャでは苦しくなるでしょうし、逆に小規模なものほどクラス構文の冗長さが際立ってくるので、その辺(要するに作りたいものの規模)が判断基準でしょうか。

 とまあ、ちょっと真面目な感じの話を書こうと思ったら行き詰まったので、これくらいで切り上げるとします。この記事で「pythonでも一つのことをやるのに色々な書き方があるんだね~」ということを実感していただけたら嬉しいです。

There should be one-- and preferably only one --obvious way to do it.
――The Zen of Python, by Tim Peters
PEP 20 -- The Zen of Python | Python.org

There should be many ways to do it.
――The Anti-Zen of Python, by Daniel Greenfeld
that · PyPI

おまけ

 こんな記事も書きました。よろしければ御覧ください。

www.haya-programming.com