静かなる名辞

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


【python】numpyで二次元配列を結合して三次元配列にする方法

 複数の二次元配列を結合して三次元配列に変換する方法について。

np.dstack

 そのものずばりのnp.dstackという関数がある。

numpy.dstack — NumPy v1.14 Manual

>>> a = np.array([[1,2,3],[4,5,6]])
>>> b = np.array([[7,8,9],[10,11,12]])
>>> a
array([[1, 2, 3],
       [4, 5, 6]])
>>> b
array([[ 7,  8,  9],
       [10, 11, 12]])
>>> np.dstack([a,b])
array([[[ 1,  7],
        [ 2,  8],
        [ 3,  9]],

       [[ 4, 10],
        [ 5, 11],
        [ 6, 12]]])

 なんか思ってたのと違う。理解するために色々やってみる。

>>> c = np.dstack([a,b])
>>> a.shape
(2, 3)
>>> c.shape
(2, 3, 2)
>>> c[:,0]
array([[ 1,  7],
       [ 4, 10]])
>>> c[:,1]
array([[ 2,  8],
       [ 5, 11]])
>>> c[:,2]
array([[ 3,  9],
       [ 6, 12]])
>>> c[:,0:,0] # 元の配列を取り出す。適当に書いてたらできた
array([[1, 2, 3],
       [4, 5, 6]])
>>> c[:,0:,1]
array([[ 7,  8,  9],
       [10, 11, 12]])

 わかるようなわからないような感じ。

 ドキュメントによると、

This is equivalent to concatenation along the third axis after 2-D arrays of shape (M,N) have been reshaped to (M,N,1) and 1-D arrays of shape (N,) have been reshaped to (1,N,1).

 つまりこれと同じということ。

>>> a_reshaped = a.reshape((2,3,1))
>>> b_reshaped = b.reshape((2,3,1))
>>> a_reshaped
array([[[1],
        [2],
        [3]],

       [[4],
        [5],
        [6]]])
>>> b_reshaped
array([[[ 7],
        [ 8],
        [ 9]],

       [[10],
        [11],
        [12]]])
>>> np.concatenate((a_reshaped, b_reshaped), axis=2)
array([[[ 1,  7],
        [ 2,  8],
        [ 3,  9]],

       [[ 4, 10],
        [ 5, 11],
        [ 6, 12]]])

 理解はできたが、ほしいものとは違うのだった。

単純なやり方

 普通はこっちがほしいと思うの。

>>> np.array([a,b])
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

 わかりやすい。上との対比で更にわかりやすくするため、np.concatenateで書いてみる。

>>> np.concatenate([[a], [b]], axis=0)
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

 つまりこっちは(M, N)の配列を(1, M, N)にしてaxis=0で結合したのと等価である。

追記

 こんな記事も書きました。numpyの配列の結合方法についてまとめています。併せて御覧ください。

www.haya-programming.com

【python】ランダムフォレストのチューニングにOOB誤り率を使う

 一般的な機械学習のアルゴリズムでは、パラメタチューニングにはグリッドサーチ・交差検証を組み合わせて使うのが割と普通だと思います。sklearnにはそれ専用のGridSearchCVというクラスまで用意されています。

 実際問題としては、GridSearchは良いとしても交差検証をやったのでは計算コストをたくさん食います。なので、他の方法で代替できるなら、そうしたいところです。

 そして、RandomForestはOOB誤り率という、いかにも強そうなスコアを計算できます。これの良いところは一回fitしただけで計算でき、交差検証と同じ(ような)結果が得られることです。

 なので、OOB誤り率を使ったパラメタチューニングを試してみたいと思います。

OOB誤り率とはなんぞ?

 OOB誤り率、OOBエラー、OOB error等と呼ばれます。これを理解するためにはランダムフォレストの学習過程を(少なくとも大雑把には)理解する必要があります。

 ランダムフォレストは木を一本作る度に、データをランダムサンプリングします(「ランダム」の所以です)。サンプリング方法はブートストラップサンプリングです。詳細はググって頂くとして、とにかくサンプリングの結果、各決定木に対して「訓練に使われなかったデータ」が存在することになります。逆に言えば、各データに対して「そのデータを使っていない決定木の集合」があります。このことを利用して、「そのデータを使っていない決定木」だけ利用して推定し、汎化性能を見ようというのがOOB誤り率のコンセプトです。

実験

 以下のようなコードを書きました。

# coding: UTF-8
import time
from itertools import product

from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import  train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import precision_recall_fscore_support as prf

def oob_tune(clf, params, X, y):
    key_index = list(params.keys())
    
    result_dict = dict()
    for ps in product(*[params[k] for k in key_index]):
        clf.set_params(**dict(zip(key_index, ps)))
        clf.fit(X, y)
        result_dict[ps] = clf.oob_score_

    return dict(zip(key_index, 
                    sorted(result_dict.items(), 
                           key=lambda x:x[1],
                           reverse=True)[0][0]))
    
def main():
    digits = load_digits()
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target)
    
    params = {"n_estimators":[50, 100, 500],
              "max_features":[4, 8, 12],
              "min_samples_leaf":[1, 2]}

    rfc = RFC(oob_score=False, n_jobs=-1)

    t1 = time.time()
    clf = GridSearchCV(rfc, params, cv=6, n_jobs=-1)
    clf.fit(X_train, y_train)
    t2 = time.time()
    best_params = clf.best_params_
    print("GridSearchCV") 
    print("time:{0:.1f}".format(t2-t1))
    print("best params")
    print(best_params)
    rfc = RFC(**best_params)
    rfc.fit(X_train, y_train)
    y_pred = rfc.predict(X_test)
    score = prf(y_test, y_pred, average="macro")
    print("p:{0:.3f} r:{0:.3f} f:{0:.3f}".format(*score))


    rfc = RFC(oob_score=True, n_jobs=-1)

    t1 = time.time()
    best_params = oob_tune(rfc, params, X_train, y_train)
    t2 = time.time()

    print("oob")
    print("time:{0:.1f}".format(t2-t1))
    print("best params")
    print(best_params)
    rfc = RFC(**best_params)
    rfc.fit(X_train, y_train)
    y_pred = rfc.predict(X_test)
    score = prf(y_test, y_pred, average="macro")
    print("p:{0:.3f} r:{0:.3f} f:{0:.3f}".format(*score))

if __name__ == "__main__":
    main()

 sklearnの機能でありそうな気がしましたが、見つからなかったので*1、OOB誤り率を使ったパラメタチューニングのプログラムは自分で書きました。

 同じパラメタ候補に対してのグリッドサーチで、GridSearchCVと比較しています。

 細かい説明は抜きで(気になる人はプログラムを追ってください。簡単なので)、結果を貼ります。

