新人のpython使いのためのTips & Tricksを書きます。要点は以下の6点です。
- 標準ライブラリを使ってコードを簡素化
- 無駄なfor/while文を排除
- コードの実行とリソース要件のトラック
- 2つのテーブルを結合する際の行数の増加を防ぐ
- Stack Overflowでコーディングソリューションを見つける
- コードのユニットテスト
#1. 標準ライブラリを使ってコードを簡素化
標準ライブラリはガンガン使いましょう。単にコードが見やすく短くなるだけでなく、より効率的に動作するようになります。
- 良くない例
import CSV
rows = []
colnames = ['a', 'b', 'c']
with open('filename.csv', 'rb') as f:
reader = csv.reader(f)
for row in reader:
rows.append(rows)
data = pd.DataFrame(rows, cols = colnames)
上記のように、for文でcsvファイルを行ごとに読み込むと、コードを書く量が多くなり、標準モジュールを用いた実装よりも動作が遅くなってしまいます。
- 良い例
import pandas as pd
data = pd.read_csv("filename.csv")
このようにpandasを利用することで、バックグラウンドで最適化されたpandasの実装を利用しながら、csvファイルの読み込みを1行に短縮することができます。
基本的に、初心者がやりたいような作業には既にライブラリが実装されています。せっかく先人たちが作ってくれたものですので、ガンガン使って省力化&効率化していきましょう。
#2. for/while文を避ける
forやwhile文を使うと、コードは複雑になり、過剰な実行時間がかかりがちです。どうしても使わなければならない処理の場合は仕方ありませんが、避けられるのであればなるべく使わないように心がけましょう。
- 良くない例
import time
import pandas as pd
start = time.time()
for row in df.iterrows():
df.loc[row[0], "Flag"] = row[1].target_column > 0
end = time.time()
print(end - start)
データフレームにフラグを立てようとしていますが、indexメソッドを用いて行ごとに反復する方法では、下の方法に比べて500倍も遅くなってしまいます。
- マシな例
import time
start = time.time()
df["Flag"] = df.apply(lambda x: x.target_column > 0, axis = 1)
end = time.time()
print(end - start)
forを使った反復処理を行わず、applyフレームワークの使用(C言語で実装されている反復処理とインデックス作成を高速化している)することで高速化することができます。
- 良い例
import time
start = time.time()
df["Flag"] = df.target_column > 0
end = time.time()
print(end - start)
これは上記の"マシな例"の更に30倍くらい早いです。スライスと論理インデックスを組み合わせて、カラム全体に適用するのがベストプラクティスです。
不必要な反復処理は、初心者のコーディングにおける最大の非効率の要因です。一方で、同じ反復処理でも既存のライブラリの多くは、データ構造に合わせて反復処理が最適化されており、ゼロから書いたものよりも高速に動作します。
また、イテレーションに気を配るという習慣を身に着けることで、将来的にパフォーマンスのデバッグにかかる時間を大幅に短縮できます。
#3. コードの実行とリソース要件のトラック
実際にコードを書いて、それをrunしてみたときに、一切のエラーがなくかつ実行時間も短く済むようなケースは稀です。何らかのエラーが発生してコードが動かなかったり、はたまたコードは正しくとも実行時間に非常に時間がかかってしまうような組み方になってしまっていることもあります。このように、非効率な部分やエラー部分を特定するために必要なのがロギングとプロファイリングです。
ロギングとは、プログラムの実行中に起こった出来事の記録を出力することです。つまり、関数のタイムスタンプ付きロギングメッセージでコードの実行状況をトラックします。これにより、何か問題が発生した場合にコードをデバッグするのに役立ちます(つまり、どのプロセスが最後に実行されたかを可視化することができます)。また、さまざまなコンポーネントの速度を可視化することで、コードの速度を最適化するのにも役立ちます。
プロファイリングでは、プログラムが必要とするリソース(メモリ、CPU時間)を測定する分析を行います。これには、各サブファンクションが必要とするリソースや、そのサブファンクションが呼び出される頻度の測定も含まれます。これにより、個々のサブファンクションだけでなく、プログラム全体を最適化することができます。
ロギングやプロファイリングを行わないと、コードのどこに非効率な部分やエラーがあるのかの特定に非常に時間がかかってしまいます。これらはお作法として習慣化してしまうのが一番です。また、実行過程をトラックするだけでなく、最後に要約統計をプリントアウトするのも分かりやすくて良いです。
詳しくは以下のドキュメントをご参照ください。
- ロギング: Logging Cookbook
- プロファイリング: The Python Profilers
#4. 2つのテーブルを結合する際に行数の増加を防ぐ
データ分析を行う際に、複数のテーブルを結合することはよくあることですが、結合しようとしている2つのテーブルの関係を定義せずにクロスジョイントすると、行数が爆発的に増えてしまう(左テーブルの各行が右テーブルの各行と結合される)可能性があり、注意が必要です。
"Exploding rows"はPythonに限ったことではありませんが、テーブル構造を結合する際には必ず考慮する必要があります。防ぎ方としては、pd.merge の validate オプションを使用して、爆発的な結合を想定していない場合にエラーをスローするのが一般的です。
#5. Stack Overflowでコーディングソリューションを見つける
QiitaやStack Overflowなど、コーディングの知見や疑問に対する答えをまとめたサイトはたくさんあり、それらを参照しない手はありません。基本的に初心者が悩む問題なんてものは、既に先人たちも悩んでおり既にエレガントな解放が示されていることがほとんどです。
適切な回答を見つけ出すためには、ある程度の適正な検索ワードを選ぶ"ググる能力"が必要になりますが、場数を踏むにつれてこの力は向上していきます。
Stack Overflowのパーマリンク(URL)は必ずコードにインラインコメントとしてコピペしておきましょう。
#6. コードのユニットテスト
ユニットテストを行うことで予期せぬエラーを防ぎましょう。
def is_prime(number):
"""Return True if *number* is prime."""
for element in range(number):
if number % element == 0:
return False
return True
構文エラーや論理的エラーなどのエラーを捕捉するために、テスト用の関数を書きましょう(アサーションやテスト用の例外を作る)。この時重要なのは、常に単一のユニット (例:クラス、関数) を分離してテストすることです。コードの他の部分にもエラーがある可能性があるため、その時に単一のユニットでテストを行わないとどこでエラーが発生しているのかの特定ができません。
import unittest
from primes import is_prime
class PrimesTestCase(unittest.TestCase):
"""Tests for 'primes.py."""
def test_is_five_prime(self):
"""Is five successfully determined to be prime?""" self.assertTrue(is_prime(5))
if name == '__main__':
unittest.main()
Pythonの組み込みライブラリ "unittest"は、非常に直感的で使いやすいです。"unittest"は1つ以上のアサーション(self.assertTrue, self.assertIsInstanceなど)で構成されています。また、py.test & noseのような外部フレームワークがあります。
ユニットテストを行うことで、コードを変更した際に発生する予期しないエラーを防ぐことができ、しっかりユニットテストを行うことで、境界条件やエッジケースをチェックし、ロジックを検証します。
優秀なプログラマーは、main関数を書く前にユニットテストを書き、期待される入力や出力、処理しなければならない境界条件について理解します。そうすることで、より良いロジックを考えることができます。