Pythonでクロージャはできないの?

答え:できるけどちょっと変(Python 2.7の場合)。

たとえばJavaScriptの場合には、次のように素直にクロージャを作れる。

js> function make_counter(init){
  >     i = init
  >     function counter(){
  >         print(i)
  >         i += 1
  >     }
  >     return counter
  > }
js> c = make_counter(3)

function counter() {
    print(i);
    i += 1;
}

js> c()
3
js> c()
4
js> c()
5

同じことをPythonでやってみる。

>>> def make_counter(init):
...     i = init
...     def counter():
...         print i
...         i += 1
...     return counter
...
>>> c = make_counter(3)

という風に一見できているように見えるけど、いざc()を実行すると、

>>> c()
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 4, in counter
UnboundLocalError: local variable 'i' referenced before assignment

エラーになる。i を代入前に参照している、と言ってるようだ。

ググってみたらこんな記事を見つけた。
cf. Python とクロージャ – プログラマのネタ帳

この記事によると、ネストした内側の関数定義から外側の変数を参照することはできるけど変更することはできない、と書いてある。実際次のような例が載っている。

>>> def outer(val):
...     def inner(arg):
...         return val + arg
... return inner
...
>>> inner = outer(10)
>>> inner(20)
30

この例は確かに問題なく動く。
さて、じゃあ、さっきのエラーになったコードに戻ってみよう。報告されたエラーはコードの4行目、print i の部分で、i が代入前に参照された、というエラーだった。「内側の関数定義から外側の変数が参照できるが変更はできない」と言うのが本当なら、このエラーはおかしい。4行目では参照しかしてないはずだ。5行目の i += 1 がエラーになるのなら理解できるんだけど。

で、結局どうやったらクロージャができるかと言うと、利用したい変数をリストに入れておけばいいらしい。

>>> def make_counter(init):
...     tmp = [init]
...     def counter():
...         print tmp[0]
...         tmp[0] += 1
...     return counter
...
>>> c = make_counter(3)
>>> c()
3
>>> c()
4
>>> c()
5

これだと tmp 変数には参照しかしないのでエラーにならない、と言うことらしい。確かにちゃんと動いているけど、なんか釈然としない。

逆ポーランド電卓

逆ポーランド電卓ってまだあるんだな。アマゾンでも売ってる。
というわけでちょっと作ってみた。

import sys

class InvalidToken(Exception):
    pass

class RPNCalc:
    def __init__(self):
        self.stack = []

    def calc(self, expr):
        try:
            tokens = expr
            if not tokens[-1] in '+-*/':
                raise InvalidToken
            for token in tokens:
                if token.isdigit():
                    self.stack.append(int(token))
                elif token == '+':
                    a = self.stack.pop()
                    b = self.stack.pop()
                    self.stack.append(b + a)
                elif token == '-':
                    a = self.stack.pop()
                    b = self.stack.pop()
                    self.stack.append(b - a)
                elif token == '*':
                    a = self.stack.pop()
                    b = self.stack.pop()
                    self.stack.append(b * a)
                elif token == '/':
                    a = self.stack.pop()
                    b = self.stack.pop()
                    self.stack.append(b / a)
                else:
                    raise InvalidToken
        except:
            print 'Invalid expression'
            exit()
        return self.stack.pop()

calculator = RPNCalc()
expr = sys.argv[1:]
print calculator.calc(expr)
^o^ > rpncalc.py 6 2 + 5  3 - 2 * /
2

やっつけの手抜きなので、正しく入力しないとエラーにならずに間違った答えを返す。そのうち気が向いたら直すかも。

^o^ > rpncalc.py 1 2 3 +
5

負の整数の割り算はマイナス無限大側に丸められる

AOJのこの問題、evalを使って簡単簡単、だと思いきやジャッジは Wrong Answer だった。原因はタイトルのとおりで、問題では割り算の小数点以下は単に切り捨てることになっている。
Python のマニュアルにはこうある:

(通常および長) 整数の割り算では、結果は整数になります。この場合値は常にマイナス無限大の方向に丸められます

Python て Ruby みたいに演算子の再定義はできないんだろうか。あれば構文解析しなくても eval でいけるんだけど。

結局 Ruby で書いて AC もらった根性なしの俺でした。

バイナリサーチ

AOJの問題を解く途中で作ったのでメモしておく。結局使わなくなったけど。
あらかじめソートされているのが前提。見つからないときは None を返す。

def binary_search(lis, x):
    if len(lis) == 0:
        return None
    else:
        i = len(lis) / 2
        if lis[i] == x:
            return x
        elif x < lis[i]:
            return binary_search(lis[:i], x)
        else:
            return binary_search(lis[i+1:], x)

a = range(100000)
print binary_search(a, 3359)
print binary_search(a, 100001)
print binary_search(a, 0)
print binary_search(a, -1)

実行結果:

^o^ > python binary_search.py
3359
None
0
None

	

素数を無限に列挙するジェネレータ

エラトステネスの篩を応用して書いてみた。

import sys
import itertools

def primes():
    p = []
    p.append(2)
    yield 2
    p.append(3)
    yield 3
    for n in itertools.count(6, 6):
        n1 = n - 1
        for x in p:
            if n1 % x == 0:
                break
            else:
                p.append(n1)
                yield n1
               n2 = n + 1
               for y in p:
                   if n2 % y == 0:
                       break
                   else:
                       p.append(n2)
                       yield n2

n = int(sys.argv[1])
for p in primes():
    if p < n:
        print p
    else:
        break
^o^ > python primes3.py 100
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

ここでは100未満の素数を出力している。さすがに primes3.py 1000000 とかやると遅い。

重複する要素を取り除いだリストと重複する要素だけを取り出したリスト

