正しいループ構成の選択
Pythonはループを行うための様々なコンストラクトを提供します。
この記事では、それらを紹介し、具体的な使用方法についてのアドバイスを提供します。
さらに、あなたのPythonコードにおける各ループ構造のパフォーマンスも見てみましょう。
意外な発見があるかもしれません。
ループ、ループ、ループ
プログラミング言語は、通常、代入、文、ループなど、いくつかの種類の基本要素から構成されています。
ループの背後にある考え方は、ループの本体に記載されている単一のアクションを繰り返すことです。
ループには様々な種類があります。
- 指定された条件が真である限り(while condition do sth.)
- ある条件が満たされるまで(do sh. until condition)
- 一定のステップ数(イテレーション)(for/from ‘x’ to ‘y’ do sth.)
- 無限ループと条件による終了/ブレーク (while condition1 do th. and exit on condition2)
Pythonでサポートされているループの構成
Pythonは上記のような構成要素の一部をサポートしており、さらにこれまで述べてきたような型のユニークな拡張も提供しています。
基本的な「while」ループ
while condition:
statements
条件」が満たされている限り、while
ループのボディにあるすべてのステートメントは少なくとも一度は実行される。
各ステートメントが実行されるたびに、条件が再評価されます。
ループの書き方は次のようになります。
リスト1
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while position < len(fruits):
print(fruits[position])
position = position + 1
print("reached end of list")
このコードでは、リストの要素を次々に出力していきます。
banana
apple
orange
kiwi
reached end of list
whileループとelse` 節の関係
この構文は Python 言語に特有のものですが、非常に便利です。
while condition:
statements
else:
statements
この while
ループは、前に紹介した通常の while
ループと同じように動作します。
この else
部分の文は、条件が成立しなくなるとすぐに実行されます。
例えば、先ほどの例のように、リストの末尾に到達した場合です。
ループの条件が満たされなくなったらthen
と解釈してもよいでしょう。
リスト2
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while position < len(fruits):
print(fruits[position])
position = position + 1
else:
print("reached end of list")
これはリストの要素を次々に出力し、さらに else 節で print
ステートメントによる追加テキストを出力します。
banana
apple
orange
kiwi
reached end of list
このように else
節を使ったループは、条件が満たされない場合にメッセージを出力したり、ステートメントを実行したりするのに便利です。
注意点としては、while
ループから抜け出したり、while
ループの中でエラーが発生した場合には、 else
節は実行されないということです。
無限に続く「while」ループ
無限ループは、常に重要なコンポーネントであり、ブレーク条件が複雑な場合は避けるべきであると教えられています。
しかし、無限ループはコードをエレガントに書くのに役立つ場合があります。
以下は、無限ループの使用例です。
- 無線アクセスポイントのようにネットワーク接続をアクティブに保とうとするデバイス
- ネットワークベースのファイルシステム(NFSやSamba/CIFS)のように、ホストシステムと常にデータを交換しようとするクライアント
- ゲーム状態の描画と更新のためのゲームループ
while True:
if condition:
break
statements
エンドレスループのボディにあるステートメントは、少なくとも一度は実行されることに留意してください。
そのため、ループの先頭から1つ目のステートメントとして、ブレーク条件を記述することをお勧めします。
例題のコードに従うと、無限ループは次のようになります。
リスト3
fruits = ["banana", "apple", "orange", "kiwi"]
position = 0
while True:
if position >= len(fruits):
break
print(fruits[position])
position = position + 1
print("reached end of list")
イテレータを使った for ループ
リストを扱うには、キーワード for
とイテレータを組み合わせて使用します。
擬似コードは次のようになります。
for temp_var in sequence:
statements
このため、リストを処理するPythonのコードは次のように単純化されます。
リスト4
fruits = ["banana", "apple", "orange", "kiwi"]
for food in fruits:
print(food)
print("reached end of list")
このタイプのループ構造では、Pythonインタープリターがリストに対する反復処理を行い、ループがリストの範囲外に出ないように配慮しています。
ループの本体にあるステートメントは、リストの各要素に対して1回ずつ実行されることに注意してください – それが1つであろうと2万個であろうと関係ありません。
リストが空の場合は、ループの本体にあるステートメントは実行されません。
for` ループの中で要素を追加したり削除したりしてリストを変更すると、Python インタープリタを混乱させ、問題を引き起こす可能性がありますので、注意してください。
イテレータと else 節を持つ for ループ
whileループと同様に、Python では
forループにも
elseステートメントが用意されています。
これは、前と同様にthen` として解釈することができます。
擬似コードは以下のようになります。
偽
このキーワードを使用すると、コードは次のようになります。
リスト5
for temp_var in sequence:
statements
else:
statements
サポートされていないループの構成
冒頭で述べたように、多くの異なるループスタイルがあります。
しかし、Pythonはそれらすべてをサポートしているわけではありません。
Python は PHP で知られているような do-until
ループや foreach
ループをサポートしていません。
このような場合は、Pythonの in
演算子を使って解決します。
ループの書き方については、上記の別の方法を参照してください。
どのループを選ぶか?
一般に、while condition
ループは、ループの文の前に条件を指定する必要があります。
そのため、ループの本文の文が一度も実行されないというケースがあります。
また、while
ループの場合、ループが何回実行されるかは必ずしも明らかではありません。
その代わり、for
ループでは、ループ本体のステートメントが何回実行されるかを指定するイテレータに注目します。
イテレートする要素の数が正確に分かっている場合は、 for
ループを使用することが推奨されます。
一方、 while
ループは、ループする要素のリストではなく、評価するためのブール式を持っている場合に適しています。
コードの品質を向上させる
若いプログラマーの多くは、コードの品質についてあまり気にしません。
むしろ、経験豊富な開発者ほど、コードをできるだけ最適化しようとし、CPUの命令数や使用中のメモリセル数を覚えているかもしれません。
では、今日、品質とは何を意味するのでしょうか。
効率性という点では、できるだけ少ない量のコードを書き、必要なプロセッサ命令数だけ効率的にコードを実行することを意味します。
しかし、今日のインタプリタ、ランタイム、フレームワークでは、この2つの指標を適切に計算することは非常に困難です。
重要なのは、このコードがどのくらいの頻度で使用されるのか、そして数マイクロ秒のCPU時間を獲得するためにどれだけの時間を最適化に費やさなければならないのか、という点です。
例として、リストに対して反復処理を行う for
ループを見てみましょう。
通常、次のように記述します。
リスト6
fruits = ["banana", "apple", "orange", "kiwi"]
for food in fruits:
print(food)
else:
print("reached end of list")
これは、0, 1, 2の値を出力します。
range()メソッドは、ループの先頭が評価されるたびにイテラブル
[0, 1, 2]` を生成します。
そのため、以下のように書くとよいでしょう。
リスト7
for entry in range(0, 3):
print(entry)
この例ではあまり最適化されていないように見えますが、もし範囲が0から1,000,000またはそれ以上であった場合を考えてみてください。
リストが大きくなればなるほど、より多くの時間を節約でき、コードの実行速度も速くなります。
さらに、これらの文は while
ループとして表現することができます。
リスト8
entryRange = range(0, 3)
for entry in entryRange:
print(entry)
この時点で、range()
関数を使うのは少し無意味な気がします。
その代わりに、条件分岐のための定数と、条件分岐と印刷のためのカウンタとして index
を使う方がいいかもしれません。
entryRange = range(0, 3)
index = 0
while index < len(entryRange):
print(entryRange[index])
index = index + 1
このような小さな最適化によって、ループのパフォーマンスを少し向上させることができます。
特に、反復処理の数が非常に大きくなった場合に効果があります。
パフォーマンステスト
これまで、私たちはループコードとそれを適切に書く方法について話しました。
パフォーマンステストは、いくつかの光をもたらすのに役立つかもしれません。
このアイデアは Ned Batchelder の興味深いブログ記事 [1] から親切にも借用したものです。
実行されるプログラムコードのパフォーマンステストを行う perf
ツールが使われています [2]。
基本的な呼び出しは perf stat program
で、stat
は統計情報の略、program は評価したい呼び出しです。
ループのバリエーションをテストするために、これらのコールが実行されました。
リスト9
index = 0
while index < 3:
print(index)
index = index + 1
この結果は、Linuxカーネルの負荷の違いにより、10回実行した場合の平均値です。
次の表はその結果を示しています。
| トピック|リスト1|リスト2|リスト3|リスト4|リスト5|。
| — | — | — | — | — | — |
| タスククロック(ミリ秒)| 20.160077|18.535264|15.975387|15.427334|15.503672
| コンテキストスイッチ|10|11|10|13|10|||。
| CPUマイグレーション|0|0|2|1|1|1
| ページフォルト|851|849|855|848|851||。
| サイクル|41,915,010|44,938,837|44,403,696|42,983,392|42,489,206|||||||1,000,000回以上
| 命令数|46,833,820|46,803,187|46,926,383|46,596,667|46,701,350|の順です。
リスト6~8については、以下のようになります。
| トピック|リスト6|リスト7|リスト8|のようになります。
| — | — | — | — |
| タスククロック(ミリ秒)|16.480322|18.193437|15.734627||||||||。
| コンテキストスイッチ|9|11|11|||。
| CPUマイグレーション|0|0|1|||。
| ページフォールト|850|851|853|。
| サイクル|42,424,639|42,569,550|43,038,837||||||。
| 命令数|46,703,893|46,724,190|46,695,710|です。
結論
Pythonはアクションを繰り返したり、ループを書いたりするための様々な方法を提供します。
特定のユースケースごとにバリアントがあります。
我々のテストでは、ループは同じ次元でほとんど差がなく、Pythonインタプリタの最適化は非常に良好であることが示されています。
リンクと参考文献
- [1] Ned Batchelder: print文には何個の命令があるか, July
2013
* [2] Debian パッケージ linux-perf
謝辞
本論文の作成にあたり,Gerold RupprechtとMandy Neumeyerのサポートとコメントに感謝したい。