GridSearchCV
time:41.8
best params
{'n_estimators': 500, 'max_features': 4, 'min_samples_leaf': 1}
p:0.977 r:0.977 f:0.977
oob
time:13.1
best params
{'n_estimators': 500, 'max_features': 8, 'min_samples_leaf': 1}
p:0.979 r:0.979 f:0.979

 やはりOOB誤り率を使った方が速いです。最適パラメータは困ったことに両者で食い違っています。テストデータでのスコアはOOB誤り率で計算した最適パラメータを用いた方がびみょ~に高いですが、これくらいだとほとんど誤差かもしれません(1データか2データ程度の違い)。

まとめ

 交差検証でやるのと同程度の結果が得られる・・・と言い切るには微妙な部分がありますが、とにかくやればできます。

 自分でパラメタチューニングのプログラムを書くと、sklearnのインターフェースから逸脱するデメリットがあるので、実際にやるべきかどうかは微妙。scoringの関数と実質的にcross validateしないcv(ダミー)を渡せば、GridSearchCVを利用してもやれそうではありますが。ちょっと悩ましいところです*2

 とにかくさっさとパラメタチューニングしたいんだ! というときは使えるでしょう。

*1:本当に存在しないか、存在するけど私が見つけられなかったかのどちらか。

*2:それとも、私が見つけられてないだけで、sklearnで直接これができる方法があるのだろうか? もしそうなら、ご存知の方は教えていただきたい

【python】bitのリストを高速にintに変換する

やりたいこと

input:[0,1,0,0]
output:4

 極めて単純明快ですが、やるだけなら簡単なので速度を測ります。

 さらに、pure pythonでやると遅いことが目に見えているのでcythonで高速にしようというネタです。

pure pythonで書いたプログラム

 素晴らしいことに(?)、bitリストのintへの変換は既出ネタです。

arrays - Bits list to integer in Python - Stack Overflow

 書き方はだいたい数パターンなので、ここの回答の一つから引用します。

def mult_and_add(bit_list):
    output = 0
    for bit in bit_list:
        output = output * 2 + bit
    return output

def shifting(bitlist):
     out = 0
     for bit in bitlist:
         out = (out << 1) | bit
     return out

 リンク先は後者の方がじゃっかん遅いと主張しています。本当でしょうか?

 自分でも動かしてみます。

# coding: UTF-8

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def mult_and_add(bit_list):
    output = 0
    for bit in bit_list:
        output = output * 2 + bit
    return output

def shifting(bitlist):
     out = 0
     for bit in bitlist:
         out = (out << 1) | bit
     return out

def bench(lst):
    t1 = time.time()
    mult_and_add(lst)
    t2 = time.time()
    res1 = t2-t1

    t1 = time.time()
    shifting(lst)
    t2 = time.time()
    res2 = t2-t1

    return np.array((res1, res2))

def main():
    result_lst = []
    for i in range(50):
        i = 10**i
        binlst = [int(x) for x in bin(i)[2:]]
        lst = []
        for j in range(100):
            lst.append(bench(binlst))
        result_lst.append(np.mean(lst, axis=0))
    df = pd.DataFrame(result_lst, columns=["muladd", "shift"])
    print(df)
    df.plot()
    plt.savefig("fig1.png")

if __name__ == "__main__":
    main()

ベンチマークの結果
ベンチマークの結果

 横軸は 10^nのnです。本当らしい。なんでだろう。

cythonを使う

 どうせpure pythonは遅いと思ったんです。だからcythonで書きました。

  • cython_bit2int.pyx
# coding: UTF-8
import array

def muladd_cy(lst):
    return _muladd(array.array("Q", lst))

def shift_cy(lst):
    return _shift(array.array("Q", lst))

cdef int _muladd(unsigned long[:] bitlist):
    cdef unsigned long int out = 0
    cdef int i = 0

    for i in range(len(bitlist)):
        out = out * 2 + bitlist[i]
    return out

cdef int _shift(unsigned long[:] bitlist):
    cdef unsigned long int out = 0
    cdef int i = 0

    for i in range(len(bitlist)):
        out = (out << 1) | bitlist[i]
    return out
  • bit2int.py
# coding: UTF-8

import time
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import pyximport; pyximport.install()
import cython_bit2int as cyb2i


def mult_and_add(bit_list):
    output = 0
    for bit in bit_list:
        output = output * 2 + bit
    return output

def shifting(bitlist):
     out = 0
     for bit in bitlist:
         out = (out << 1) | bit
     return out

def bench(lst):
    t1 = time.time()
    mult_and_add(lst)
    t2 = time.time()
    res1 = t2-t1

    t1 = time.time()
    shifting(lst)
    t2 = time.time()
    res2 = t2-t1

    t1 = time.time()
    cyb2i.muladd_cy(lst)
    t2 = time.time()
    res3 = t2-t1

    t1 = time.time()
    cyb2i.shift_cy(lst)
    t2 = time.time()
    res4 = t2-t1

    return np.array((res1, res2, res3, res4))

def main():
    result_lst = []
    for i in range(50):
        i = 10**i
        binlst = [int(x) for x in bin(i)[2:]]
        lst = []
        for j in range(100):
            lst.append(bench(binlst))
        result_lst.append(np.mean(lst, axis=0))
    df = pd.DataFrame(result_lst, columns=["muladd", "shift", 
                                           "muladd_cy", "shift_c"])
    print(df)
    df.plot()
    plt.savefig("fig2.png")

if __name__ == "__main__":
    main()

 array使うのはフェアな比較じゃないって? まあ大目に見て下さい。

ベンチマークの結果2
ベンチマークの結果2

 桁数が少ない領域では速度差が少ない(というかヘタしたら負けてる)ので微妙かも。恐らくlistからarrayへの変換が重いのでしょう。こっちだとshiftの方が速くなるのは理屈通りでした。

【python】順列・組み合わせを計算する方法

 Pythonで、順列(Permutation)と組み合わせ(Combination)がほしくなるときがある。また、順列・組み合わせの数がほしくなることもある。

 順列・組み合わせそのものはitertoolsで、その数はscipyで出せる。計算方法についてまとめておく。

 目次

スポンサーリンク


順列・組み合わせそのものがほしい場合

 つまり"abc"から2個取り出す→[["a","b"], ["b", "c"], ["a", "c"]]という結果を期待している場合。

 これは標準ライブラリのitertoolsでできる。

順列の場合

 itertools.permutationsを使う。

>>> from itertools import permutations
>>> list(permutations("abc", 2))
[('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')]

組み合わせの場合

 itertools.combinationsを使う。

>>> from itertools import combinations
>>> list(combinations("abc", 2))
[('a', 'b'), ('a', 'c'), ('b', 'c')]

 極めて容易。

順列・組み合わせの数だけほしい場合

 順列・組み合わせの数がほしいときもある。つまりnPrとnCrである。数字がほしいだけで、結果そのものは要らないケース。

 困ったことに、直接これを計算してくれる関数はmathモジュールなどには用意されていないようだ。実現する方法は何通りかある。

itertoolsの関数の結果をlen()する

 安直。パフォーマンス上はよくないだろうし、無駄っぽい。が、一応できる。

