2012-01-27
■他の言語に慣れた人がRubyを使ったときにつまずきがちな9つのポイント
他の言語に慣れた人がPythonを使ったときにつまずきがちな9つのポイントの続編。ささださんも書いています see: だいありー
0は真, 空文字列も真
C言語などと違って、0は偽ではない。PythonやJavaScript, PHPと違って空文字列も偽ではない。
>if 0 then print "true!\n" end true! > if "" then print "true!\n" end (irb):1: warning: string literal in condition true!
括弧がなくても0引数での関数呼び出しが起きる
PythonやJavaScriptでは式中に現れた関数名は関数を指す。しかしRubyでは0引数での呼び出しが行われる。
# Python >>> def foo(): print "foo!" ... >>> foo <function foo at 0x100445f50>
# Ruby > def foo() print "foo!\n" end => nil > foo # メソッド呼び出しが起きる foo! => nil
変数はすべて参照
> a = [] > b = a > b << 1 > a => [1]
ソースコードのエンコーディング
Ruby1.9ではソースコードのエンコーディングを指定せずにソースコード中でASCII以外の文字を使うとシンタックスエラーになる。
$ cat > t.rb p "ほげ" $ ruby1.9 t.rb t.rb:1: invalid multibyte char (US-ASCII) t.rb:1: invalid multibyte char (US-ASCII)
PythonやScheme同様の方法でソースコードのエンコーディングを指定できる。詳しいことはこちら:多言語化
# -*- coding: utf-8 -*- p "ほげ"
1.8で下記のようなコードをSJISで書くと構文エラー(unterminated string meets end of file)になる。ruby起動時のオプションに-Ksを指定する必要がある。
print "機能"
JIS規格に準拠した処理系が、ソースコードにUS-ASCII以外の文字が入っていた場合にどういう挙動をするかは未規定である。
整数がオーバーフローしない
整数の加減乗除やシフト演算で整数(Fixnum)の範囲を超えた場合、Cなどのようにあふれたビットを捨てて範囲内に収めるのではなく、上限のないBignumに変換される。そして文字列表示を見ても区別がつきにくい。
> x = -4611686018427387904 > x.class => Fixnum > x /= -1 => 4611686018427387904 > x.class => Bignum
Cで書かれたアルゴリズムを逐語訳したりすると、かなり気づきにくいバグが入ってしまう。
同名のクラス定義があった場合、衝突せずに合成される
class Foo def x; 1 end end class Foo def y; 2 end end p Foo.new.x #=> 1 と表示される p Foo.new.y #=> 2 と表示される
場合によっては便利に使えるケースもあるかも知れない。ソースコードを読む人が意図的に合成されていることを知らないと混乱しそうではある。
あと3つは募集中です
■Pythonでサロゲートペアの範囲の数値実体参照を文字に戻す
𠮟 (𠮟)が数値実体参照で表現されているのをPythonのUnicode文字に戻すのにどうするの?という質問について。普通にunichr(134047)したら以下のような例外が飛ぶ。
ValueError: unichr() arg not in range(0x10000) (narrow Python build)
(narrow Python build)と書かれているように、これってコンパイル時のオプションに依存する挙動で、下記のオプションを付けてビルドしなおせば解決する。もしくはPython3を使うか。
./configure --enable-unicode=ucs4
とはいえ、ビルドしなおすのも3にするのも嫌だったりするわけなので解決方法を調べてみた。
>>> code = 134047 >>> "%08x" % code '00020b9f' >>> ("\U" + "%08x" % code).decode("unicode-escape") u'\U00020b9f' >>> print _ 叱
できたできた。ちなみにコードの最後の叱が𠮟ではないのは、はてなダイアリーが𠮟を勝手に実体参照に置き換えてしまって正しい表示ができないので、なるべく見た目が近いように書き換えた為です。
ref. ValueError: unichr() arg not in range(0x10000) (narrow Python build), please help - Stack Overflow
2012-01-25
■ 他の言語に慣れた人がPythonを使ったときにつまずきがちな9つのポイント
今日質問されて、以前Twitterで書いたのを思い出して、そして検索性が悪くて見つけ出すのに苦労した。こちらに転載しておく。詳細は気が向いたときに埋める。
オプション引数の評価タイミング
Rubyではオプション引数は関数が呼ばれるたびに評価される。
def foo() print "foo!" end def bar(x=foo()) end bar #=> foo! と出力される bar #=> foo! bar #=> foo!
Pythonでは関数の定義時に1回だけ評価される。
def foo(): print "foo!" def bar(x=foo()): pass #=> foo!と出力される bar() #=> 何も出力されない bar()
「引数が省略されたら今の日時」みたいな毎回評価したい場合はデフォルト値をNoneにしておいて「Noneだったら=省略されていたら」のif文を書けばOK。
def foo(when=None): if when == None: when = datetime.now()
内包表記はスコープを作らない
>>> x = 42 >>> [x + 100 for x in range(5)] [100, 101, 102, 103, 104] >>> x 4
また、下のようなコードでは、関数の中の自由変数を評価するタイミングで親のスコープでのiは2になっているので、どの関数を呼び出しても+2になってしまう。
>>> functions = [lambda x: x + i for i in range(3)] >>> functions[0](100) 102
上記の「オプション引数は関数定義時に評価される」という挙動を利用したテクニックとして 「lambda x, i=i: x + i」 という書き方もある。でも、そもそも「パラメータの違いによって微妙に異なる挙動をする、類似した複数個の関数を作りたい」というモチベーションは、クラスを使って実装するのが素直なのでは。
class Func(object):
def __init__(self, i): self.i = i
def __call__(self, x): return x + self.i
変数はすべて参照
>>> a = [] >>> b = a >>> b.append(1) >>> a [1]
代入ではコピーが行われない。コピーしたければfrom copy import deepcopyとか。
文字列とユニコード文字列は別物
Pythonで文字列(str)と呼ばれるものは、バイトの列である。それとは別にユニコード文字列(unicode)があり、これは「Unicode コード単位」の列である。大雑把に言って文字列の各要素は8bitで、ユニコード文字列の各要素は16bitか32bitである。
# MacのTerminalにて(エンコーディングはUTF-8) >>> "ほげ" '\xe3\x81\xbb\xe3\x81\x92' >>> u"ほげ" u'\u307b\u3052'
# Windowsのコマンドプロンプトにて(エンコーディングはcp932) >>> "ほげ" '\x82\xd9\x82\xb0' >>> u"ほげ" u'\u307b\u3052'
unicodeとstrの暗黙の変換
結合などでのDecode
ユニコード文字列同士は結合しても何の問題もないが、ユニコード文字列とバイト列(str)を結合しようとすると、バイト列の方をユニコード文字列に変換しようとする。この時、エンコーディングがasciiであると仮定して変換するので日本人は大体UnicodeDecodeErrorで死ぬ。そのバイト列がUTF-8なのかSJISなのかはあなたしか知らないので、明示的に x.decode("utf-8") などとやってPythonに教えてあげなきゃダメ。
>>> u"ほげ" + u"ふが" u'\u307b\u3052\u3075\u304c' >>> u"ほげ" + "ふが" # 2つめがユニコード文字列ではない --------------------------------------------------------------------------- Traceback (most recent call last) ----> 1 u"ほげ" + "ふが" UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 0: ordinal not in range(128)
結合の他に、比較などでもこの暗黙の変換が行われる。暗黙の変換に起因するトラブルを減らすために、Python3.0からは"ほげ"って書いたらユニコード文字列、b"ほげ"って書いたらバイト列、ユニコード文字列とバイト列は暗黙に変換しない、というルールに変わった。結合しようとすると「TypeError: Can't convert 'bytes' object to str implicitly」となる。明示的に変換せよ。
printでのEncode
逆に、ユニコード文字列をprintする際には、端末の文字コードにあわせてバイト列に変換が行われる。
print u"ほげ" #=> ほげ と表示される
しかし、出力をファイルにリダイレクトすると、出力先が端末ではないので文字コードを取得できずasciiとみなされる。そのためこのスクリプトは出力をリダイレクトすると UnicodeEncodeError で死ぬ。
tmp$ python t.py
ほげ
tmp$ python t.py > out.txt
Traceback (most recent call last):
File "t.py", line 2, in <module>
print u"ほげ"
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
出力先が端末かファイルかに関係なく特定のエンコーディングで出力したいのならば、標準出力をwrapしてしまうのが手軽。
import sys, codecs
sys.stdout = codecs.getwriter("utf-8")(sys.stdout)
see also: UnicodeDecodeError/UnicodeEncodeErrorに悩まないPython 2.x プログラミング - atsuoishimotoの日記
ソースコードのエンコーディング
ソースコードのエンコーディングを指定せずにソースコード中でASCII以外の文字を使うとシンタックスエラーになる。
SyntaxError: Non-ASCII character '\xe3' in file t.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
他の言語では、ソースコードのエンコーディングをlatin-1などと仮定することがある。しかしその場合、たとえばソースコードがShift-JISで記述されていると「表」という文字の2バイト目がバックスラッシュと同一の文字になるので「"図表"」の2つめの引用符がエスケープされてしまうなどの問題が起きる。(see also: Wikipedia 2バイト目が5C等になりうることによる問題)
Pythonではそれを避けるために「ASCII以外の文字を使うならエンコーディングを指定すること」と規定している。エラーメッセージに詳細へのリンクが書かれているが、簡単に説明すると、ソースコードの1行目か2行目に正規表現 "coding[:=]\s*([-\w.]+)" にマッチする文字列があればよい。例:
# -*- coding: utf-8 -*- print u"ほげ"
1要素のタプルにはカンマが必要
タプルはカッコではなくカンマで作られる。1要素のタプルは (x, )である。カッコは省略してもよい。
言語リファレンスの式のリストより一部抜粋:
少なくとも一つのカンマを含む式のリストは、タプルになります。//単一要素のタプルを作りたければ、末尾にカンマが必要です。//空のタプルを作りたいなら、中身が空の丸括弧ペア: () を使います。
>>> (1) 1 >>> 1, (1,)
代入によってローカル変数が作られる
JavaScriptなどと異なり、代入によってローカル変数が作られる。なので、ネストした関数の中で外のスコープに存在する変数名対して代入を行なっても、内側のスコープで新しいローカル変数が作られるだけで、外の変数には影響を与えない。
def outer(): x = 1 def inner(): x = 2 # ここで関数innerのローカル変数xが作られていて、 # outerのxは書き換えられていない inner() print x outer() #=> 1 と表示される。ほら書き換わってない
python3.0では「この名前はローカル変数ではない」という宣言(nonlocal)が可能になった。
def outer(): x = 1 def inner(): nonlocal x x = 2 inner() print(x) outer() #=> 2 と表示される。書き換わった!
また、代入が実行されるタイミングでローカル変数が作られるのではなく、関数の中で代入されうる変数はその関数のローカル変数となる。下の例で、xは関数fooの中で代入されうるのでfooのローカル関数になる。なのでprint xのタイミングで「local variable 'x' referenced before assignment」(ローカル変数xは代入する前に参照されている)というエラーになる。
x = 1 def foo(): print x x = 2 foo() #=> UnboundLocalError: local variable 'x' referenced before assignment
整数がオーバーフローしない
整数の加減乗除やシフト演算で整数の範囲を超えた場合、Cなどのようにあふれたビットを捨てて範囲内に収めるのではなく、上限のない「長整数」に変換される。
>>> 1 << 63 9223372036854775808L >>> -9223372036854775808 / -1 9223372036854775808L >>> type(1) <type 'int'> >>> type(1L) <type 'long'>
特にCで書かれた暗号化や擬似乱数生成のアルゴリズムは、オーバーフローによって切り詰められることを想定して書かれている場合があって、Pythonに逐語訳すると期待通りに動かない。
おまけFAQ
良い入門書はないですか?
strだとかintだとかの説明はどこに?
ライブラリリファレンスの第2章: 2. 組み込み関数
文字列とかリストとかタプルとかについて調べようと思ったがライブラリリファレンスの目次をざっと読んでもそれらしき所がない…
ライブラリリファレンスの第5章 5. 組み込み型 の「シーケンス型」の節にまとめられている。
シーケンス型には 7 つあります: 文字列、Unicode 文字列、リスト、タプル、バイト配列 (bytearray)、バッファ、そして xrange オブジェクトです。
2012-01-22
■ブタのスカモルツァを食べた話
チーズ専門店HISADA チーズ通販/商品詳細 スカモルツァ マイアリーノ豚 SCAMORZA MAIALINO 1P(約100g) Scamorza - Wikipedia, the free encyclopedia
ワックスで包まれている。って茶色い方は僕がいじりすぎて剥がれちゃったんだけど。
ワックスをはがすとこんな感じ。
ザクザクっ、と切ってフライパンで焼く
サラダに乗せて出来上がり。
このあとワックスの剥がれてなかった白い方も食べてみたが、そっちの方が表面の半透明なゾーンが薄くて、中のモチモチの部分が多い感じだった。茶色のほうが燻製されていて香りが素晴らしい。僕は茶色が好みだなぁ。
2012-01-21
■GTDマインドマップ ver3
GTDレバレッジメモasマインドマップ、GTDレバレッジメモasマインドマップ その2の続編。昨年10月に作ったver.2を描き直したくなったのでTwitterでブツブツつぶやきながら2ポモドーロほど作業をして新しいバージョンを作った話。以下Twitterからの転載。
以前GTDを読んで作ったマインドマップを、使っているうちにちょこちょこ書きたしたり別のアイデアが湧いたりしているので、いっちょ0から描き直してみるかな、と思っているナウ。前のバージョン http://pic.twitter.com/zUAPBOuu
最初はGTDの本に書いてあることを書き漏らさないようにしようとしすぎて密度が高かった、前回のバージョンでは特に右端の軸で、オリジナルのGTDでは5つに分割されていたものが3つにまとめられるなどしている。今回はもっとバッサリ行く予定。
まずは目的の明確化。目的はGTDをそのままマインドマップ化することでも、GTDを人に教えることでもない。自分が定期レビューの際に混乱しがちなので、混乱せずにやるべきことを見通せるように、今のマインドマップをもっとすっきりとしたものにすることだ。自分が毎週のレビューの際に見直すための指標を作ることだ。
このマップの中で重要なことってなんだろう、って自問したら、その重要な四つのキーワードが現在のバージョンでもマップの4隅にあることに気がついた。「収集」「行動」「目的」「定期レビュー」だ。
なぜ僕はレビューの際に混乱するのか?理由は簡単だ。今まで収集されるものはタスクや、やりたいこと(ゴール)が中心だったので、タスクの整理というワークフローで処理できていたが、執筆プロジェクトを開始して執筆に関するアイデアやコンセプトなんかが大量に出てきて整理しきれてないんだ。
GTDのフローチャート通りに判断するなら、この執筆アイデアの断片は「行動すべきか」を問われるべき。行動する必要がないなら、必要になったときに取り出せる形で資料として保存すべき。あれ、「資料として保存する」も行動だな。
GTDはINBOXの整理の過程で別のアイテムが追加されうるってことに明確に気づいていないな。INBOXにXが入っていて、それに対して「これは何?どうすべきもの?」と問うて、その答えが「Xは資料、保管しておくべき」となった時、Xの他に「Xを保管する」ってタスクが現れている。それが2分以内でできるからその場で処理しているだけ。でもいつでも2分以内でできるとは限らない。「INBOXの中のデータを資料箱に、必要な時に取り出せるようにしまう」というタスクに時間がかかる可能性が考慮されていない。現実には、僕はまだ「必要な時に取り出せるようにしまうための場所」を用意してなかったり、そのためのシステムを作っていない。だから2分で終わらない。だから着手できずにダラダラ放置して、その結果、未整理のアイデア付箋が山になってしまって困っているわけだ。
収集とは脳から出すことであり、レビューとは脳に戻すこと。
資料がどう整理させるべきかについて僕の中に答えがない。必要なときに取り出せないものには意味が無いので、取り出せることを保証する必要がある。デジタルデータにして検索することができるものならそれもひとつの手だろう。でもアイデアメモのたぐいをどうすべきかが難しい。
答えがないものをウンウン唸っていても出てくるもんでもないので、そこは「考える必要がある」とでもメモして保留にするか。
1ポモドーロ終了 http://pic.twitter.com/7hTejhUe
あと1ポモドーロで完成させて寝るつもりでやろう。あー、付箋の整理が結局済んでないなぁ。資料の整理方法について考える必要がある、それがないのが付箋が積み上がって不快感を醸し出している原因だ、ってわかっただけでも収穫か。明日歯医者の待ち時間で考えよう。
2ポモドーロ終了 http://pic.twitter.com/pRWZ43MW
ver2の際には縦軸が5つにわかれている必要ないな、と3つにまとめたんだけど、今回は横軸がもはや要らないような気がする。整理とは意味と場所を一致させることだから、意味の見極めなしに整理は無理。目的の明確化が適切な計画と行動の前に必要。それはわかるが、整理と計画、行動がこの順で並んでいることにあまり意味がないよね。(追記: これを書いたときには気づいていなかったがそもそもGTDオリジナルでは 整理→レビュー→行動 だったのでさらにめちゃくちゃだった)
次に考えるべきことは「机の上の未整理の付箋の山を適切に整理する」という事自体が、具体的な、実行可能な、時間の見積もりができるタスクではない、ってことだ。つまりこれは粒度としてはプロジェクトだから、目的が明確でなければならない。付箋を整理するのは何のためだ?
…既に原稿に書いた部分の付箋と、まだ書いてないアイデアの付箋とが混じってるんだよな。まだの付箋に関しては、該当の章を書こうと思ったときに取り出せることが重要。だからとりあえず章立てと見比べてどの章に入りそうかで分類しよう。既に書いた付箋は何のために整理するのか?
本当に「既に書いている付箋」なら捨てていいと思うんだ。それができてない最大の理由は「流れに上手く入らないので書いてない」内容を「捨てるには惜しい」と思っているせいだ。だからまずやるべきことは書いた内容か書いてない内容かを照らし合わせることだ。その上で、やはり捨てるには惜しい内容だと思ったなら、コラムの見出しだけ考えて他の「まだ書いていない付箋」と同じように分類すればいい。コラムを書くときに参照すればよい。書かないかも知れないけど。
ちなみにver3の読みづらい字になんて書いてあるかというと「収集(記憶に頼らない):人間の頭脳は有限である」「行動:人は一歩ずつしか歩けない」「目的:目的の明確化が効率化の要」「レビュー:定期的に戻す保証が安心して忘れるために必要」って感じ。
当初、解決したいと思っていた「付箋の山を何とかする」がレビューの問題だと思っていたのでレビューを改善することを目的に挙げたが、問題はそこではなかった。「付箋の山を何とかする」が実行するには粒度が大きすぎるタスクであり、それは目的を明確にしてブレイクダウンする必要があるってことだな。
レビューがうまく出来てない件に関してはまだ解決できてないけど、このマップは余白が多めだし、まあ余白で書き切れないならまた描き直せばいいし。なんとかなるでしょう。今日はもう寝るとしよう。
2012-01-20
■Pythonのpickle.dumpに3番目の引数がある話
cPickle.dump(obj, f, -1)みたいな使い方を見て、なんだこの-1?と思ったので調べてみた。簡潔に言えば、これはフォーマットの指定で、指定しなかったときは後方互換性のために初期のASCII形式での保存がおこなわれる。-1ってのは最新のを使えという意味で、今ならバージョン2ということになる。
There are currently 3 different protocols which can be used for pickling.
- Protocol version 0 is the original ASCII protocol and is backwards compatible with earlier versions of Python.
- Protocol version 1 is the old binary format which is also compatible with earlier versions of Python.
- Protocol version 2 was introduced in Python 2.3. It provides much more efficient pickling of new-style classes.
http://docs.python.org/library/pickle.html
で、気になるのはファイルサイズとか速度とかだよね。
計測方法
Numpyのndarrayで1000x1000のサイズの0〜1の乱数が入った行列を作り、それを文字列にシリアライズしてサイズと時間を計測した。サイズは文字列の長さ、時間はipythonの%timeitで適当な回数繰り返して計測した。
時間
まあざっくり100倍違いますね。
%timeit s = cPickle.dumps(m) 1 loops, best of 3: 1.14 s per loop %timeit s = cPickle.dumps(m, 1) 100 loops, best of 3: 18.4 ms per loop %timeit s = cPickle.dumps(m, 2) 100 loops, best of 3: 18.4 ms per loop
サイズ
こちらの差は3倍弱。
len(cPickle.dumps(m)) 22217167 len(cPickle.dumps(m, 1)) 8000141 len(cPickle.dumps(m, 2)) 8000136
まとめ
version 1と2の違いは対象がndarrayだとよくわからないけど、少なくともversion 0を使い続ける明確な理由がないなら新しいバージョンを使った方がよさそう。
おまけ
Cで書かれていないpickleの方を使った場合、version 0では速度に大した差が出ないが、1以降なら2〜3倍遅い。
%timeit s = pickle.dumps(m) 1 loops, best of 3: 1.16 s per loop %timeit s = pickle.dumps(m, 1) 10 loops, best of 3: 42.3 ms per loop %timeit s = pickle.dumps(m, 2) 10 loops, best of 3: 42.1 ms per loop
■gitにでかいバイナリファイルを入れるとどうなるか
ふと気になったのでgitにでかいバイナリファイルを入れたらどうなるのか調べてみた。
自分の発表が録画された112メガのaviファイルを実験対象に使う。
cp
まずはgitを使わない普通のcpの時間を測っておく。
real 0m0.744s user 0m0.001s sys 0m0.179s
git add
git addにはコピーの10倍以上の時間がかかる。
real 0m9.339s user 0m7.989s sys 0m0.490s
git commit
git commitには意外と時間が掛からなかった。
real 0m0.055s user 0m0.002s sys 0m0.031s
git clone
先ほど動画ファイルをcommitしたリポジトリをcloneしてみる。単純なファイルコピーに比べると3倍弱時間がかかる。
real 0m1.943s user 0m0.932s sys 0m0.433s
git addには3パターンある
コミット済みのものを変更せずにそのままもう一度addした場合、きっとタイムスタンプを見てすぐに「変更されていない」と判断している。
real 0m0.020s user 0m0.001s sys 0m0.004s
タイムスタンプを変えるためにtouchしてからaddしてみた。それでも最初に追加した時ほどの時間は掛からない。何で判断しているのかな?
real 0m0.616s user 0m0.507s sys 0m0.094s
もちろんcatの追記でちょこっとファイル内容を変更してやると、初回add時と同じくらいの時間がかかる。
real 0m9.159s user 0m7.994s sys 0m0.490s
追記: time md5 ...ってしたらreal 0m0.617sだったので、2番目の例ではだいたいそれに類する処理が走っているんだと思われる。ということはむしろ3番目の例でこんなに時間がかかっているのが不思議なのか。バイナリーファイルかどうかとか無関係に差分を計算しているのかな?
さらに追記: 初回にaddしたときも同程度の時間がかかってるんだから差分計算じゃないよね、という指摘があったが全くそのとおり。んー、じゃあzlibでの圧縮に時間がかかっている?というわけで試しにtime zip ... ってやってみたら real 0m9.792s だった。なるほど、つまりは圧縮に時間がかかっている、と。
まとめ
細かい数値は記憶に残らないのであえてバッサリ要約すると、1GBのファイルがコピーに7秒掛かる環境で、同じファイルがaddに80秒、cloneに20秒掛かる、という比率。addにかかっている時間は大部分がzlibで圧縮するのに掛かる時間。ネットワーク越しcloneなどは計測していない。ファイルサイズを変えての計測も試してない。
■Pythonの例外を記録してみる
よくあるエラー が意外と参照されている。で、ふと「例外のフックを使って起こった例外を全部記録しておいて、たくさん溜まってから集計したら面白いんじゃないか」と思った。というわけでsite.pyに下のように書きたしてみた。
def setup_exc_hook(): "record exceptions" import sys, traceback def hook(type, value, tb): sys.__excepthook__(type, value, tb) lines = traceback.format_exception(type, value, tb) file("/Users/nishio/exclog.txt", "a").write( "".join(lines) + "\n" + "=" * 40 + "\n") sys.excepthook = hook setup_exc_hook()
とはいえ、どちらかというとPythonやプログラミングに不慣れな人のデータが欲しいわけなので…誰かそういう心当たりありませんか?あ、妻に使わせてみればいいのか?



