プログラムをクラッシュさせるコードは明らかに間違っていますが、プログラムの問題を発見する手段はクラッシュだけではありません。その他の兆候は、プログラム内のより微妙なバグや読み取り不能なコードを示している可能性があります。ガスのにおいがガス漏れを示したり、煙のにおいが火災を示したりするのと同じように、コードのにおいは潜在的なバグを示すソース コード パターンです。コードの臭いは、必ずしも問題があることを意味するわけではありませんが、プログラムに注意を払う必要があることを意味します。
この章では、いくつかの一般的なコードの匂いをリストします。バグを予測することは、後でバグに遭遇し、理解し、修正するよりも、はるかに時間と労力がかかりません。すべてのプログラマーは、デバッグに何時間も費やして、コードの 1 行を変更するだけで修正できることを発見したという話があります。このため、潜在的な小さなエラーであっても一時停止し、コードの潜在的な問題を再確認して除外するように促す必要があります。
もちろん、コードの匂いが問題になる必要はありません。コードの臭いを修正するか無視するかは、最終的にはあなた次第です。
重複コード
最も一般的なコードの臭いは、コードの重複です。重複コードとは、他のコードをコピーしてプログラムに貼り付けることです。たとえば、この短いプログラムには繰り返しコードが含まれています。ユーザーにどのように感じているかを 3 回尋ねていることに注意してください。
print('Good morning!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good afternoon!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good evening!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
重複コードは、コードの変更が困難になるため問題です。重複コードの 1 つのコピーに加えた変更は、プログラム内のすべてのコピーに適用する必要があります。どこかで変更を加えるのを忘れた場合、または別のコピーに別の変更を加えた場合、プログラムはエラーになる可能性が高くなります。
コードの重複に対する解決策は、コードを重複排除すること、つまり、コードを関数またはループ内に配置して、プログラム内に 1 回出現させることです。以下の例では、繰り返されるコードを関数に移動し、その関数を繰り返し呼び出しています。
def askFeeling():
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good morning!')
askFeeling()
print('Good afternoon!')
askFeeling()
print('Good evening!')
askFeeling()
次の例では、繰り返されるコードをループに移動しました。
for timeOfDay in ['morning', 'afternoon', 'evening']:
print('Good ' + timeOfDay + '!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
関数とループを使用して、2 つの手法を組み合わせることもできます。
def askFeeling(timeOfDay):
print('Good ' + timeOfDay + '!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
for timeOfDay in ['morning', 'afternoon', 'evening']:
askFeeling(timeOfDay)
「おはよう/こんにちは/こんばんは!」メッセージの生成は似ていますが、同一ではないことに注意してください。プログラムの 3 回目の改善では、コードをパラメーター化して、同じ部分の重複を排除しました。また、timeOfDay
パラメーターとtimeOfDay
ループ変数は、別の部分を置き換えます。余分なコピーを削除してこのコードの重複を排除したので、必要な変更を 1 か所で行うだけで済みます。
すべてのコードの匂いと同様に、コードの重複を避けることは、常に従わなければならない厳格で迅速なルールではありません。一般に、繰り返されるコード セグメントが長くなるほど、またはプログラム内に重複コピーが多く出現するほど、重複排除の必要性が高くなります。コードを 1 回または 2 回コピーして貼り付けてもかまいません。ただし、プログラムのコピーが 3 つまたは 4 つある場合は、通常、コードの重複排除を検討します。
コードを繰り返す価値がない場合もあります。このセクションの最初のコード サンプルと最新のコード サンプルを比較します。繰り返されるコードは長くなりますが、シンプルで簡単です。重複排除された例は同じことを行いますが、ループ、新しいループ変数、および名前付きのtimeOfDay
引数を持つ新しい関数が含まれますtimeOfDay
重複コードは、コードを一貫して変更するのが難しくなるため、コード臭です。プログラムに複数のコードが繰り返されている場合、解決策はコードを関数またはループに入れて、1 回だけ表示されるようにすることです。
マジックナンバー
プログラミングに数字が関係するのは当然のことです。しかし、ソース コードに表示される一部の数値は、他のプログラマーを混乱させる可能性があります (または、それらを作成してから数週間後に混乱する可能性があります)。たとえば、次の行の数字を考えてみましょう604800
。
expiration = time.time() + 604800
time.time()
この関数は、現在の時刻を表す整数を返します。expiration
変数が 604,800 秒の時間を表すと想定できます。しかし604800
不思議なことに、この締め切りには何の意味があるのでしょうか? コメントは明確にするのに役立ちます:
expiration = time.time() + 604800 # Expire in one week.
これは良い解決策ですが、さらに優れた解決策は、これらの「魔法の」数を定数に置き換えることです。定数は、最初の割り当て後に値が変更されないことを示すために名前が大文字で書かれている変数です。通常、定数はソース コード ファイルの先頭でグローバル変数として定義されます。
# Set up constants for different time amounts:
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY
`--snip--`
expiration = time.time() + SECONDS_PER_WEEK # Expire in one week.
マジック ナンバーが同じであっても、目的に応じてマジック ナンバーに異なる定数を使用する必要があります。たとえば、カードのデッキには 52 枚のカードがあり、1 年は 52 週間です。しかし、プログラムにこれら 2 つの量がある場合は、次のようにする必要があります。
NUM_CARDS_IN_DECK = 52
NUM_WEEKS_IN_YEAR = 52
print('This deck contains', NUM_CARDS_IN_DECK, 'cards.')
print('The 2-year contract lasts for', 2 * NUM_WEEKS_IN_YEAR, 'weeks.')
このコードを実行すると、出力は次のようになります。
This deck contains 52 cards.
The 2-year contract lasts for 104 weeks.
個別の定数を使用すると、後で個別に変更できます。プログラムの実行中に定数変数の値を変更しないでください。しかし、それは、プログラマーがソース コードでそれらを更新できないという意味ではありません。たとえば、コードの将来のバージョンにワイルド カードが含まれる場合、影響を与えずに定数をweeks
変更できます。cards
NUM_CARDS_IN_DECK = 53
NUM_WEEKS_IN_YEAR = 52
マジック ナンバーという用語は、数値以外の値にも適用されます。たとえば、文字列値を定数として使用できます。次のプログラムを考えてみましょう。このプログラムでは、ユーザーに方向を入力するように求め、方向が北の場合は警告を表示します。入力'nrth'
ミスにより、プログラムは警告を表示できなくなります。
while True:
print('Set solar panel direction:')
direction = input().lower()
if direction in ('north', 'south', 'east', 'west'):
break
print('Solar panel heading set to:', direction)
if direction == 'nrth': # 1
print('Warning: Facing north is inefficient for this panel.')
このバグを見つけるのは困難です:'nrth'
このプログラムはまだ構文的に正しい Python コードであるため、文字列のタイプミスです。プログラムはクラッシュせず、無視しやすい警告メッセージもありません。NRTH
しかし、定数を使用して同じ間違いを犯すと、Python が定数が存在しないことに気付くため、このエラーはプログラムをクラッシュさせます。
# Set up constants for each cardinal direction:
NORTH = 'north'
SOUTH = 'south'
EAST = 'east'
WEST = 'west'
while True:
print('Set solar panel direction:')
direction = input().lower()
if direction in (NORTH, SOUTH, EAST, WEST):
break
print('Solar panel heading set to:', direction)
if direction == NRTH: # 1
print('Warning: Facing north is inefficient for this panel.')
NRTH
タイプミスのあるコード行によってスローされる例外によりNameError
、プログラムを実行するとすぐにエラーが明らかになります。
Set solar panel direction:
west
Solar panel heading set to: west
Traceback (most recent call last):
File "panelset.py", line 14, in <module>
if direction == NRTH:
NameError: name 'NRTH' is not defined
マジック ナンバーはその目的を伝えず、コードを読みにくくし、更新しにくくし、検出できないタイプミスを起こしやすいため、コードの臭いです。解決策は、定数変数を使用することです。
コメントアウトされたコードとゾンビ コード
コードをコメントアウトして実行しないようにすることは、一時的な措置としては問題ありません。他の機能をテストするためにいくつかの行をスキップし、後で追加できるようにコメントアウトすることもできます。しかし、コメントアウトされたコードがまだそこにある場合、なぜそれが削除されたのか、またどのような状況で再び必要になるのかは完全な謎です. 以下の例を見てください。
doSomething()
#doAnotherThing()
doSomeImportantTask()
doAnotherThing()
このコードには多くの未解決の疑問があります: なぜdoAnotherThing()
コメントアウトされているのでしょうか? まだ含めますか?2 番目の呼び出しがコメント化されていないのはなぜですかdoAnotherThing()
? 最初に 2 回呼び出されたのはなぜですかdoAnotherThing()
? またはdoSomeImportantTask()
1 回後にコメントアウトされたのはなぜですか? コメントアウトされたコードを削除しない理由はありますか? これらの質問に対する既成の答えはありません。
ゾンビ コードとは、到達不能または論理的に実行できないコードです。たとえば、関数内のステートメントの後のコード、ステートメント ブロック内のreturn
alwaysFalse
条件付きのコード、関数内の決して呼び出されないコードはゾンビ コードです。if
これを実際に確認するには、対話型シェルで次のように入力します。
>>> import random
>>> def coinFlip():
... if random.randint(0, 1):
... return 'Heads!'
... else:
... return 'Tails!'
... return 'The coin landed on its edge!'
...
>>> print(coinFlip())
Tails!
return 'The coin landed on its edge!'
if
この行は、実行がその行に到達する前にandブロックelse
内のコードが戻るため、ゾンビ コードです。ゾンビ コードは、実際にはコメント アウトされたコードと同じであるにもかかわらず、それを読んだプログラマーがプログラムの有効な部分であると考えるため、誤解を招きます。
スタブは、これらのコード臭の例外です。これらは、まだ実装されていない関数やクラスなど、将来のコードのプレースホルダーです。スタブには、実際のコードの代わりに、pass
何もしないステートメントが含まれています。(別名no-op ) ステートメントのみpass
、言語構文がコードを必要とするスタブを作成できます。
>>> def exampleFunction():
... pass
...
この関数が呼び出されると、何もしません。代わりに、最終的にコードが追加されることを示すだけです。
または、実装されていない関数を誤って呼び出すことを避けるために、raise NotImplementedError
1 つのステートメントでそれをスタブすることができます。これは、関数を呼び出す準備ができていないことをすぐに示します。
>>> def exampleFunction():
... raise NotImplementedError
...
>>> exampleFunction()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in exampleFunction
NotImplementedError
をスローすると、プログラムが誤ってスタブ関数またはメソッドを呼び出すたびに警告が表示されますNotImplementedError
。
コメント アウトされたコードとゾンビ コードは、コードがプログラムの実行可能な部分であるとプログラマーに誤解させるため、どちらもコードのにおいです。代わりに、それらを削除し、Git や Subversion などのバージョン管理システムを使用して変更を追跡してください。バージョン管理については、第 12 章で説明します。バージョン管理を使用すると、プログラムからコードを削除し、必要に応じて後で簡単に追加することができます。
デバッグを印刷
印刷デバッグはprint()
、変数値を表示するために一時的な呼び出しをプログラムに配置してから、プログラムを再実行する方法です。このプロセスは通常、次の手順に従います。
- プログラムのバグに注意してください。
- 一部の変数値を表示するために使用します
print()
。 - プログラムを再実行します。
print()
前のものでは十分な情報が表示されなかったため、さらに を追加しました。- プログラムを再実行します。
- 最終的にエラーを特定する前に、前の 2 つの手順を数回繰り返します。
- プログラムを再実行します。
- 一部を削除するのを忘れたことに気付き
print()
、それらを削除します。
印刷のデバッグは迅速かつ簡単に思えます。しかし、バグを修正するために必要な情報が表示されるまで、プログラムを何度も実行する必要があることがよくあります。解決策は、デバッグを使用するか、プログラムのログ ファイルを設定することです。デバッグを使用すると、一度に 1 行ずつコードを実行し、任意の変数を調べることができます。デバッグを使用すると、単にprint()
呼び出しを挿入するよりも時間がかかるように見えるかもしれませんが、長い目で見れば時間を節約できます。
ログ ファイルには、プログラムに関する多くの情報を記録できるため、プログラムの実行を以前の実行と比較できます。Python では、組み込みlogging
モジュールにより、わずか 3 行のコードで簡単にログ ファイルを作成する機能が提供されます。
import logging
logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('This is a log message.')
モジュールをインポートして基本設定をセットアップしlogging
たら、を使用して情報を画面に表示するlogging.debug()
代わりに、 を呼び出して情報をテキスト ファイルに書き込むことができます。print()
印刷やデバッグとは異なり、呼び出しは、logging.debug()
出力がデバッグ情報であり、出力がプログラムの通常の操作の結果であることが明確にわかります。デバッグの詳細については、オンラインで読むことができる (Automate the Boring Stuff with Python) の第 11 章を参照してくださいautbor.com/2e/c11
。
数値接尾辞付きの変数
プログラムを作成するとき、同じ種類のデータを格納する複数の変数が必要になる場合があります。このような場合は、変数名に数字の接尾辞を追加して、変数名を再利用しようとすることがあります。password1
たとえば、タイプミスを防ぐためにユーザーがパスワードを 2 回入力する必要がある登録フォームで作業している場合、これらのパスワード文字列をおよび というpassword2
変数に格納できます。これらの数字の接尾辞は、変数の内容や変数の違いをうまく説明できません。また、これらの変数がいくつあるかも示していません。1 つpassword3
だけですかpassword4
? 数字のサフィックスを怠惰に追加するのではなく、別の名前を作成してみてください。このパスワードの例では、より適切な名前はpassword
andですconfirm_password
。
別の例を見てみましょう。開始座標と終了座標を処理する関数がある場合、パラメーターx1
、 、y1
、x2
およびがあるとしますy2
。しかし、数字の接尾辞が付いた名前はstart_x
、名前、、、start_y
およびend_x
ほどend_y
多くの情報を伝えません。と変数がx1
とにy1
比べて相互に関連していることも明らかです。start_x
start_y
数値に 2 を超える接尾辞が付いている場合は、リストまたはセット データ構造を使用して、データをセットとして格納することをお勧めします。たとえばpet1Name
、 、pet2Name
、pet3Name
などの値を というリストに格納できますpetNames
。
このコードの匂いは、数字で終わるすべての変数に当てはまるわけではありません。たとえば、enableIPv6
「6」は数字の接尾辞ではなく「IPv6」識別名の一部であるため、変数に という名前を付けてもまったく問題ありません。ただし、一連の変数に数値接尾辞を使用する場合は、それらをリストや辞書などのデータ構造に置き換えることを検討してください。
クラスには関数またはモジュールのみが存在する必要があります
Java などの言語を使用するプログラマーは、クラスを作成してプログラム コードを整理することに慣れています。たとえば、メソッドDice
を持つこのクラスの例を見てみましょうroll()
。
>>> import random
>>> class Dice:
... def __init__(self, sides=6):
... self.sides = sides
... def roll(self):
... return random.randint(1, self.sides)
...
>>> d = Dice()
>>> print('You rolled a', d.roll())
You rolled a 1
これはよく整理されたコードのように見えるかもしれませんが、実際に何が必要かを考えてみましょう: 1 から 6 までの乱数です。クラス全体を単純な関数呼び出しに置き換えることができます。
>>> print('You rolled a', random.randint(1, 6))
You rolled a 6
他の言語と比較して、Python はコードを整理するために場当たり的なアプローチを使用します。これは、Python のコードがクラスや他のボイラープレート構造に存在する必要がないためです。単一の関数呼び出しを行うためだけにオブジェクトが作成される場合、または静的メソッドのみを含むクラスを作成する場合、これらのコードのにおいは、関数の作成に適している可能性があることを示しています。
Python では、関数をグループ化するためにクラスではなくモジュールを使用します。とにかくクラスはモジュール内になければならないので、コードをクラスに入れることはコードに組織の不要なレイヤーを追加するだけです。第 15 章から第 17 章では、これらのオブジェクト指向設計の原則について詳しく説明します。Jack Diederich の PyCon 2012 トーク "Stop Writing Classes" では、Python コードを複雑にする他の方法について説明しています。
ネストされたリストについて
リストは、複雑な数値列を簡潔に表現する方法です。たとえば、5 の倍数をすべて除外して、0 から 100 までの数字の文字列のリストを作成するには、通常、次のループが必要ですfor
。
>>> spam = []
>>> for number in range(100):
... if number % 5 != 0:
... spam.append(str(number))
...
>>> spam
['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17',
`--snip--`
'86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99']
または、リスト内包表記構文を使用して、1 行のコードで同じリストを作成できます。
>>> spam = [str(number) for number in range(100) if number % 5 != 0]
>>> spam
['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17',
`--snip--`
'86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99']
Python は、セットと辞書を含むリストも理解できます。
>>> spam = {
str(number) for number in range(100) if number % 5 != 0} # 1
>>> spam
{
'39', '31', '96', '76', '91', '11', '71', '24', '2', '1', '22', '14', '62',
`--snip--`
'4', '57', '49', '51', '9', '63', '78', '93', '6', '86', '92', '64', '37'}
>>> spam = {
str(number): number for number in range(100) if number % 5 != 0} # 2
>>> spam
{
'1': 1, '2': 2, '3': 3, '4': 4, '6': 6, '7': 7, '8': 8, '9': 9, '11': 11,
`--snip--`
'92': 92, '93': 93, '94': 94, '96': 96, '97': 97, '98': 98, '99': 99}
セット定義では、角括弧の代わりに中括弧を使用して、セット値を生成します。辞書は辞書値を生成し、コロンを使用してリスト内のキーと値を区切ります。
これらの理解は簡潔であり、コードをより読みやすくすることができます。ただし、内包表記は反復可能なオブジェクト (この場合、range(100)
呼び出しによって返されるオブジェクト) からrange
リスト、セット、または辞書を生成することに注意してください。リスト、セット、および辞書はすべて反復可能なオブジェクトです。つまり、次の例のように、リスト内にリストをネストできます。
>>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> nestedStrList = [[str(i) for i in sublist] for sublist in nestedIntList]
>>> nestedStrList
[['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']]
しかし、ネストされたリスト内包表記 (またはネストされた集合内包表記と辞書内包表記) は、多くの複雑さを少量のコードに詰め込み、コードを読みにくくします。リスト内包表記を 1 つ以上のfor
ループに拡張することをお勧めします。
>>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> nestedStrList = []
>>> for sublist in nestedIntList:
... nestedStrList.append([str(i) for i in sublist])
...
>>> nestedStrList
[['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']]
内包表記には複数のfor
式を含めることもできますが、これも読み取り不能なコードを生成する傾向があります。たとえば、次のリスト内包表記は、ネストされたリストからフラット リストを生成します。
>>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> flatList = [num for sublist in nestedList for num in sublist]
>>> flatList
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
このリスト内包表記には 2 つのfor
式が含まれていますが、経験豊富な Python 開発者でも理解するのは困難です。展開されたフォームは 2 つのfor
ループを使用して、同じフラット リストを作成しますが、読みやすくなっています。
>>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
>>> flatList = []
>>> for sublist in nestedList:
... for num in sublist:
... flatList.append(num)
...
>>> flatList
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
リストは構文的に簡潔であり、簡潔なコードを生成できますが、リストを入れ子にするという極端なことはしないでください。
空の例外キャッチ ブロック
例外をキャッチすることは、何か問題が発生した場合でもプログラムを実行し続けることを保証する主な方法の 1 つです。例外が発生しても、それを処理するブロックがない場合except
、Python プログラムはすぐに実行を停止してクラッシュします。これにより、保存されていない作業が失われたり、ファイルが半分完成した状態になったりする可能性があります。
エラーを処理するコードを含むブロックを提供することで、except
クラッシュを防ぐことができます。しかし、エラーの処理方法を決定するのは難しく、プログラマーは単にステートメントを使用してブロックを空のままにしておく可能性がありpass
ますexcept
。たとえば、次のコードではpass
create a except
block を使用していますが、何もしません。
>>> try:
... num = input('Enter a number: ')
... num = int(num)
... except ValueError:
... pass
...
Enter a number: forty two
>>> num
'forty two'
発生したものはステートメントによって処理されるため、このコードは'forty two'
に渡されてもクラッシュしません。しかし、エラーに対して何もしないことは、クラッシュよりも悪い場合があります。プログラムがクラッシュするため、不良データや不完全な状態で実行し続けることはありません。これにより、後でさらに深刻なバグが発生する可能性があります。数字以外の文字が入力されても、コードはクラッシュしません。しかし現在、変数には整数ではなく文字列が含まれているため、変数の使用時に問題が発生する可能性があります。私たちのステートメントは、エラーを処理することよりも、エラーを隠すことを目的としています。int()
int()
ValueError
except
num
num
except
不適切なエラー メッセージで例外を処理することは、別のコード臭です。この例をチェックしてください:
>>> try:
... num = input('Enter a number: ')
... num = int(num)
... except ValueError:
... print('An incorrect value was passed to int()')
...
Enter a number: forty two
An incorrect value was passed to int()
このコードがクラッシュしないのは良いことですが、問題を解決する方法を知るための十分な情報がユーザーに提供されません。エラー メッセージはユーザー向けであり、プログラマ向けではありません。int()
このエラー メッセージには、関数への参照など、ユーザーが理解できない技術的な詳細が含まれているだけでなく、問題の修正方法もユーザーに通知されません。エラー メッセージでは、何が起こったのか、ユーザーが何をすべきかを説明する必要があります。
プログラマーにとっては、ユーザーが問題を解決するために実行できる詳細な手順よりも、役に立たない 1 つの説明をすばやく作成する方が簡単です。ただし、プログラムが考えられるすべての例外を処理できない場合、それは不完全なプログラムであることに注意してください。
コードの匂いの誤解
一部のコードの匂いは、実際にはコードの匂いではありません。プログラミングは、あまり知られていない悪いアドバイスでいっぱいです。それらは文脈から取り除かれたり、有用性を失った後も持続したりします。優れた教師である技術書の著者を非難します。
これらのプラクティスのいくつかはコードのにおいがすると言われたことがあるかもしれませんが、それらのほとんどは良いものです。私はそれらを「誤解されたコードの匂い」と呼んでいます。これらは無視できる警告であり、無視すべきです。それらのいくつかを見てみましょう。
return
誤解: 関数の最後にあるステートメントは1 つだけであるべきです
「ワンイン、ワンアウト」の考え方は、アセンブリと FORTRAN 言語プログラミングの時代からの誤解されたアドバイスから来ています。これらの言語では、サブルーチン (関数のような構造) の途中を含む任意の時点でステップインできるため、サブルーチンのどの部分が実行されたかをデバッグすることが困難になります。関数にはこの問題はありません (実行は常に関数の先頭から開始されます)。しかし、アドバイスは行き詰まり、「関数とメソッドにはreturn
ステートメントを 1 つだけ含める必要があり、それは関数またはメソッドの最後にある必要があります」になりました。
関数またはメソッドごとに 1 つのステートメントを実装しようとすると、return
多くの場合、複雑な一連のif-else
ステートメントが必要になります。これは、return
複数のステートメントを使用する場合よりも混乱を招きます。関数またはメソッドに複数のステートメントを含めることはreturn
問題ありません。
try
神話: 関数には多くても 1 つのステートメントが必要です
通常、「関数とメソッドは 1 つのことを行う必要があります」というのは良いアドバイスです。しかし、これを例外処理を別の関数で行う必要があると解釈するのは行き過ぎです。たとえば、削除したいファイルがもう存在しないかどうかを示す関数を見てみましょう。
>>> import os
>>> def deleteWithConfirmation(filename):
... try:
... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'):
... os.unlink(filename)
... except FileNotFoundError:
... print('That file already did not exist.')
...
このスニペットの支持者は、関数は常に 1 つのことを行う必要があり、エラー処理は 1 つであるため、この関数を 2 つの関数に分割する必要があると主張しています。彼らは、ステートメントを使用する場合try-except
、それは関数の最初のステートメントであり、次のように関数のすべてのコードをカプセル化する必要があると主張しています。
>>> import os
>>> def handleErrorForDeleteWithConfirmation(filename):
... try:
... _deleteWithConfirmation(filename)
... except FileNotFoundError:
... print('That file already did not exist.')
...
>>> def _deleteWithConfirmation(filename):
... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'):
... os.unlink(filename)
...
これは不必要に複雑なコードです。_deleteWithConfirmation()
関数は、直接呼び出されるべきではなく、呼び出しを介して間接的に_
のみ呼び出されるべきであることを示すために、アンダースコア プレフィックスでプライベート マークが付けられるようになりました。handleErrorForDeleteWithConfirmation()
この新しい関数は、削除されたファイルを処理するエラーではなく、ファイルを削除する意図と呼んでいるため、厄介な名前が付けられています。
関数は小さくてシンプルであるべきですが、それは常に「1 つのこと」を実行することに限定されるべきだという意味ではありません (どのように定義しても)。関数に複数のtry-except
ステートメントがあり、それらのステートメントに関数のすべてのコードが含まれていなくてもかまいません。
誤解: フラグ パラメータは不適切です
関数またはメソッド呼び出しに対するブール型パラメーターは、フラグ パラメーターと呼ばれることがあります。プログラミングでは、フラグは「有効」または「無効」などのバイナリ設定を表す値であり、通常はブール値で表されます。True
これらの設定は、有効 (つまり) または無効 (つまり)として記述できますFalse
。
関数呼び出しへのフラグ パラメーターが悪いという誤った考えは、次の例のように、関数がフラグの値に応じて 2 つのまったく異なることを行うという主張に基づいています。
def someFunction(flagArgument):
if flagArgument:
# Run some code...
else:
# Run some completely different code...
実際、関数がこのようになっている場合は、関数のコードのどちらの半分を実行するかをパラメーターで決定するのではなく、2 つの別個の関数を作成する必要があります。しかし、フラグ パラメーターを受け取るほとんどの関数は、これを行いません。たとえば、sorted()
関数のreverse
キーワード引数にブール値を渡して、並べ替え順序を決定できます。sorted()
関数をとという名前の 2 つの関数に分割してもreverseSorted()
、コードは改善されません (必要なドキュメントの量も増えます)。したがって、フラグ パラメータが常に悪いという考えは、コードの匂いに対する誤解です。
神話: グローバル変数は良くない
関数とメソッドは、プログラム内のミニプログラムのようなものです。これらには、関数が戻るときに忘れられるローカル変数を含むコードが含まれています。これは、プログラムの終了後に変数が忘れられた場合と似ています。関数は独立しています。呼び出し時に渡された引数に応じて、コードが正しく実行されるか、エラーが発生して実行されます。
ただし、グローバル変数を使用する関数とメソッドでは、有用な分離が失われます。関数で使用するすべてのグローバル変数は、実際にはパラメーターのように、関数への別の入力になります。パラメータが増えると複雑さが増し、エラーの可能性が高くなります。グローバル変数の間違った値が原因で関数にバグが発生した場合、その間違った値がプログラムのどこにでも設定される可能性があります。このエラー値の考えられる原因を検索するには、関数内のコードまたは関数を呼び出すコード行を分析するだけではなく、プログラム全体のコードを調べる必要があります。したがって、グローバル変数の使用を制限する必要があります。
たとえば、数千行の長さの架空のプログラムpartyPlanner.py
の関数を見てみましょう。calculateSlicesPerGuest()
プログラムのサイズがわかるように、行番号を含めました。
1504\. def calculateSlicesPerGuest(numberOfCakeSlices):
1505\. global numberOfPartyGuests
1506\. return numberOfCakeSlices / numberOfPartyGuests
このプログラムを実行すると、次の例外が発生したとします。
Traceback (most recent call last):
File "partyPlanner.py", line 1898, in <module>
print(calculateSlicesPerGuest(42))
File "partyPlanner.py", line 1506, in calculateSlicesPerGuest
return numberOfCakeSlices / numberOfPartyGuests
ZeroDivisionError: division by zero
プログラムには、return numberOfCakeSlices / numberOfPartyGuests
行が原因でゼロ除算エラーが発生しています。これを引き起こすには変数numberOfPartyGuests
を設定する必要があります0
が、numberOfPartyGuests
この値はどこから取得するのでしょうか? これはグローバル変数であるため、このプログラムの数千行のどこにでも発生する可能性があります! トレースバック情報から、calculateSlicesPerGuest()
架空のプログラムの 1898 行目で呼び出されていることがわかります。numberOfCakeSlices
1898 行を見ると、パラメーターにどのパラメーターが渡されたかがわかります。ただし、numberOfPartyGuests
グローバル変数は、関数呼び出しの前にいつでも設定できます。
グローバル定数は、悪いプログラミング手法とは見なされないことに注意してください。これらの値は変更されないため、他のグローバル変数のようにコードが複雑になることはありません。プログラマーが「グローバル変数は悪い」と言うとき、それは定数変数を意味するものではありません。
グローバル変数は、例外を引き起こす値が設定されている可能性がある場所を見つけるためのデバッグの労力を増やします。これにより、グローバル変数を多用することは悪い考えになります。しかし、すべてのグローバル変数が悪いと考えるのは、コードの匂いの定理です。グローバル変数は、小規模なプログラムで、またはプログラム全体に適用される設定を追跡する場合に役立ちます。グローバル変数を避けることができる場合、それはグローバル変数を避けるべきであることを意味します。しかし、「グローバル変数は良くない」というのは単純すぎる見方です。
誤解: コメントは不要
悪いコメントは、まったくコメントしないよりも悪いです。古い情報や誤解を招く情報を含むコメントは、理解を深めるどころか、プログラマーにとってより多くの問題を引き起こします。しかし、この潜在的な問題は、すべての注釈が悪いと宣言するために使用されることがあります。この見解は、すべてのコメントをより読みやすいコードに置き換えて、プログラムにコメントをまったく含めないようにする必要があると考えています。
コメントは英語 (またはプログラマーが話す言語) で書かれているため、変数、関数、およびクラス名ではできない方法で情報を伝えることができます。しかし、簡潔で効果的なコメントを書くのは難しいです。コメントは、コードと同様に、書き直して何度も繰り返す必要があります。私たちはコードを書くとすぐにコードを理解するので、コメントを書くことは無意味な余分な作業のように思えるかもしれません。その結果、プログラマーはコメントが不要であることを受け入れる傾向があります。
より一般的な経験は、コメントが多すぎるまたは誤解を招くコメントよりも、コメントが少なすぎるかまったくないプログラムです。コメントを拒否することは、「旅客機で大西洋を横断するのは 99.999991% の安全しかないので、泳いで行くつもりだ」と言っているようなものです。
第 10 章では、効果的なコメントの書き方について詳しく説明しています。
要約する
コードのにおいは、コードを記述するためのより良い方法がある可能性があることを示しています。彼らは必ずしも変更を求めるわけではありませんが、あなたに別の見方をする必要があります。最も一般的なコードの匂いはコードの重複です。これは、関数またはループ内にコードを配置する機会を意味する可能性があります。これにより、将来のコード変更は 1 か所で行うだけで済みます。他のコードの匂いには、説明的な名前の定数に置き換えることができるコード内の解釈されない値であるマジック ナンバーが含まれます。同様に、コメント アウトされたコードやゾンビ コードはコンピューターによって実行されることはなく、後でプログラム コードを読むプログラマーを誤解させる可能性があります。後でそれらをプログラムに追加し直す必要がある場合は、それらを削除して、Git などのソース管理システムに依存することをお勧めします。
print_debug は、print()
呼び出しを使用してデバッグ情報を表示します。このデバッグ方法は簡単ですが、長い目で見れば、デバッグとログ記録に頼ってエラーを診断する方が多くの場合は高速です。
x1
、 、x2
などの数字の接尾辞が付いた変数はx3
、通常、リストを含む単一の変数に置き換えるのが最適です。Java などの言語とは異なり、Python ではクラスではなくモジュールを使用して関数をグループ化します。単一のメソッドまたは静的メソッドのみを含むクラスは、コードの臭いであり、クラスではなくモジュールにコードを配置する必要があることを示唆しています。リスト式はリスト値を作成するための簡潔な方法ですが、ネストされたリスト内包表記はしばしば判読できません。
また、空のブロックexcept
で処理される例外はコードの匂いです。エラーを処理するのではなく、エラーを排除するだけです。短く不可解なエラー メッセージは、エラー メッセージがまったくないのと同じように、ユーザーにとって役に立ちません。
これらのコードのにおいの定理に加えて、プログラミングのアドバイスが機能しなくなったり、時間の経過とともに非生産的であることが証明されたりします。これらには、関数ごとに 1 つのreturn
ステートメントまたはtry-except
ブロックのみを配置すること、フラグ パラメーターまたはグローバル変数を使用しないこと、およびコメントを不要と見なすことが含まれます。
もちろん、すべてのプログラミングのアドバイスと同様に、この章で説明するコードの匂いは、プロジェクトや個人の好みに当てはまる場合と当てはまらない場合があります。ベスト プラクティスは客観的な尺度ではありません。より多くの経験を積むにつれて、どのコードが読み取り可能で信頼できるかについてさまざまな結論に達するでしょうが、この章のアドバイスは何を考慮すべきかを概説しています。