>>> from itertools import permutations
>>> from itertools import combinations
>>> len(list(permutations("abc", 2)))
6
>>> len(list(combinations("abc",2)))
3

 まあ、他の方法を使った方が良さげ。

自分で実装する

 数学的定義通り書くことができる。

>>> def P(n, r):
...     return math.factorial(n)//math.factorial(n-r)
... 
>>> def C(n, r):
...     return P(n, r)//math.factorial(r)
... 
>>> P(3, 2)
6
>>> C(3, 2)
3

 もうちょっと工夫して、無駄な階乗の計算を端折ることも考えられる。ただ、あまりごちゃごちゃ書くのも大変だし、実はscipyにあるので車輪の再発明をする必要すらない。

 nPrとかnCrを自分で実装している人は、素直にライブラリを使いましょう。

ライブラリを使う(おすすめ)

 scipyにある。どちらもscipy.specialから使える。

scipy.special.perm — SciPy v1.3.0 Reference Guide

scipy.special.comb — SciPy v1.3.0 Reference Guide


 注意点としては、exact=Trueを指定しないとfloatで近似値が返されること。この機能は近似値の方が便利/それで十分という用途では活用すれば良いと思う(ちなみにデフォルトはFalse。なので正確な値がほしいときは一々指定してやる必要がある)。

 あと、使い方はいまいちよくわからないけどndarrayも渡せるし、組み合わせの方はrepetitionオプションで重複ありの組み合わせも計算できる。

>>> from scipy.special import perm, comb
>>> perm(3, 2, exact=True)
6
>>> comb(3, 2, exact=True)
3

 あっさりしたものですね。

まとめ

 順列・組み合わせそのものがほしいときはitertools、順列・組み合わせの数がほしいときはscipyを使おう。
(余談だが、scipyは色々あって便利なのに、ドキュメントの検索のしづらさとgoogleの検索順位の低さで損してる気がする。)

【python】sklearnのtolってなんだ?

 sklearnの公式ドキュメントをよく読む方なら、色々なモジュールに"tol"というオプションがあることに気づいていると思います。

 たとえばSVCだと、こんな風に書いてあります。他のモジュールも似たり寄ったりですが。

tol : float, optional (default=1e-3)

Tolerance for stopping criterion.

 出典:
sklearn.svm.SVC — scikit-learn 0.20.1 documentation

 よくわからないし、なんとなく重要そうじゃないし、デフォルトから変える必要もないでしょ、ということで無視されがちなパラメタですが、冷静に考えたら何なのかまったくわからない。実は重要だったりすると大変ですね。ということで、調べました。

 まずtoleranceという単語とcriterionという単語に対応する日本語がぱっと出てきません(そこからかよ)。仕方がないのでググると、「耐性」や「公差」「許容誤差」のような意味が出てきます。なんとなく雰囲気は伝わりました。

toleranceの意味・使い方 - 英和辞典 Weblio辞書

 ちなみに後者のcriterionは基準という意味でした。これは雰囲気でもなんでもなく、そのまんま基準です。

 よって、「Tolerance for stopping criterion.」を訳すと「打ち切るための許容誤差の基準」となり、なんとなくわかるようでわかりません。

 仕方ないので色々キーワードを変えて検索していると、CrossValidatedの質問を見つけました。

machine learning - What exactly is tol (tolerance) used as stopping criteria in sklearn models? - Cross Validated

tol will change depending on the objective function being minimized and the algorithm they use to find the minimum, and thus will depend on the model you are fitting. There is no universal tolerance to scikit .
超訳:目的関数とそれを最小化するアルゴリズムに依存するから、モデルによってちげーよ。sklearnには普遍的なtolなどない

 あっ、そうですか・・・。

 これで終わってしまうのも寂しいので、SVMでtolを変えて結果が変わるか実験してみます。

# coding: UTF-8

import time

from sklearn.datasets import load_digits
from sklearn.svm import SVC
from sklearn.model_selection import cross_validate, StratifiedKFold as SKF

def main():
    digits = load_digits()
    
    svm1 = SVC(C=5, gamma=0.001, tol=1)
    svm2 = SVC(C=5, gamma=0.001, tol=0.5)
    svm3 = SVC(C=5, gamma=0.001, tol=0.001)
    svm4 = SVC(C=5, gamma=0.001, tol=0.00001)

    skf = SKF(random_state=0)

    scoring = {"p": "precision_macro",
               "r": "recall_macro",
               "f":"f1_macro"}

    skf = SKF(n_splits=5, shuffle=True, random_state=0)

    for svm, tol in zip([svm1, svm2, svm3, svm4], [1, 0.5, 0.001, 0.00001]):
        t1 = time.time()
        scores = cross_validate(svm, digits.data, digits.target,
                                cv=skf, scoring=scoring)
        t2 = time.time()
        print("tol:{0:5} time:{1:8.3f} p:{2:.3f} r:{3:.3f} f:{4:.3f}".format(
            tol,t2-t1,
            scores["test_p"].mean(),
            scores["test_r"].mean(),
            scores["test_f"].mean()))

if __name__ == "__main__":
    main()

 結果は、

tol:    1 time:   0.661 p:0.990 r:0.989 f:0.989
tol:  0.5 time:   0.833 p:0.991 r:0.990 f:0.991
tol:0.001 time:   1.193 p:0.992 r:0.992 f:0.992
tol:1e-05 time:   1.226 p:0.992 r:0.992 f:0.992

 性能への影響はごく僅かですが、トータルの処理時間は若干(数10%程度)削れるようです。少しでも軽くしたい、というときは検討する(性能への悪影響が抑えられる範囲でtolを上げる)価値はありますね。

【python】行を上書きしてprintする方法

 出力の行を上書きしたいときがある。

 キャリッジリターン(\r)を使うと簡単にできるが、ちょっと難しい点もある。

方法

 簡単な例を以下のサンプルコードに示す。

import time

def main():
    for i in range(20):
        print("\r{0}".format(i), end="")
        time.sleep(0.2)
    print("")

if __name__ == "__main__":
    main()

 これを動かすと、0から19の数字が同じ行に上書きされて出力される。素晴らしい。

スポンサーリンク




 プログレスバー(のようなもの)なんかも簡単に書ける。

import time

def main():
    for i in range(10):
        print("\r[{0}] {1}/{2}".format(
            "="*(i+1)+"-"*(10-i-1) , i+1, 10), end="")
        time.sleep(0.2)
    print("")

if __name__ == "__main__":
    main()

 必要になったら積極的に使っていきたい。

駄目な方法

 実は、これはちょっと間違えると簡単にできなくなる。私はネットに転がっているサンプルコードを真似しては「できない! 駄目じゃん!」となるのを10回くらい繰り返してきたが、単に正しい方法でやっていなかっただけだった。

 結論を言うと、改行すると駄目。そしてprintはデフォルトで改行する。

import time

def main():
    for i in range(20):
        print("\r{0}".format(i))
        time.sleep(0.2)
    print("")

