コーディング中に避けるべき6つのPythonの間違い
新しいプログラマーとして、私たちは皆間違いを犯してきました。しかし、プログラミング言語の特性を知らないために発生する間違いもあります。そこで、Pythonでプログラミングをする際に犯しがちな最も一般的な間違いをいくつかまとめました。
6. イテレーション中にリストを修正する
ループ中にPythonリストを変更することは、賢いショートカットのように思えるかもしれませんが、これは典型的なPythonの落とし穴です!この巧妙なミスは、要素をスキップしたり、完全にめちゃくちゃなリストになってしまったりと、予期しない動作につながることがあります。
数字のリストがあるとしましょう。そこから5未満の数字をすべて削除したいと思います。簡単そうですよね?
numbers = [1, 2, 6, 3, 7, 4, 8, 5]
print("Original list:", numbers)
for number in numbers:
if number < 5:
numbers.remove(number)
print("Modified list (oops!):", numbers)
このコードを実行すると、2という数字がリストに残っていることがわかります。
しかし、なぜでしょうか?このコードでは、numbersを繰り返し処理して、5未満の要素を削除しようとしています。しかし、要素を削除するとリストが縮み、後続要素のインデックスがずれます。
これにより、ループが要素をスキップする可能性があります。例えば、1が削除されると、2は1の古い位置に移動しますが、ループの内部カウンタはすでに進んでいるため、2をスキップする可能性があります。
リストの要素に基づいてリストを変更する最も安全で一般的な方法は、リストのコピーを反復処理するか、保持したい要素だけを含む新しいリストを構築することです。コピーアプローチでは、反復処理の前にリストの浅いコピーを作成します。この方法により、元のリストから要素を削除しても反復処理には影響しません。
numbers = [1, 2, 6, 3, 7, 4, 8, 5]
print("Original list:", numbers)
for number in numbers[:]: # Iterate over a slice (copy) of the list
if number < 5:
numbers.remove(number)
print("Modified list (fixed with copy):", numbers)
numbers[:]を使用することにより、numbersリストのスライスを作成します。これは浅いコピーです。その後、forループはこのコピーを繰り返し処理し、元のnumbersリストに加えた変更とは完全に独立しています。
しばしば、これを達成するよりPythonicな方法は、保持したいアイテムのみを含む新しいリストを作成することです。このアプローチは、インプレースでの変更を回避し、しばしばより効率的です。
numbers = [1, 2, 6, 3, 7, 4, 8, 5]
print("Original list:", numbers)
filtered_numbers = [number for number in numbers if number >= 5]
print("Filtered list (new list method):", filtered_numbers)
このコードでは、リスト内包表記を使用して、元のリストから必要な要素を含む新しいリストを作成します。これにより、元のリストはそのまま保たれます。
5. 名前の衝突
変数やファイル名の選択は、小さな詳細のように思えるかもしれませんが、Pythonが振り向いてあなたを噛む(比喩的に)ことがあります。名前を適切に付けないことは、奇妙なバグ、隠れた機能、そして数時間の混乱を引き起こす可能性があります。このような事態が起こる一般的な方法は2つあります:Pythonのキーワードや組み込み名を自分の名前として使用すること、そして誤って既存のPythonモジュールの名前でスクリプトに名前を付けることです。
Python は変数に list、str、あるいは sum と名付けることを妨げません。しかし、できるからといって、すべきであるというわけではありません。
list = [1, 2, 3]
print(list)
このコードは問題なく実行されます。しかし、Pythonでlist()関数を含む別の行を追加すると、エラーが発生します。
list = [1, 2, 3]
print(list)
list = list('123')
それは、あなたが独自のリストオブジェクトで組み込みの list() 関数を上書きしたからです。list('123') を呼び出そうとすると、Pythonはあなたが自分のリストを呼び出そうとしていると考え、組み込み関数を呼び出しているのではありません。したがって、Python のコア機能と衝突しないように、常に説明的な名前を使用してください。
numbers = [1, 2, 3]
converted = list('123')
これは予約語の使用を避けるのに役立つだけでなく、コードの可読性やデバッグのための良い習慣でもあります。同様に、スクリプトファイルの名前付けについても同じことが言えます。例えば、ランダムな数を実験するために random.py という名前のファイルを作成する場合:
# random.py
import random
print(random.randint(1, 10))
しかし、これを実行すると、「モジュール 'random' に属性 'randint' がありません」というエラーが表示される可能性が高いです。Pythonは標準ライブラリからの実際の random モジュールではなく、あなた自身のファイル (random.py) をインポートしてしまっています。典型的なシャドウイングバグです。
既存のモジュール(例:random.py、math.py、os.py、またはjson.py)と同じ名前のスクリプトを命名するのは避けてください。そして、もしうっかりそうしてしまった場合は、名前を変更した後に対応する.pycや__pycache__フォルダーを削除することを忘れないでください。
4. 可変関数引数
Pythonでは関数の引数にデフォルト値を設定できるため、とても便利です。しかし、そのデフォルト値がミュータブル(リストや辞書のように)であると、巧妙なバグの罠に陥ることになります。
デフォルトの可変オブジェクトは、関数が定義されるときに1回だけ作成され、呼び出されるたびには作成されません。つまり、それが変更されると、その変更は次の呼び出しのために残ります。
def add_item_to_list(item, item_list=[]):
item_list.append(item)
return item_list
print(add_item("apple"))
print(add_item("banana"))
このコードを実行すると、add_item_to_list()への各呼び出しが、リストを指定しない場合、新しく空のリストから始まることを期待するかもしれません。しかし、出力には予期しないものが表示されます:
私たちはその関数を2回呼び出しました。では、なぜ2回目の呼び出しでも"apple"がまだそこにあるのでしょうか?リストitem_listは一度だけ作成されるため、関数へのすべての呼び出しは同じリストを変更し続けます。これにより、驚くべきかつ非常にバグの多い動作が発生します。これを修正するために、デフォルトとしてNoneを使用し、関数の内側でリストを作成することができます:
def add_item_to_list(item, item_list=None):
if item_list is None:
item_list = []
item_list.append(item)
return item_list
print(add_item("apple"))
print(add_item("banana"))
デフォルトでNoneを指定すると、関数が呼び出されるたびに新しいリストが作成されます。
3. 誤った方法でファイルを扱う
Pythonでファイルを扱うのはとても簡単です。しかし、コードに潜り込む可能性のある一般的な間違いがあります。それはファイルを閉じないことです。一見無害に思えるかもしれませんが、ファイルを開いたままにしておくと、メモリリークやファイルの破損、他のプログラムがアクセスできないロックされたファイルにつながることがあります。
file = open('data.txt', 'r')
content = file.read()
print(content)
# Forgot to close the file!
ファイルを開いたままにしておくと、深刻なトラブルを引き起こす可能性があります。他のプロセスによるファイルの削除や変更を妨げることさえあります。そのため、作業が終わった後は必ずファイルを閉じるようにしましょう。close()関数を使用してこれを行うことができます。
file = open('data.txt', 'r')
content = file.read()
print(content)
file.close()
別のベストプラクティスは、Pythonのwithステートメントを使用することであり、これは自動的にファイルを閉じる処理を行います。
with open('data.txt', 'r') as file:
content = file.read()
print(content)
withブロックは、すべての裏でのクリーンアップを処理します。ブロックを抜けるとすぐに、Pythonがファイルを閉じてくれます。これにより、自分でファイルを手動で閉じる心配をする必要がなくなります。
2. モジュールからすべてをインポートする
初心者としては、スクリプトの最初に from module import * を追加して終わらせたくなるものですが、コードが大きくなるにつれて、このショートカットは災難の元になります。
from math import *
from custom_math import *
print(sqrt(16))
もし math と custom_math の両方が sqrt() という関数を定義していたら、どちらが呼び出されるのでしょうか?Pythonはそれを教えてくれません。ただ、前のものを静かに上書きしてしまいます。
あなたはそれに気づかずに全く異なる関数を使っている可能性があります。さらに、大規模なコードベースでは、デバッグは悪夢のようになります。関数やクラスがどこから来ているのか分からなくなります。だからこそ、インポートに関しては明示的であるべきです。
import math
import custom_math
print(math.sqrt(16))
print(custom_math.sqrt(16))
あるいはこんな風に:
from math import sqrt as m_sqrt
from custom_math import sqrt as c_sqrt
print(m_sqrt(16))
print(c_sqrt(16))
明示的なものは暗黙的なものよりも良い(PythonのZenの核心的な考え方)。あなたは常に自分が何を使っているのか、そしてそれがどこから来たのかを知っています。
1. 適切な例外処理
どんなに慎重に作られたコードでも、予期しない問題に直面することがあります。これらはPythonでは例外と呼ばれています。それらをうまく処理することが、プログラムを堅牢にするのです。しかし、多くの初心者は、幅広いexcept句を使用して「すべてをキャッチする」考え方で例外処理にアプローチします。それは今のところエラーを無視するかもしれませんが、コードを脆弱にし、混乱させ、デバッグを難しくしてしまいます。
try:
result = 10 / 0
except:
print("Something went wrong!")
これはすべてをキャッチします。予期しないエラーや処理したくないエラーも含めて。構文エラー、キーボード割り込み、タイプミス、壊れたインポート。Pythonはその一般的なexceptでそれらすべてを飲み込みます。しかし、これは理想的ではありません。代わりに、キャッチする内容について具体的であるべきです:
try:
result = 10 / 0
except ZeroDivisionError:
print("You can't divide by zero!")
あなたは期待しているエラーだけを捕捉しています。他に何かが間違っている場合、Pythonはそれを依然として発生させます。したがって、実際のバグを黙って無視することはありません。また、複数の例外を賢く処理することもできます:
try:
value = int(input("Enter a number: "))
result = 10 / value
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("You can't divide by zero!")
単独のexcept:を避けてください。すべての例外をキャッチする必要がある場合(稀ですが)、except Exception:を使用し、それでも慎重に取り扱ってください。特定の例外を処理することで、コードはより予測可能になり、デバッグが容易になり、より意味のあるフィードバックを提供できるようになります。
これらのミスを避けることで、エラーが少なくなり、Pythonコードのデバッグが少なくて済むようになります。Pythonを始めたばかりでリソースを探している場合、Pythonを教えるゲームが多くあります。それ以外にも、費用追跡ツールのようなプロジェクトや基本的なスクリプトを作成することもできます。