リストから重複した要素を取り除く

Python には uniq がないらしい。ああ、なんてこった。
というわけで作ってみた。

def uniq(lis):
    r = []
    for i in lis:
        if i not in r:
            r.append(i)
    return r

a = [1, 1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9]

print 'Original: ', a
print 'Uniq: ', uniq(a)

もうちょっときれいにならないかな。reduceでできそうな気がする。

^o^ > python uniq.py
Original:  [1, 1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9]
Uniq:      [1, 2, 3, 4, 5, 6, 7, 8, 9]

リストから重複した要素だけを取り出す

今度は逆に、重複した要素だけを取り出してみる。

def overlup(lis):
    r = []
    def func(x, y):
        if x == y and x not in r:
            r.append(x)
        return y
    reduce(func, lis)
    return r

a = [1, 1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9]

print 'Original: ', a
print 'Overlup: ', overlup(a)
^o^ > python overlup.py
Original:  [1, 1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9]
Overlup:   [1, 4, 7]

ネストしたforループから脱出するには例外を使う

タイトルで言いたいことがすんでしまった^^)

forループを途中で脱出するにはbreakを使うけど、breakではひとつのループしか脱出してくれない。たとえば次の例ではbreakしても外側のforループは回り続けて、else節が実行される。

for i in range(5):
    for j in range(5):
        k = i + j
        print k
        if k > 5:
            break

        else:
            print "End of nested loop."
^o^ > python nested_loop.py
0
1
2
3
4
1
2
3
4
5
2
3
4
5
6
3
4
5
6
4
5
6
End of nested loop.

外側のforループもいっぺんに脱出したいときには、例外を使う。たとえばこんなふうに:

class EndLoop(Exception):
    pass

try:
    for i in range(5):
        for j in range(5):
            k = i + j
            print k
            if k > 5:
                raise EndLoop
            else:
                print "End of nested loop."
except EndLoop:
    print "Break in loop."

実行すると、ちゃんと途中でふたつのforループを抜け出していることがわかる。

^o^ > python nested_loop2.py
0
1
2
3
4
1
2
3
4
5
2
3
4
5
6
Break in loop.

挿入ソート

Pythonでやってる人を見かけたので俺も。

import random

def insertion_sort(lis):
    for i in range(1,len(lis)):
        tmp = lis.pop(i)
        for j in range(i):
            if tmp < lis[j]:
                lis.insert(j, tmp)
                break
            else:
                lis.insert(i, tmp)
                return lis


l = range(1,10) * 2
random.shuffle(l)
print "unsorted:", 
 l2 = insertion_sort(l)
print "sorted: ", l2

実行結果:

^o^ > python insertion_sort.py
unsorted: [1, 9, 6, 3, 8, 6, 5, 2, 7, 4, 2, 9, 8, 5, 7, 3, 4, 1]
sorted:   [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9]

Python の for文には else をつけることができるんだ。初めて使った。

リストのappendメソッドはNoneを返す

リスト(数列)の差分のリストがほしかったんだけど、はまったのでメモ。
最初、↓こう書いた。

def difference(lis):
    d = reduce(lambda x, y: (x[0].append(y-x[1]), y), lis, ([],0))
    d[0].pop(0)
    return d[0]

a = range(10)
diff = difference(a)
print diff

2行目のx[0].append(y-x[1])で差分を蓄積していってそれを返している(3行目は余分な初期値との差分があるからそれを取り除いている)。
ところが実行するこうだ。

^o^ > python difference.py
Traceback (most recent call last):
  File "difference.py", line 9, in 
    diff = difference(a)
  File "difference.py", line 3, in difference
    d = reduce(lambda x, y: (x[0].append(y-x[1]), y), lis, ([],0))
  File "difference.py", line 3, in 
    d = reduce(lambda x, y: (x[0].append(y-x[1]), y), lis, ([],0))
AttributeError: 'NoneType' object has no attribute 'append'

‘NoneType’のオブジェクトは’append’なんて属性持ってないと。しばらく悩んだけど、原因はタイトルのとおり。
1回目のappendを呼び出したところでNoneが返ってくるので、2回目の呼び出しでは失敗する。

>>> a = range(5)
>>> a
[0, 1, 2, 3, 4]
>>> type(a.append(5))
<type 'NoneType'>

結局、次のように書き換えたらうまくいったけど、関数がひとつ増えてしまった。

def difference2(lis):
    d = reduce(lambda x, y: (add_list(x[0], y-x[1]), y), lis, ([],0))
    d[0].pop(0)
    return d[0]

def add_list(lis, x):
    lis.append(x)
    return lis

a = range(10)
diff = difference2(a)
print diff
^o^ > python difference2.py
[1, 1, 1, 1, 1, 1, 1, 1, 1]

RubyのArray#pushならselfを返してくれるから素直に書けるんだけどなあ。

zip関数

いまさらだけどPython にも zip関数があるのを知った。というか見逃していた。

>>> a = range(5)
>>> a
[0, 1, 2, 3, 4]
>>> b = ['a', 'b', 'c', 'd', 'e']
>>> zip(a, b)
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e')]

今まで↓こう書いてたよ。ばかみたい。zipのほうがよっぽどすっきりしている。

>>> map(lambda x, y: (x, y), a, b)
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e')]

とはいえ、違いもある。
zipは長さを短いほうにあわせる。

>>> c = ['foo', 'bar', 'baz']
>>> zip(a, c)
[(0, 'foo'), (1, 'bar'), (2, 'baz')]

mapだと長いほうにあわせて、足りない部分にはNoneが入る。

>>> map(lambda x, y: (x, y), a, c)
[(0, 'foo'), (1, 'bar'), (2, 'baz'), (3, None), (4, None)]