if __name__ == "__main__":
    main()

 これはうまく動かない。必ずend=""を指定する必要がある。これは改行を抑止するオプション(正しく説明するとendのデフォルトが改行文字で、それを空文字列に変更している)。

 上手く動かないで改行されて出てくるときは、プログラム中で改行される要素がないかどうかをよく確認してみる必要がある。

複数行を上書きしたいときは?

 ANSIエスケープシーケンスを使うとある程度の制御ができます。

ANSIエスケープコード - コンソール制御 - 碧色工房
Python - Pythonで標準出力を上書きで複数行表示させたい|teratail


 より複雑な制御を行いたい場合は、cursesなどを使ってください。

Python で Curses プログラミング — Python 3.7.4 ドキュメント

まとめ

 行を上書きできると面白いものだなぁ。

【python】pythonでscanf的なことをする

 一年以上前にこんな記事を書きました。

 これはこれで今読み返すと面白い(香ばしい)記事ですが、真剣にpythonでscanfと同じことをしたくてアクセスしてきた人には役に立たないという問題点がありました。

 そこで、pythonでscanfと同じことをする方法について真面目な記事を書いておきます。

 目次

 なお、本記事では次のコマンドで実行できる対話的インタプリタの出力を掲載します。返り値の文字列をそのまま出していますが、標準出力から出力したいときはprintを使ってください。

$ python


スポンサーリンク


何はともあれ入力を受け取る

 python2ではraw_input、python3ではinputという関数があり、標準入力からのキー入力を受け取ることができます。

 python2の場合

>>> s = raw_input()
hoge
>>> s
'hoge'

 python3の場合

>>> s = input()
hoge
>>> s
'hoge'

 この受け取った文字列を加工することで、なんとか所要を達するのが基本的なアプローチ方法になります。

文字列操作

 pythonには文字列の操作方法がいろいろあり、また文字列操作関数(メソッド)もいろいろあります。多すぎて網羅しきれないので、よく使うものだけまとめておきます。

>>> s = "hoge fuga" # 文字列の定義
>>> s[0] # 0文字目を取り出す
'h'
>>> s[-1] # 一番最後の文字を取り出す
'a'
>>> s[:4] # 0から3文字目を取り出す
'hoge'
>>> s[2:7] # 0から6文字目を取り出す
'ge fu'
>>> s.split(" ") # 半角スペースでsplitする
['hoge', 'fuga']
>>> s.replace("g", "G") # 文字列を置換する
'hoGe fuGa'
>>> s.isnumeric() # 数字の文字列かどうかを判定する
False

 なお、pythonはC言語と違って、「char型」というものはありません。1文字でもすべて「1文字の文字列」という扱いになります。

 このような文字列操作を型変換と組み合わせて使うことで、かなり色々なことができます。

>>> s = "1 1.5 1,5" # 文字列の定義
>>> splitted_s = s.split(" ") # とりあえずsplit
>>> splitted_s # splitした結果を表示
['1', '1.5', '1,5']
>>> d1 = int(splitted_s[0]) # splitした結果の0番目をint型にする 
>>> d1
1
>>> f1 = float(splitted_s[1]) # splitした結果の1番目をfloat型にする
>>> f1
1.5
>>> f2 = float(splitted_s[2].replace(",", ".")) # splitした結果の2番目をfloat型にする
>>> f2
1.5

 こういった処理をループなどと組み合わせて頑張れば、大抵のことは実現可能です。でも実際にやってみると頑張るのはなかなか辛いので、scanfのようなものがほしくなるのかもしれません。

正規表現を使う

 たとえば、こんな入力をした場合どうでしょう?

>>> s = "1, 1.5, 2, 2.5:hoge"

 この場合、一回のsplitで綺麗にsplitすることはできません。何回かに分けて繰り返しsplitするというのも一つの考え方ですが、こういう場合は正規表現を使うと簡単です。正規表現は標準ライブラリのreモジュールで使えます。

>>> s = "1, 1.5, 2, 2.5:hoge"
>>> import re
>>> re.split(r",\s+|:", s)
['1', '1.5', '2', '2.5', 'hoge']

 一発で綺麗にsplitできましたね。re.splitは区切り文字列の正規表現パターンと文字列を渡すと、それを探してそこで切ってくれます。

 ",\s+|:"というパターンの意味を簡単に説明すると、「カンマの後にスペースが続く OR コロン1つ」を区切り文字として使うという意味です。見た目よりは簡単な内容を表しているのがおわかり頂けるでしょう。それでいて、けっこう色々な表現ができます。

 正規表現はプログラミングをやるなら覚えておいて損はしないので、まだ使ったことがない人も使えそうな機会がある度に積極的に使って、少しずつでも覚えていくことをおすすめします。

parseライブラリを使う

 正規表現で複雑なsplitなどが簡単にできると言っても、けっきょく型変換は自分でやるしかないのが実情でした。これではscanfには及びません。

 そこで、parseというライブラリを使うことができます。これは外部ライブラリなので、pipで入れる必要があります。

$ pip install parse

 ※具体的なインストール方法は環境によって異なります。自分の環境でライブラリを入れる方法を確認した上で、正しい方法でインストールしてください。

 さっそく使ってみましょう。次のように使うことができます。

>>> parse.parse("{:d} hoge {:f}", "1024 hoge 11.19")[0]
>>> import parse
>>> s = input()
hoge 1024.20.48, 56
>>> parse.parse("hoge {:d}.{:f}, {:d}", s)
<Result (1024, 20.48, 56) {}>

 {:d}や{:f}がC言語でいうところのフォーマット文字列であり、scanfというかsscanfのように使うことができます。当然結果は型変換済みです。

 resultというオブジェクトが返っていますが、このオブジェクトはリストのようにインデックスでアクセスすることが可能です。

>>> result = parse.parse("hoge {:d}.{:f}, {:d}", s)
>>> result[0]
1024
>>> result[1]
20.48
>>> result[2]
56

 あるいは、結果を辞書のような形で格納することもできます。次のようにフォーマットを指定します。

>>> result = parse.parse("hoge {var1:d}.{var2:f}, {var3:d}", s)
>>> result["var1"]
1024
>>> result["var2"]
20.48
>>> result["var3"]
56

 なお、これらのリスト(厳密にはtuple)と辞書には、result.fixed, result.namedでアクセス可能です。

 parseライブラリはとても色々なことができます。より詳しく知りたい方は、parseライブラリの公式ページを読んでみてください。

GitHub - r1chardj0n3s/parse: Parse strings using a specification based on the Python format() syntax.

まとめ

 pythonにはそのままのscanfはありませんが、色々な手段で同じ目的を達することができます。最初は覚えるのが大変ですが、慣れてくると色々なことが簡単にできるのに気づいてくるはずです。Let's enjoy Python!

printf編

 兄弟記事です。よろしければこちらも御覧ください。

www.haya-programming.com

