タグ:

Pythonのデフォルト引数、Django O/Rマッパーの落とし穴

こんにちは。 弊社のサーバサイド開発ではPythonをメインの開発言語として採用しています。 また、Webフレームワークとして主にDjangoを採用しています。 Pythonは非常に強力な言語なのですが、日本ではそこまで広まっていないようで、弊社で最近採用したエンジニアにも、昔はRubyやPerlをメインの言語として使っていたという方も多いです。 ここ数日、コードレビューをしていたときに、Pythonあるあるネタなハマりポイントを見つけたので紹介しようと思います。
Python関数のデフォルト値について
Pythonの関数は引数のデフォルト値を指定することができて、デフォルト値が設定されている引数は省略して呼び出すことができますが、実はこのデフォルト値は1度しか評価されない、という罠があります。
>>> def add_x(array=[]):  # array に 'x' を追加。
...   array.append('x')
...   return array
...
>>> add_x()
['x']         # OK
>>> add_x()
['x', 'x']    # ???
arrayのデフォルト値 [] は最初の呼び出し時だけ評価されるので上記のような呼び出し結果になります。 Pythonのチュートリアルにも【重要な警告】として書いてあったりします。 より具体的にはこういうハマり方をします。
>>> def log_message(message, timestamp=datetime.datetime.now()):
...   print timestamp, message
...
>>> log_message('hoge')
2014-03-03 14:14:02.664905 hoge
>>> log_message('fuga')
2014-03-03 14:14:02.664905 fuga  # 呼び出した時刻を表示して欲しいけど、最初の呼び出しの時の時刻が表示されてしまう
  デフォルト値は関数呼び出しの度に評価したい場合、以下のように書いたりします。
>>> def add_x(array=None):
...   if array is None:
...     array = []
...   array.append('x')
...   return array
  より省略してこう書くのもよくあるパターンです。
>>> def add_x(array=None):
...   array = array or []
...   array.append('x')
...   return array
Djangoのコードを書くときに気をつける点
ここまでがPythonあるあるネタでした。 Djangoでは更にハマリポイントが追加されます。 具体的には関数のデフォルト値にQuerysetを指定した場合です。
>>> def count_entries(entries=None):
...   '''ブログのエントリ数をカウント'''
...   entries = entries or Entry.objects.all()
...   return entries.count()
なんの問題も無さそうなコードですが、Entryオブジェクトの数が増えてくるととんでもないことになります。
>>> count_entries(Entry.objects.all())
...
(処理がかえってこない...)
これ、最悪の場合だとサーバがメモリ不足になって落ちてしまいます。 どういうことが起きているか、順を追って説明します。 entries or …. の部分でentriesに対する真偽値評価が行われます。 Queryset には __len__ が定義されているため、len(entries) が0かどうかが評価されますが、ココにある通り、len を呼び出すとentriesのクエリが評価されてしまうため、Entryオブジェクトが作成されることになります。 クエリの結果が数十万行を超える場合、Entryオブジェクトも数十万個作成しようとします。 これには処理時間もメモリも大量に消費してしまうため、処理に非常に時間がかかったり、メモリ不足で落ちてしまうといった結果になってしまうのです。
>>> def count_entries(entries=None):
...   '''ブログのエントリ数をカウント'''
...   if entries is None:
...     entries = Entry.objects.all()
...   return entries.count()
このように書くと len は呼び出されないので問題は回避できます。   関連して最近では、QuerysetやModelインスタンスのスコープはなるべく狭めるようコードを書く習慣づけをしています。 というのも、上述のようにQuerysetオブジェクトはクエリが評価済みかそうでないかが非常に重要なのですが、関数の引数で受け取ってしまうと、わかりにくくなってしまう場合が多いです。 Modelインスタンスの場合も同様で、関数の外でどういう操作が行われクエリが発行されているのか気にしながらコードを書くことになります。 これらは結構難儀な問題に発展する場合が多いので、スコープをなるべく狭め、実行されるクエリの見通しを良くするように心がけています。 O/Rマッパーは非常に強力なツールですが、どこでどういうクエリが発行されるのかを意識せずに開発できてしまうため、商用環境などで大量のデータを取り扱うときにはじめて問題が顕在化してしまうことがよくあります。 チュートリアルだけを読んだ状態だと、こういった罠にはまりやすいですので、発行されるクエリは常に意識して開発するとよいです。   長くなりましたが、まとめると3点です。 ・Pythonの関数のデフォルト値は一度しか評価されないよ ・DjangoのQuerysetを意図せずに評価すると大変なことになるよ ・O/Rマッパーを使っていても、発行されるクエリは常に意識しよう   それでは!   参考URL: http://docs.python.jp/2/tutorial/controlflow.html#tut-defaultargs http://docs.python.jp/2/library/stdtypes.html#truth http://docs.djangoproject.jp/en/latest/ref/models/querysets.html#when-querysets-are-evaluated  
TOP

事業・サービスに
関するお問い合わせ

サービスに関するご相談や
ご質問など、こちらからお問い合わせください。

03-6441-2441 03-6441-2441

受付時間月〜金 10:00〜18:00 ※祝日除く

CONTACT