Pythonらしいコードの書き方
はてなブログに移行して最初の記事はやはりPythonネタにしました。 はてなブログいいですね。デザインの編集がやりやすくなったのと、Markdownで書けるのが素晴らしいです。
PyCon 2013の動画を見ていたら、素晴らしい"Transforming Code into Beautiful, Idiomatic Python"という発表を見つけたのでそのまとめです。
今どきのPythonコードのベターな書き方を紹介しています。
Transforming Code into Beautiful, Idiomatic Python ...
結構長くなってしまったので、知ってる項目は読み飛ばしてもらえばと思います。
ループの基本
整数のループ
まずは基本のループ。
Cのfor int i=0; i<6; i++
をPythonで単純に書くとこうなります。
for i in [0,1,2,3,4,5]: print i**2
リストの部分は通常range(6)
に置き換えますが、iの値がとてつもなく大きい値までループで回した場合にはメモリを消費しすぎてしまうという問題があります。
多くの人がすでに知っているとは思いますが、xrangeを使うのがベターです
for i in xrange(6): print 1**2
xrangeはrangeと違って一気にメモリを確保しないので、メモリが節約できます。
動画中では、xrangeという名前は醜い!と言って笑いを取っていましたw
ちなみにPython3ではrangeがxrangeと同様の動きをするようになりましたので、rangeを使用してOKです。
コレクションのループ
リストなどの要素をループ中で取得したい場合、他の言語から移ったばかりの初心者はこのように書きがちです。
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print colors[i]
Pythonのfor文はこのように他の言語のforEachと同様の働きもします。
colors = ['red', 'green', 'blue', 'yellow'] for color in colors: print color
for文では英語の単数形と複数形を使うことで、何をループさせているのかひと目で分かりやすくさせるのがお作法になってます。
逆順のループ
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)-1, -1, -1): print colors[i]
reversedを使うことで何をしているのか分かりやすくなります。
colors = ['red', 'green', 'blue', 'yellow'] for color in reversed(colors): print colors
むしろ最初のrangeの例を使う人なんているのかな・・・
コレクションのループと同時に要素番号も取得したい
わりとよくあるパターンだと思います。
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print i, '-->', colors[i]
これはenumerateを使うことでスッキリ書くことができます。
colors = ['red', 'green', 'blue', 'yellow'] for i, color in enumerate(colors): print i, '-->', color
同時に二つのコレクションをループさせたい
二つのリストから同じ要素番号の要素を取ってきて、要素数が少ない方に合わせてループを終了したいというパターンです。
names = ['raymond', 'rachel', 'mattew'] colors = ['red', 'green', 'blue', 'yellow'] n = min(len(names), len(colors)) for i in range(n): print names[i], '-->', colors[i]
複数のリストをまとめてくれるzipを使うとスッキリ書くことができます。
(実際にはイテラブルなものであれば、リストじゃなくても大丈夫です)
for name, color in zip(names, colors): print name, '-->', color
zipもrange同様、要素が大きすぎる場合にはメモリの問題があるので、そのようなときはzipをitertoolsモジュールのizipに置き換えるといいです。Python3ではrange同様、zipがizipと同じ動作をするように変更されているそうです。
ちなみにitertoolsにはizip以外にも便利なものが揃っているので、一度はドキュメントに目を通すといいと思います。combinationsとかproductを知った時には目からうろこが落ちました。
複数のループ終了条件
ここからは少し複雑なループの使い方になります。
ループ中で一つも条件に合わなかった時、例外的な値を返したいという処理はよくあると思います。ついフラグを使いたくなってしまうパターンですね。
def find(seq, target): found = False for i, value in enumerate(seq): if value == tgt: found = True break if not found: return -1 return i
フラグ変数が入るとコードが見難くなり、何をやっているのか分かりにくくなりがちです。
このような場合はfor文の中でelseを使うことでフラグを使わなくてもエレガントに書けます。
def find(seq, target): for i, value in enumerate(seq): if value == tgt: break else: return -1 return i
し、知らなかったーー!! elseはfor文がbreakされずに正しく終了したときに実行されるらしいです。
辞書の使い方
何かとお世話になる辞書(dict)ですが、知ってるとエレガントにコードが書けるテクニックがいくつかあるそうです。
キーでのループ
まずは基本。dictでループを回すとキーの値を取得できます。
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k in d: print k
キーとバリューでのループ
キーが取得できるのだから、単純に思いつくのはこのような形です。
for k in d: print k, '-->', d[k]
辞書オブジェクトのitems()を使うとd[k]をする必要がなくなり、変数名を自由に付けられるのでコードの見通しがよくなります。
for k, v in d.items(): print k, '-->', v
ちなみにrange, zip同様、辞書のサイズが大きいとメモリを消費してしまうので、そのような場合はiteritems()を使うとイテレーターで取得するのでメモリの節約になります。
for k, v in d.iteritems(): print k, '-->', v
ペアから辞書の作成
キーとバリューの候補が既にリスト等で存在する場合は、for文などを使わなくても一発で辞書に変換できます。
names = ['raymond', 'rachel', 'mattew'] colors = ['red', 'green', 'blue'] d = dict(izip(names, colors)) >>> d {'rachel': 'green', 'mattew': 'blue', 'raymond': 'red'}
辞書によるカウント
自然言語処理などでは、どの単語が文書に何回登場したかを数えたいことがよくあります。
辞書はお手軽なのでよく使いますが、新しい単語が登場したときに新たなキーとして登録しないとエラーになるため、少し工夫をする必要があります。
colors = ['red', 'green', 'red', 'blue', 'green', 'red'] d = {} for color in colors: if color not in d: d[color] = 0 d[color] += 1 >>> d {'blue': 1, 'green': 2, 'red': 3}
辞書オブジェクトのgetを使うと、キーが存在しなかった場合は引数で渡した値をバリューとしてセットしてくれるので、上のコードのfor文内は1行で済みます。
d = {} for color in colors: d[color] = d.get(color, 0) + 1
さらに便利なのがcollectionsモジュールのdefaultdictです。defaultdictはキーが存在しなかったときに、自動的に作成するオブジェクトを指定できます。
し、知らなか(ry
d = defaultdict(int) for color in colors: d[color] += 1
defauldict(int)とはどのような意味でしょうか?手元でインタプリタを起動して確認してもらえばと思いますが、int()は0を返します。
>>> int() 0
つまり、defultdict(int)は登録されていないキーが呼び出された時に、自動的に{キー: 0}を登録してくれます。
動画では紹介されていませんが、個人的にはカウンタとして使用するなら同じくcollectionsモジュールのCounterの方がより直感的でオススメです。
辞書によるグルーピング
辞書のバリューをリストにするという、よくあるケースです。
例えば、文字列の長さごとにグルーピングすると、こんな感じになると思います。
names = ['raymond', 'rachel', 'matthew', 'ronger', 'betty', 'melissa', 'judith', 'charlie'] d = {} for name in names: key = len(name) if key not in d: d[key] = [] d[key].append(name) >>> d {5: ['betty'], 6: ['rachel', 'ronger', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']}
カウントの初期値を0にしたのと同様に、キーが存在しなかったときに空のリストをバリューとして登録する必要があります。
これは辞書オブジェクトのsetdefaultで置き換えることができます。
d = {} for name in names: key = len(name) d.setdefault(key, []).append(name)
if文の処理を圧縮することができましたが、発表中ではこれですら美しくないということでdefaultdictを使うことをオススメしていました。
d = defaultdict(list) for name in names: key = len(name) d[key].append(name)
これもカウントのときにint()が0を返すのと同様に、
>>> list()
[]
list()が空のリストを返すことを活用しています。
辞書へのアクセスが通常と同じd[key]なので、setdefaultより直感的に理解しやすいでしょう。
コードの可読性をよくする
キーワード引数
Pythonは関数への引数に '引数名=値' という書き方が可能です。タイプ量は増えますが、引数の名前が分かると関数の振る舞いに予想がつくので可読性が向上します。
twitter_search('@obama', False, 20, True) twitter_search('@obama', retweets=False, numtweets=20, popular=True)
キーワード引数を使うことで、関数の定義を確認しなくてもFalseやTrueが何を設定しているのかひと目で分かります。
名前付きタプル
動画中のサンプルだと分かりづらいので、Python 2.4 Advent Calendar 2012 (6) namedtuple がないが分かりやすくて参考になります。
こちらでも簡単なサンプルを示します。defaultdict同様、collectionsモジュールをインポートするのを忘れないでください。
Point = namedtuple('Point3d', 'x y z') point = Point(10,20,30) >>> point Point3d(x=10, y=20, z=30) >>> print point.x, point.y, point.z 10 20 30 >>> print point[0], point[1], point[2] 10 20 30
このように通常のタプルの振る舞いに加えて、オブジェクトのように参照することが可能になります。ちょっとした構造体のようなものが欲しいときに便利です。
し、知らなかったーー!!というか公式ドキュメント見た時にイマイチ利点が分からなかったので忘れてた。
展開して代入
有名なので解説は必要ないでしょう。0x30がユーモアですねw
p = 'Raymond', 'Hettinger', 0x30, 'python@example.com' fname = p[0] lname = p[1] age = p[2] email = p[3] fname, lname, age, email = p
実は応用すると代入が簡単に行える以外にも便利な場面があります。
フィボナッチ数列を求める関数を例とすると、
def fibonacci(n): x = 0 y = 1 for i in range(n): print x t = y y = x + y x = t
tを一時変数として利用するプログラマーには非常にありふれた方法ですが、直感的に分かりづらいこともあります。
このように書きなおすことで、一時変数を使用する必要はなくなります。
def fibonacci(n): x, y = 0, 1 for i in range(n): print x x, y = y, x+y
x, yが同時のタイミングで代入されるため、このような書き換えが可能になっています。
その他
時間の都合上駆け足になっていましたが、その他にもjoin, deque, デコレータ、with文、リスト内包表記、ジェネレーターなどが紹介されてました。 デコレーターは未だに自分で実装して使いこなすことができないです・・・。
参考文献
HerokuのMongoDBにローカルのDBの内容をアップロードする
実は前回紹介した LocationTweetは最近流行りのHerokuで公開しているのですが、データベースで位置情報を簡単に扱うためにHeroku標準のPostgreSQLではなく、MongoHQというアドオンでMongoDBを使用しています。
MongoHQにローカルのデータをアップロードするのに少し手間取ったのでメモしておきます。
Herokuが提供しているPostgreSQLならここを見る限り便利ツールで楽チンっぽいですが(すいません、HerokuのDBが新しくなるときにMongoDBに乗り換えてしまったので実際にやったことないです)、MongoDBだとちょっとめんどくさくなってしまいます。
実はHerokuのアプリのアドオンとして導入したMongoHQには普通にアクセスできるので、MongoDB自体に標準で存在する機能でdump/restoreすることでローカルのデータをアップロードすることが可能です。
ローカルのダンプ
まずはローカルのMongoDBからアップロードしたいdbをダンプします。
$ mongodump -h <host>:<port> -d <ダンプしたいdb名> -o <ダンプ先>
MongoHQへアップロード
次にダンプしたローカルの内容をmongorestoreでMongoHQにアップロードしますが、公式に載っているコマンドを見ると、
$ mongorestore -h hostname.mongohq.com:port_number -d database_name -u username -p password /path/on/my/local/computer
ということで5つほど引数が必要そうな事がわかります。
Herokuのサイトから自分のアプリの設定画面にあるAdd-onsにあるMongoHQのGUIにアクセスすると、Admin項目から
$ heroku config
で見られる
MONGOHQ_URL => mongodb://heroku:<password>@<hostname>.mongohq.com:<port_number>/<database_name>
に書いてあります。ここでは伏せてありますが、実際には皆さんのMongoHQの情報が表示されます。
これを見ながら先ほどのmongorestoreを書き換えていきます。
$ mongorestore -h <hostname>.mongohq.com:<port_number< -d <database_name> -u heroku -p <password> <ダンプしたデータの保存先>
これで先ほどローカルからダンプしたデータをMongoHQにアップロードすることができます。
ちなみに、自分はエラーが出て地理空間インデックスをアップロード先で作られなかったため、
$ mongo <hostname>.mongohq.com:<port_number>/<database_name> -u heroku -p <password>
で直接MongoHQのシェルにアクセスしてインデックスを作成しなおしました。
というか直接シェルにアクセスできるぐらいだから、Herokuのアプリからじゃなくてもアクセスできそうですね。MongoHQ自体がそういうサービスなんだろうけど。
LocationTweet 気になる場所の今のつぶやきを見る
気になる場所の今のつぶやきを見ることができるLocationTweetというWebアプリを公開しました!
LocationTweet
地図に表示されたピンをタップするとその場所のつぶやきを見ることができます。
学校、駅、観光地など気になる場所について他の人が今どのようなつぶやきをしているか見てみましょう!新しい発見があるかもしれません。
以下はアプリの裏側の話
続きを読むWikipediaから位置情報のデータベースを作る
Wikipediaの記事から位置情報をマイニングするスクリプトをPythonで作りました!
Wikipediaの建物や場所の記事のタイトル右端にある位置情報と情報ボックス内の位置情報から抽出しています。Wikipediaは日々更新されているので、2012年にオープンしたばかりのスカイツリーやダイバーシティのような情報も抽出することができます。
抽出した結果はこんな感じのデータになります
タイトル|カテゴリ|緯度|経度 両国国技館|landmark|35.6969166667|139.793527778 シカゴ||41.9|-87.65 スーパーカミオカンデ||36.4166666667|137.3 アメリカ航空宇宙局|landmark|38.8830555556|-77.0163888889 関西国際空港||34.4272222222|135.243888889 任天堂|landmark|34.9697222222|135.756194444 佐鳴湖||34.7105555556|137.690277778 IBM|landmark|41.1080555556|-73.72
抽出結果のcsvとスクリプトをgithubにアップしました
https://github.com/Kesin11/wikipedia_geomining
csvをそのまま眺めても多くの人は面白くないと思うので、それを地図にマッピングしたウェブアプリも公開しました。一応スマホ向けに作ってあります。(Androidは動作確認していません)
http://location-tweet.herokuapp.com
(こちらのアプリの紹介もいずれ書く予定です)
ただマッピングするだけだと面白くないので、ピンをタップするとその場所に関するツイートを見られるようにしてあります。自分が良く知ってる場所のツイートを見てみると結構面白かったりします。
MapKitでピンに合わせて地図の表示領域を自動調節する
前回の記事の続きです
執筆時ではXcode4.4.1でARCを使うコードとなっています。Objective-CやXcodeの進化は速いのでこのバージョンより新しい(古い)Xcodeではこの記事の通りにやっても動かない可能性があります。ご注意ください。
ピンが全て表示されるように地図の中心位置と倍率を調整する
普通の地図アプリを使ってて当たり前にある機能なのに、自分で地図アプリを作ろうとすると結構悩む部分だと思います。正直、この機能はAppleが用意してくれてもいいと思うのですが・・・。
この機能を実現するには地図の表示領域を設定するsetRegionに渡すための中心点(center)と倍率(span)を計算で求めます。
地図の中心点は全てのピンから最大最小の緯度経度の4点を求め、その中心になります。
倍率に関しては正直なところMKcoordinateRegionMakeのcenter, spanの関係がイマイチ理解できてないのでちゃんと説明できないのですが、最大緯度から最小緯度への距離と、最大経度から最小経度への距離をspanに入れてやればOKなようです。
というわけでコードにするとこんな感じです
double minLat = 9999.0; double minLng = 9999.0; double maxLat = -9999.0; double maxLng = -9999.0; double lat, lng; for (id<MKAnnotation> annotation in mapView.annotations){ lat = annotation.coordinate.latitude; lng = annotation.coordinate.longitude; //緯度の最大最小を求める if(minLat > lat) minLat = lat; if(lat > maxLat) maxLat = lat; //経度の最大最小を求める if(minLng > lng) minLng = lng; if(lng > maxLng) maxLng = lng; } CLLocationCoordinate2D center = CLLocationCoordinate2DMake((maxLat + minLat) / 2.0, (maxLng + minLng) / 2.0); MKCoordinateSpan span = MKCoordinateSpanMake(maxLat - minLat, maxLng - minLng); MKCoordinateRegion region = MKCoordinateRegionMake(center, span); [mapView setRegion:[mapView regionThatFits:region] animated:YES];
博多駅、新大坂駅、東京駅、新青森駅にピンを表示させて上のコードで地図を自動調節させるとこんな感じです。ここから博多駅のピンを消去し、もう一度地図の表示を調節させます。
新大森駅のピンがちょっとはみ出てますが、だいたい期待した表示でしょう。今度は新青森駅のピンを削除します。
今度もOKですね。横の余白の広さも自分的には調度良いと感じますが、気になる方はspanに適当な値をかけると調整できるかと思います。
ちなみに、新大阪駅のピンも削除すると、
東京駅に最大ズームされてしまいます^^; ピンの数が1つのときだけは独自に適当なspanを設定する必要があるかと思います。
今回は静止画でしか地図の表示が変わる様子をお見せできませんでしたが、実際に動かしてみると地図がヌルヌルと調度良い位置、倍率に調整されるので気持ちいいです。ぜひ試してみてください。