【python】pythonでprintf的なことをする

 一年以上前にこんな記事を書きました。

 これはこれで今読み返すと面白い(香ばしい)記事ですが、真剣にpythonでprintfと同じことをしたくてアクセスしてきた人には役に立たないという問題点がありました。

 そこで、pythonでprintf的なことをする方法をまとめておきます。

 目次

 なお、本記事では次のコマンドで実行できる対話的インタプリタの出力を掲載します。返り値の文字列をそのまま出していますが、標準出力から出力したいときはprintを使ってください。

$ python


スポンサーリンク


概要

 pythonでprintfのようなことをする方法は、3つあります。

  • %演算子を使った記法
  • str.format()メソッド
  • f文字列

 なんで3つもあるんでしょうねぇ・・・。困ったものです。

 とりあえず1つずつ説明していきます。

%演算子を使った記法

 一番昔からあり、最新のpythonでも使える方法です。なのでpythonのバージョンに依存せず使えるというメリットがあります。デメリットとしては、ちょっとわかりづらくトラブルの原因にもなること、柔軟性に劣ることが挙げられます。

 これは次のように使います。

>>> "hoge %s" % "fuga"
'hoge fuga'

 いかにもprintf的です。色々な例を示してみます。

>>> "hoge %s %s" % ("fuga", "piyo") # 複数渡すことができる
'hoge fuga piyo'
>>> "hoge %(fuga)s %(piyo)s" % {"fuga":"piyo", "piyo":"fuga"} # 辞書を使うと名前を指定して渡せる
'hoge piyo fuga'
>>> "hoge %d %03d" % (10, 20) # intもばっちり
'hoge 10 020'
>>> "hoge %.3f" % float(1/3) # floatも同様
'hoge 0.333'

 この%はあくまでも文字列オブジェクトに対する演算子操作であることに注意してください。

 こうして見るとよさそうですが、複雑なことをしようとすると色々なトラブルが起こるらしいので、近年は次に説明するformatメソッドなどを使うことが公式ドキュメントなどでも推奨されています。

 もっと詳しく知りたい方は、公式ドキュメントを参照してください。

4. 組み込み型 — Python 3.6.9 ドキュメント

str.format()メソッド

 これはpythonの文字列オブジェクトに標準で実装されているメソッドです。python2.6以上で使えます。

 使い方は簡単です。

>>> "hoge {}".format("fuga")
'hoge fuga'

 最初はprintfや上の%演算子とはフォーマットが異なるのに戸惑うでしょう。formatメソッドでは"{}"でくくられた範囲をフォーマットとして解釈します。

 formatメソッドでも上と同様のことができます。

>>> "hoge {} {}".format("fuga", "piyo") # 複数渡す
'hoge fuga piyo'
>>> "hoge {fuga} {piyo}".format(fuga="piyo", piyo="fuga") # 名前を指定
'hoge piyo fuga'
>>> "hoge {0} {0} {1}".format("fuga", "piyo") # こういうこともできる。indexを指定すると便利
'hoge fuga fuga piyo'
>>> "hoge {0:10.3f}".format(1.4) # 文字列幅と精度を指定している
'hoge      1.400'

 こちらの方が柔軟性が高いのと、メソッドなので通常のオブジェクト指向の延長上で捉えることができ、%演算子ほど場当たり的な側面がありません。なので、この方法を積極的に覚えていくことをおすすめします。

 なお、公式ドキュメントはこちらです。
組み込み型 — Python 3.7.4 ドキュメント

f文字列

 f文字列は一番新しい方法です。python3.6から導入されました。

 %演算子にしろstr.format()メソッドにしろ、ちょっと記述が冗長という欠点がありました。それを補ったのがf文字列です。これは文字列の中で直接式の値を評価し、それを文字列に変換して出力することができます。

 これを使うためには、文字列リテラルの先頭にfを付けます。

>>> f"{1+1}"
'2'

 簡潔な感じです。こういう使い方もできます。

>>> a = "hoge"
>>> f"{a}"
'hoge'

 関数だって呼び出せます。

>>> def pow2(x):
...     return x*x
... 
>>> b = 10
>>> f"{pow2(b)}"
'100'

 表示フォーマットも指定できます。formatのときと同じような書き方です。

>>> f"{1/3:.3f}"
'0.333'

 なかなか凄いですね。でも、個人的にはちょっとトリッキーすぎて使いづらい気が・・・。

 公式ドキュメントはここです。

2. 字句解析 — Python 3.6.9 ドキュメント

2019/08/22追記
 この記事を書いてから月日が過ぎ、近年は少し事情が変わってきたのかなと思うようになりました。

 なんだかんだで簡潔に書けるのがf文字列の良いところです。また、最近のPython環境は3.6以降が標準になってきたので(何かと美味しい機能が3.6以降で増えたので)、f文字列の互換性の問題もあまり気にならなくなってきました。

 今後はf文字列でいいかなという感じもします。

まとめ

 printf的なことをする3種類の方法を紹介しました。

 どの方法が良いのか? については、

  • 歴史的使命を終えつつある%演算子を使った記法
  • 現役バリバリなのでおすすめのsrt.format()メソッド
  • まだよくわからないf文字列

 という感じで、まずはsrt.format()メソッドを使いこなせるようになることを推奨します。もちろん他の方法を使ってはいけないということではないので、少しずつ覚えていくのがおすすめです。

2019/08/22追記
 新し目のPythonしか触らないという前提であれば、f文字列から覚えてもいいと思います。記述量が減らせて楽です。

scanf編

 兄弟記事です。よろしければこちらも御覧ください。

www.haya-programming.com

【python】距離・非類似度行列を計算する

記事概要

 非類似度行列(距離行列)の計算方法について説明する。

計算方法

対象データと使う非類似度

 とりあえず、データを5つ作る。irisの先頭5要素を抽出する。

from sklearn.datasets import load_iris
iris = load_iris()
data = iris.data[:5]

 5*5の非類似度行列を作りたいというシチュエーションである。

スポンサーリンク



リストとループを使った方法

 scipy.spatial.distanceを使うと距離(非類似度)の計算は簡単にできる。

scipy.spatial.distance.pdist — SciPy v1.2.1 Reference Guide

 euclideanとcosineを使ってみることにする。

 愚直にループを回して行列にしたのが以下のプログラム。

import numpy as np
from scipy.spatial.distance import euclidean, cosine 
from sklearn.datasets import load_iris

iris = load_iris()
data = iris.data[:5]
result = []
for dist in [euclidean, cosine]:
    tmp1 = []
    for v1 in data:
        tmp2 = []
        for v2 in data:
            tmp2.append(dist(v1, v2))
        tmp1.append(tmp2)
    a = np.array(tmp1)
    print(a)
    result.append(a)

 実行結果。

[[0.         0.53851648 0.50990195 0.64807407 0.14142136]
 [0.53851648 0.         0.3        0.33166248 0.60827625]
 [0.50990195 0.3        0.         0.24494897 0.50990195]
 [0.64807407 0.33166248 0.24494897 0.         0.64807407]
 [0.14142136 0.60827625 0.50990195 0.64807407 0.        ]]
