[コラム] 複雑な計算式

複雑な計算式

一見、複雑に見える計算式でも、一つ一つ丁寧に計算していけば結果を得ることができます。しかし複雑に見える計算式は、コードを解読したくないと思わせてしまいますし、見るだけでも疲れてしまいます。

Pythonに限らず、プログラミング言語では、式の途中結果を変数に代入しつつ計算を進めることができますので、複雑な計算を行う場面でも可読性を上げることができます。ここでは、複雑になってしまった計算式を分かりやすく整理する工程を見ていきたいと思います。

次のような例を見てみましょう。あなたは飲み会の幹事です。飲み会に参加を表明した人が当日全員参加して会費を払ってくれれば問題ないのですが、当日キャンセルしてキャンセル料も払わずに逃げる人がいるかもしれません。その場合の幹事の損失額を計算する式を考えてみました。(※この計算式には、冗長な部分があります。)

損失 = (((参加予定人数-当日キャンセル人数)*会費) + (当日キャンセル人数*キャンセル料)) - (((参加予定人数-当日キャンセル人数)*会費) + (キャンセル料を支払ってくれた人数*キャンセル料))

loss = (((num_expected-num_canceled)*fee_person) + (num_canceled*fee_cancel)) - (((num_expected-num_canceled)*fee_person) + (num_paid_cancel * fee_cancel))

上記計算式は、最終的に"全員が支払いをした時の総額 - 実際に支払われた総額"で、損失額を求めようとしています。

式を整理するために明示的に丸カッコを付けている箇所があるとはいえ、非常にコードが複雑になっていますし、1行の文字数も長いのでコードが読みにくいという問題があります。

計算は、一番内側の丸カッコの部分から順に計算していけば答えを導き出すことはできますし、Pythonは計算結果を正しく出力してくれますが、よく見ると式の中には同じ計算を行っている箇所が複数ありますし、複雑なコードはバグの温床にもなります。そのため、このコードを読みやすく書き換える必要があるでしょう。

上記のような計算式の場合、それぞれの計算の要素を分解してコードを書くことができます。次のようにコードを書くことができます。

# 入力値(例)
num_expected = 50    # 参加予定人数
num_canceled = 5     # 当日キャンセル人数
fee_person = 3000    # 会費(1人あたり)
fee_cancel = 1000    # キャンセル料
num_paid_cancel = 3  # キャンセル料を支払った人数

# 実参加人数
num_attended = num_expected - num_canceled

# 全員が支払いをした場合の、参加費とキャンセル費
ideal_fee_total = num_attended * fee_person
ideal_cancel_total = num_canceled * fee_cancel

# 実際に支払われた参加費とキャンセル費
actual_fee_total = num_attended * fee_person
actual_cancel_total = num_paid_cancel * fee_cancel

# 損失額 (全員が支払いをした時の総額 - 実際に支払われた総額)
loss = (ideal_fee_total+ideal_cancel_total) - (actual_fee_total+actual_cancel_total)
print('損失は', loss, '円です')

このように、一つ一つの計算を分けて記述し、分かりやすい変数名に代入していくことにより、計算順序や、それぞれの計算が何を意味しているのかが分かってきます。また、可読性も向上しているのが確認できるかと思います。このようなメリットがあるので、1行で計算式を書ききるのではなく、行を分けて書くことも非常に重要です。

ちなみに、上記のようにコードを各行に分けて整理してみると、不必要な計算が存在することが見えてきます。上記コードの12行目と16行目に、"num_attended * fee_person"という計算式があります。この計算を2回行う必要はないので1つにまとめます。計算部分のコードを抜き出して整理すると、次のようになります。

# 実参加人数
num_attended = num_expected - num_canceled

# 全員が支払いをした場合の、参加費とキャンセル費
ideal_fee_total = num_attended * fee_person
ideal_cancel_total = num_canceled * fee_cancel
# 実際に支払われたキャンセル費
actual_cancel_total = num_paid_cancel * fee_cancel

# 損失額 (全員が支払いをした時の総額 - 実際に支払われた総額)
loss = (ideal_fee_total+ideal_cancel_total) - (ideal_fee_total+actual_cancel_total)
print('損失は', loss, '円です')

修正後のコードは、"actual_fee_total"変数の計算部分(元の16行目)を削除し、"loss = ~"の行(元の20行目)の"actual_fee_total"変数を"ideal_fee_total"変数に置き換えています。

ここまで書き換えて、"loss = ~"の行の計算式を見てみると、さらに計算を省略できる部分が見えてきます。

loss = (ideal_fee_total+ideal_cancel_total) - (ideal_fee_total+actual_cancel_total)

丸カッコで囲っている部分を、全て取り除いて書いてみます。後半の丸カッコの直前にマイナス記号があるので、後半の丸カッコの中の各変数のプラスマイナスの記号が逆になるので注意してください。

loss = ideal_fee_total + ideal_cancel_total - ideal_fee_total - actual_cancel_total

ここで、"ideal_fee_total"変数が2つ存在することが分かります。計算式の順番を並び替えてみましょう。

loss = ideal_fee_total - ideal_fee_total + ideal_cancel_total  - actual_cancel_total

そうすると、計算式の前半に"ideal_fee_total - ideal_fee_total"という部分ができました。これを計算するとゼロになりますので、計算から除外することができます。つまり、次のように書き直すことができます。

loss = ideal_cancel_total  - actual_cancel_total

どうでしょう。すっきりしましたね。つまり、損失額を計算するためには、実際に支払われた参加費を計算する必要はなく、"お店に支払わなくてはならないキャンセル料"と"キャンセルした人から受け取ったキャンセル料"の差を求めればよかったのです。

それでは、損失額を計算するのに必要な部分のコードだけを残して、整理して書いてみましょう。

# 入力値(例)
num_canceled = 5     # 当日キャンセル人数
num_paid_cancel = 3  # キャンセル料を支払った人数
fee_cancel = 1000    # キャンセル料

ideal_cancel_total = num_canceled * fee_cancel
actual_cancel_total = num_paid_cancel * fee_cancel

# 損失額 ("お店に支払わなくてはならないキャンセル料" - "キャンセルした人から受け取ったキャンセル料")
loss = ideal_cancel_total - actual_cancel_total
print('損失は', loss, '円です')

一番最初のコードと見比べると、かなりスッキリとしたかと思います。このように、複雑なコードを整理すると、不要な計算を取り除くことができることに気づけたり、可読性が向上するなどのメリットがあります。

厳密に言うと、このコードは「(num_canceled - num_paid_cancel) * fee_cancel」という式に短縮できますが、短縮するかどうかは、短縮による利点と可読性のバランスを考慮して判断する必要があります。(今回の場合は短縮してしまっても問題ないでしょう。) コードの中に多少無駄が存在していても、処理スピードにほとんど差がないのであれば可読性を優先すべきで、逆に処理スピードが求められる場面なのに可読性を優先し続けるのも問題です。つまりはバランスです。

プログラムを始めたばかりだと、複雑な計算式を書いた時に高揚することもあるかと思いますが、プログラマーにとって重要なのは可読性を向上させ、誰がみても理解できるコードを書けるようになることです。式が複雑かなと思ったら行を分けるなどしてみたり、他人に意見を求めることを習慣にしてみましょう。

おすすめの記事