[[0.00000000e+00 1.42083650e-03 1.26527175e-05 8.99393151e-04 2.42323318e-04]
 [1.42083650e-03 0.00000000e+00 1.20854727e-03 1.20606955e-03 2.75920410e-03]
 [1.26527175e-05 1.20854727e-03 0.00000000e+00 7.83016618e-04 3.31860741e-04]
 [8.99393151e-04 1.20606955e-03 7.83016618e-04 0.00000000e+00 1.28295812e-03]
 [2.42323318e-04 2.75920410e-03 3.31860741e-04 1.28295812e-03 0.00000000e+00]]

 基本的にはこれでできるということ。問題点としては、

  • 対角成分とその上下は同じなのに二回計算するのは無駄
  • オーバーヘッドが気になるかもしれない
  • 冗長

 ということが主に挙げられるだろう。自分でループの条件を工夫したり速い書き方になるよう努力して対処しても良いが、それすらscipyを使うと簡単にできる。

scipyの関数を使った方法

 行列にするところまでscipyで処理することもできる。

 やり方は恐らく一つではないが、今回はscipy.spatial.distance.pdistとscipy.spatial.distance.squareformが目についたので使ってみることにする。

 pdistは対角成分で分けた行列の半分を計算する。これは一次元配列で結果を返すので、squareformで二次元配列に変換する(squareformは適当にコピーして形を変えるだけで、それほど本質的な処理はしていない)。

from scipy.spatial.distance import pdist, squareform
from sklearn.datasets import load_iris

iris = load_iris()
data = iris.data[:5]
for dist in ["euclidean", "cosine"]:
    print(squareform(pdist(data, metric=dist)))

 結果は上と同じなので省略。短く書けるのが素晴らしい。

 2019/02/21追記:pdistとsquareformについては以下の記事も御覧ください。

www.haya-programming.com

まとめ

 できた。

おまけ(非類似度・距離と類似度の関係について)

 たまに頭がこんがらがるのでまとめておくと、次のような関係になる。あまり厳密ではないので注意。

  • 類似度とは、sim(a, a) = 1となるmetric
  • 距離とはdist(a, a) = 0となるmetric
  • 非類似度とはdissim(a, a) = 0となるmetric

 同じもの同士が0になるか1になるかという観点からだけ見れば、距離と非類似度は同じものである。ただし、非類似度は普通は0~1の値を取るものに対して言うのではないだろうか。

 また、0~1の値を取る場合、類似度に変換するにはよく見る次の式が使える。

 sim(a,b) = 1 - dissim(a, b)

【python】sklearnライクなデータセットを作る

 自作したりネットから拾ってきたデータセットに、sklearnライクなインターフェースがあるとそこそこ便利です。

 なので、作る方法について調べました。

 とりあえずデータセットを読み込んで型を調べます。

>>> from sklearn.datasets import load_iris
>>> iris = load_iris()
>>> type(iris)
<class 'sklearn.utils.Bunch'>

 Bunchという型らしいです。

 ではこのBunchを継承したクラスを定義してデータセットのオブジェクトを作るのかというとそうではなく、実はこのBunchというのは「キーにattribとしてアクセスできる辞書のようなもの」であり、コンストラクタに渡したキーと値のセットを内部で作ります。わかりづらいと思いますので例を示すと、

>>> from sklearn.datasets.base import Bunch
>>> hoge = Bunch(hogehoge="hoge!")
>>> hoge.hogehoge
'hoge!'

 こうやって使えるということです。sklearnのソースを読んでも、このような形で使っているので間違いないようです。

https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/datasets/base.py

 なので、自作のsklearnライクなデータセットを作りたいということであれば、このBunchをimportしてあげた上で、

Bunch(data=自作したnumpy配列のデータなど, target=自作したnumpy配列のターゲットなど)

 こんな形で書けば良いということです。あとはload_***関数を作ってこのBunchのインスタンスをreturnしてやれば完了です。

 ちなみに、Bunchのattribはその気になれば外部から上書きすることも可能。実際のdatasetsの実装もそうなっているようです。些か危ない気もしますが、pythonによくある「紳士協定で使え(間違った使い方なんかしないよね)」という奴でしょう。しかし、in-placeで処理される関数をうっかり呼び出したら普通に事故る気が・・・。

>>> from sklearn.datasets import load_iris
>>> import numpy as np
>>> iris = load_iris()
>>> iris.data[:10]
array([[5.1, 3.5, 1.4, 0.2],
       [4.9, 3. , 1.4, 0.2],
       [4.7, 3.2, 1.3, 0.2],
       [4.6, 3.1, 1.5, 0.2],
       [5. , 3.6, 1.4, 0.2],
       [5.4, 3.9, 1.7, 0.4],
       [4.6, 3.4, 1.4, 0.3],
       [5. , 3.4, 1.5, 0.2],
       [4.4, 2.9, 1.4, 0.2],
       [4.9, 3.1, 1.5, 0.1]])
>>> np.random.shuffle(iris.data)
>>> iris.data[:10]
array([[7.7, 2.8, 6.7, 2. ],
       [5.2, 3.5, 1.5, 0.2],
       [6.5, 3. , 5.8, 2.2],
       [5.1, 3.8, 1.5, 0.3],
       [6.2, 2.8, 4.8, 1.8],
       [4.4, 2.9, 1.4, 0.2],
       [4.9, 3. , 1.4, 0.2],
       [4.3, 3. , 1.1, 0.1],
       [6.4, 3.1, 5.5, 1.8],
       [6.8, 3.2, 5.9, 2.3]])

 やはり事故るのだった。ちょっと怖いですねぇ。read-onlyにするのはそこまで難しくはないと思うのですが、敢えてそうしていないのか、単に実装がヘボいのか

追記

 公式リファレンス見たら、「Bunchオブジェクトは俺たちが便利に使うために用意しているだけだ、自分のデータセットで同じものを作ろうとはするな、Xとyを定義すりゃいいんだ」と言ってました。

Frequently Asked Questions — scikit-learn 0.22.2 documentation

 自作データセットと組み込みのデータセットとでAPIを統一したいシチュエーションでは、ちょっと困りますね。でもまあ、やるなと言っているので、特殊な事情があるとき以外は自重しましょう。

【python】SOMのライブラリSomocluはかなりおすすめ

 SOM(Self-organizing maps:自己組織化写像)は割と古めの、データの可視化手法です(それ以外にも使えると思いますが)。

 今回はpythonのSOMライブラリSomocluを使ってみたら、けっこう良かったというネタです。

 目次


スポンサーリンク


SOMの概要

 昨今は深層学習が流行りですが、SOM、自己組織化写像は敢えて言えば単層学習とでも言うべきでしょうか。平面上だったり立体状(まあ理屈の上では何次元でも定義できる)に並べたニューロンにデータをマッピングします。それ以上の説明はwikipediaとか、ググれば色々出てくるページを読んでください。

  • wikipedia

自己組織化写像 - Wikipedia

  • 九州工業大学大学院の先生が書いた読みやすかったページ

http://www.brain.kyutech.ac.jp/~furukawa/data/SOMtext.pdf

  • わかりやすい解説

子供でもわかる「自己組織化マップ」

ライブラリがない

 SOM、けっこう面白い性質があるみたいなのて使ってみたいのですが、ググってみるとpythonで使えそうなライブラリがとにかくあまり出てきません。

  • SOMPY

 申し訳ないけど、ちょっと使いづらかった。というかインストールしても挙動が変な感じだった。
GitHub - sevamoo/SOMPY: A Python Library for Self Organizing Map (SOM)

  • sompy

 日本人の方が実装されたようです。率直に言って「作ってみた」レベルで、実用にはどうかという感じ
自己組織化マップ(SOM)のPythonライブラリsompyを公開しました - 俺とプログラミング

  • PyMVPA

 多変量解析のためのそれなりに大きいライブラリで、SOMも実装されている。これが使えればよかったのだと思うが、python2系のサポートしかないので没・・・。
Self-organizing Maps — PyMVPA 2.6.1.dev1 documentation

 他にも色々あったのですが、割愛。古い手法なので、敢えて作ろうという人がいないのかな・・・。

 というか、SOMでググると「実装してみた」系の記事はたくさん出てくるのに、まともに使えるライブラリは出てこないというの、かなり異常というか残念というか・・・。

それでも頑張ってググった

 Somocluというのを見つけました。

Introduction — Somoclu 1.7.5 documentation

 ウリの部分を適当に訳したり訳さなかったりしつつ抜粋

  • OpenMPとCUDAがサポートされていてGPUでも計算できる
  • 当然マルチプラットフォームでLinux, macOS, and Windowsでサポートされている
  • 「Planar and toroid maps」平面とドーナツみたいな形のSOM両方が作れる
  • 「Rectangular and hexagonal grids」四角と六角形がいける
  • 「Gaussian or bubble neighborhood functions」近傍の計算を効率化する系のがある
  • 「Visualization of maps, including those that were trained outside of Python.」
  • マップの初期化にはPCAが使える

 すごく良さそう。あと、pythonに依存しないツールでコマンドラインから直接コマンドで叩けます。pythonバインディングもあるよ、という位置づけ。真剣に開発されてる感じです。

使ってみた

 とりあえず使ってみました。SOMの可視化結果でよく見るU-matrixという奴を出します。以下のコードで動きました。

# coding: UTF-8
import numpy as np

from somoclu import Somoclu
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA

def main():
    # データを読み込む
    dataset = load_iris()
    X = dataset.data
    y = dataset.target
   
    # SOMに入れる前にPCAして計算コスト削減を測る(iris程度では無駄) 
    pca = PCA(n_components=0.95) 
    X = pca.fit_transform(X)

    # SOMの定義
    n_rows = 16
    n_cols = 24
    som = Somoclu(n_rows=n_rows, n_columns=n_cols,
                  initialization="pca", verbose=2)

    # 学習
    som.train(data=X, epochs=1000)

    # U-matrixをファイル出力
    som.view_umatrix(labels=y, bestmatches=True,
                     filename="umatrix.png")

if __name__ == "__main__":
    main()

 説明不要な感じ。コードも直感的だし、特に不満がないです。

 こんな画像が出てきます。

U-matrix
U-matrix

 この画像の見方は色の濃淡が重要で、色の明るい部分は相対的に縮尺が縮んでおり、逆に暗い部分は縮尺が相対的に大きい訳です。PCAで可視化した結果を参考に貼っておきます。

PCAによるirisの可視化結果
PCAによるirisの可視化結果

 紫がラベル0に、緑と黄色が1と2に対応している訳です。SOMを使うと、このようにデータの構造を捉えることができます。

 使いやすいし動作もまともだし、Somocluは素晴らしいライブラリです。SOMが必要になったら積極的に使っていきたいところ。

今どきSOMなんか使うの?(蛇足パート)

 t-SNEみたいなよくできた手法があるのに今更SOM? と思う方もおられるかと思いますが、SOMはSOMでメリットがあると感じています。

 というのは、t-SNEはけっきょくパラメタに依存するし、ミクロな構造を捉えるのは得意でもマクロな構造はどこまで正しいのか? という問題があるからです。

 例として、digitsを可視化してみます。

# coding: UTF-8
import numpy as np

from sklearn.datasets import load_digits
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from somoclu import Somoclu
import matplotlib.pyplot as plt

def main():
    print("loading data")
    digits = load_digits()
    pca = PCA(n_components=0.95)
    pca_data = pca.fit_transform(digits.data)

    # tsneで可視化
    print("tsne")
    tsne = TSNE()
    X = tsne.fit_transform(pca_data)
    fig, ax = plt.subplots()
    plt.scatter(X[:,0], X[:,1], c=digits.target/10)
    
    i = 0
    for xy, l in zip(X, digits.target):
        if i%8 == 0: # 描画されるtextが多いと汚いので省く
            ax.annotate(l, xy=xy)
        i += 1
    plt.savefig("tsne_digits.png")

    # somで可視化
    print("som")
    # データを適当に省く
    sample_index = np.random.choice(X.shape[0], 400, replace=False)
    sample_X = pca_data[sample_index]
    sample_y = digits.target[sample_index]

    # som
    som = Somoclu(n_rows=30, n_columns=40,
                  initialization="pca")
    som.train(data=sample_X, epochs=1000)
    som.view_umatrix(labels=sample_y, bestmatches=True,
                     filename="som_digits.png")

if __name__ == "__main__":
    main()

t-SNEで可視化したdigits
t-SNEで可視化したdigits

SOMで可視化したdigits
SOMで可視化したdigits

 一見するとt-SNEは同じラベルごとにまとまっていて綺麗なんですが、形の似ている数字が近くに来るのはむしろSOMの方という気もします。0の周りに5,6,9が来るというのは(数字の形を考えると)妥当そうですね。主観的になってしまいますが、SOMも捨てたものではないという気がします。

まとめ

 SOMとSomocluは良いのでみんな使おう。

【python】クラス変数のスコープには注意が必要

 pythonでクラスを書くとき、一番多用するのは(恐らく)インスタンス変数ですが、クラス変数もたまに使います。

 しかし、pythonのクラス変数にはちょっと「クセ」があります。リスト内包表記と組み合わせたときに問題になることが多いようです。

問題の例

 リスト内包表記と組み合わせ、他のクラス変数を使いまわしてクラス変数を定義しようとした例です。

class Hoge:
    x = 5
    y = [x for i in range(10)]

 なんと以下のエラーを吐きます。

NameError: name 'x' is not defined

 こんな人畜無害そうなコードなのに、どうしてなのでしょう。

原因

 クラス変数のスコープの取り扱いが原因です。以下のstackoverflowのページによくまとまった説明があります。

python - Accessing class variables from a list comprehension in the class definition - Stack Overflow

Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions.

The class’s suite is then executed in a new execution frame (see section Naming and binding), using a newly created local namespace and the original global namespace. (Usually, the suite contains only function definitions.) When the class’s suite finishes execution, its execution frame is discarded but its local namespace is saved. [4] A class object is then created using the inheritance list for the base classes and the saved local namespace for the attribute dictionary.

 要約すると、クラス変数の中にスコープがネストされた場合、クラス変数の定義されたスコープは読まないよ、ということを言っています。リスト内包表記はスコープを作るので、この問題が起こります。

 どうしてこんな仕様になっているんでしょう? 上のリンク先でも貼られているPEP 227のページに解説があります。

PEP 227 -- Statically Nested Scopes | Python.org

Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions. This rule prevents odd interactions between class attributes and local variable access. If a name binding operation occurs in a class definition, it creates an attribute on the resulting class object. To access this variable in a method, or in a function nested within a method, an attribute reference must be used, either via self or via the class name.

 「それができるとメソッド定義で事故るだろーが、親切なpython様が対策してやってるんだよ」と書いてあります。

class Hoge:
    x = 5
    def fuga(self):
        x = 10
        print(x)

 色々な事故を招きそうなコードです。こういうことはできない方が妥当で、クラス変数にはHoge.xとしてアクセスするべきです。だけど、この措置がまったく無関係のリスト内包表記にまで影響してしまっています。

 じゃあ、こうすれば動くのか?

class Hoge:
    x = 5
    y = [Hoge.x for i in range(10)]

 動きません。

NameError: name 'Hoge' is not defined

 エラー内容が変わります。Hogeの定義が終わっていない以上、Hoge.xにも当然アクセスできない、ということです。困ったもんですね。

解決策

 こういう場合、lambda式による関数呼び出しなどを使って「ネストされたスコープ」にクラス変数を届けてやれ、とリンク先で解説されています。

class Hoge:
    x = 5
    y = (lambda x:[x for i in range(10)])(x)

 スコープのネストさえなければ他のクラス変数は使えるし、いったん関数スコープを作ってやれば、あとは普通の名前解決のルールで処理されるので大丈夫な訳です。

 これは面倒くさいし、慣れていない人にはラムダ式を直接呼び出しているこのコードは可読性が低いだろうし*1、コードレビューなどで「何やってんのこれ?」とか突っ込まれるくらいならまだ良い方で、これで書いたコードを勝手に他の誰かが直しちゃって大惨事、という事態も想定されます。なので、複数人で管理するコードでは、

  • コメントで何をやっているのかしっかり明記する。このページのURLを貼っておくのもおすすめです(爆)
  • そもそも使わないで回避策で書く

 このような対策が必要になるかと思います。

 回避策というのは、要するにリスト内包表記を使わなければ良いので、たとえば今回例として出したコードならこう書けます。

class Hoge:
    x = 5
    y = [x]*10

 記述もすっきりするし、どうしてもリスト内包表記を使わないと書けないような処理(そんな処理はほぼない。最悪普通のfor文で書ける)以外では、積極的にリスト内包表記を使う理由はありませんね・・・。

まとめ

  • クラス変数のスコープにはクセがあり、リスト内包表記と組み合わせて使うと事故る
  • 正面から対策するのは厄介
  • 逃げちゃえ

*1:私はpythonワンライナーで遊んできた経験があるので、この手のコードにも慣れてしまってなんとも思いませんが

emacsでpythonを書いているとき「arithmetic error」

 tabキーでインデントしようとすると表題の通りのエラーが出て、インデントできない状況になった。

 ググって出てきたのはここ。

Indentation not working properly in emacs for python - Stack Overflow

Check the value of python-indent-offset. If it is 0, change it M-x set-variable RET python-indent-offset RET 4 RET.

 この対処法で確かに直った。emacsがインデントの設定を推測しようとして、勝手に変になったということらしい。よくわからない。けど、解決すれば良いや。

 同じことを繰り返されると面倒なので、init.elにも以下の記述を追加しておく。どうせ空白スペース4つ以外のインデントなんか使わないし。

(add-hook 'python-mode-hook
(lambda () (setq python-indent-offset 4)))

 根治できたかどうかはわからないが、とにかく解決はした。

掛け算および割り算するとそれぞれある値になる数字の組を求める

 プログラミングにはあまり関係のないテーマだし、中学校レベルの数学がわかればできるネタだが……ちょっと欲しくなったので。

問題

  a, bの2つの数(とりあえず正の実数)を考える。 a, bは次の条件を満たす。

  •  a\cdot b = C_1
  •  \frac{a}{b} = C_2

  C_1, C_2が与えられたとき、 a, bを適当に定めたい。

 つまり、ディスプレイの画素数と縦横比が決まっているとき、x画素*y画素のxyがそれぞれどうなるかを求めたい、という形の問題。こんなことに数分悩んでしまった。

解く

 両方の式を掛けあわせる。
 a^2 = C_1\cdot C_2
 a = \sqrt{C_1\cdot C_2}
 b = \frac{C_1}{a} = \frac{a}{C_2}
 簡単に解けた。

試してみる

 pythonで書いてみる。 C_1=100, C_2=\frac{3}{4}とする。

>>> from math import sqrt
>>> def f(c1, c2):
...     a = sqrt(c1*c2)
...     b = a/c2
...     return a,b
... 
>>> f(100, 3/4)
(8.660254037844387, 11.547005383792516)
>>> a, b = f(100, 3/4)
>>> a*b
100.00000000000001

 上手く行っている。

まとめ

 ただの簡単な連立方程式なので解けば良いだけだった。

【python】三項演算子のネストには注意

 三項演算子(条件演算子)はpythonの文法でもっとも特徴的な要素かもしれません。

Trueのとき if 条件 else Falseのとき

 これには賛否両論がありますが、とにかく便利なことには違いありません。

 ただし、ネストして使うときは注意が必要です。

A if condition1 else B if condition2 else C

 こういうものを書いた場合、二通りに解釈できますね。

((A if condition1 else B) if condition2 else C)
(A if condition1 else (B if condition2 else C))

 実行してみるとわかりますが、下の解釈になります。

>>> "A" if True else "B" if False else "C"
'A'
>>> "A" if False else "B" if False else "C"
'C'

 if condition1 elseの三項演算子を先に認識し、 condition2の方はそのelse項(と言うしかない)とみなしている訳です。まあ、これは自然な仕様でしょう。

 ではかっこを付けたときの評価の順番を考えてみましょう。条件演算子では、まず条件が評価されます。それからTrueの場合か、Falseのときのどちらかが評価されます。

 なので、かっこで囲った方の優先順位が下がる(後に評価される)ような挙動になります。「かっこでくくると優先順位が上がる(先に評価される)」と捉えていると混乱します。思い通り動かすためにはかっこで囲う必要がある場合がほとんどなのに、それすら思い通り動かないというひどい状態に陥ります。

 コツは構文木とか決定木を書くつもりでグルーピングしていくことです。そうすると一応思い通り書けるようになります。

 注意が必要だね、という話でした。そもそもそういう事態(三項演算子の多重ネスト)を避けた方が聡明といえば、それまでかもしれませんが、必要に迫られて書くときは注意してください。