第8回 適切な割り勘をレコメンドする

9. もっと気の利いた割り勘をする

■要件

前回のプログラムでは1,000円単位だったので、他の人に3,000円ずつ払わせて、幹事(自分)は400円でした。なかなかオイシイですが、ちょっと気まずいですね。
こんなときは500円単位、それでも差が大きければ100円単位にしていくのではないでしょうか。
今回は、まず1,000円単位で割り勘をして他の人と幹事の支払い額の差が500円以上であれば500円単位、100円単位と細かくしていきます。最後のひとつだけ表示することにします。

ソースコード

import math

total = int(input("合計金額: "))
num = int(input("人数: "))
per_capita = math.ceil(total / num / 1000) * 1000
surplus = total - per_capita * (num - 1)
if per_capita - surplus < 500:
    print("1,000円単位では、一人あたり" + str(per_capita) + "円で、余りは" + str(surplus) + "円です。")
else:
    per_capita = math.ceil(total / num / 500) * 500
    surplus = total - per_capita * (num - 1)
    if per_capita - surplus < 500:
        print("500円単位では、一人あたり" + str(per_capita) + "円で、余りは" + str(surplus) + "円です。")
    else:
        per_capita= math.ceil(total / num / 100) * 100
        surplus = total - per_capita * (num - 1)
        print("100円単位では、一人あたり" + str(per_capita) + "円で、余りは" + str(surplus) + "円です。")
合計金額: 12400
人数: 5
500円単位では、一人あたり2500円で、余りは2400円です。
合計金額: 11600
人数: 4
1,000円単位では、一人あたり3000円で、余りは2600円です。

■解説

0. このプログラムを理解するのに必要な知識
  1. if文のネスト
1. 複数の条件分岐を重ねる

今回のプログラムでは、大きな単位で割り勘してみて、ダメだったら細かい単位を試すというロジックになっています。この場合、以前使ったif~elif~else構文では表現できません*1
このようにif文(今回はelse節)の中にif文を入れることをネスト(入れ子)構造といいます。for文でもネスト構造はよく用いられます。
ネストするには、ifブロックの中にifブロックを作るのでさらに深い(行頭スペースが多い)インデントを作ります。
f:id:inato_gen:20190301091737j:plain

10. 自作関数を作って再利用できるようにする

■要件

このプログラムでも要件は満たしますが、よく見ると同じような処理を何度も行っています。
この同じような処理を部品化して保守性の高いコードにしてみましょう。

ソースコード

import math

def split_check(total, num_member, unit):
    per_capita = math.ceil(total / num_member / unit) * unit
    surplus = total - per_capita * (num_member - 1)
    return [per_capita, surplus]

def display(unit, per_capita, surplus):
    message = "{:,}円単位では、一人あたり{:,}円で、余りは{:,}円です。"
    print(message.format(unit, per_capita, surplus))

total = int(input("合計金額: "))
num = int(input("人数: "))
for unit in [1000, 500, 100]:
    per_capita, surplus = split_check(total, num, unit)
    if per_capita - surplus < 500:
         break
display(unit, per_capita, surplus)

■解説

0. このプログラムを理解するのに必要な知識
  1. def文で関数の作成
  2. 引数を持つ関数
  3. return文で返り値を返す
  4. 文字列のフォーマット
  5. break文でループを抜ける
1. 自作関数の定義

今まで、print関数やint関数、random.randint関数など、python組み込みのものを使ってプログラムを作ってきました。こうした関数と同じように独自の処理を関数にすることができます。

def 関数名():
   処理(関数本体ブロック)

関数の定義はdef文を使って行います。

  • 関数名は英大文字小文字、アンダースコア「_」、数字(ただし、数字で始まる関数名はNG)
  • 関数名の後ろに丸カッコ「( )」
  • 次の行からコードブロックで処理内容を定義
  • 関数名()で呼び出し
# 自作関数の定義
def my_func():
    print("my_func関数が実行されました")

# 自作関数の呼び出し
my_func()
my_func関数が実行されました

プログラムは基本的に上から実行されますが、def文に関しては関数が定義され、呼び出し可能になるだけで実際には動作しません。関数の中身が実行されるのは、呼び出された後です。
f:id:inato_gen:20190305162852j:plain

2. 引数を受ける関数を作成する

int関数のように引数を受け付けて動的に動く関数の場合、関数定義の丸カッコ中に受ける引数名を列挙します。

def 関数名(引数1, 引数2, ...):
    関数本体

関数定義の中で受ける引数名を特に仮引数(かびきすう)と言い、関数の中では変数として振る舞います。

def my_func2(x):
    print("my_func2関数が実行されました")
    print(str(x) + "を2倍すると" + str(x * 2))

a = 100
my_func2(a)
my_func2関数が実行されました
100を2倍すると200

f:id:inato_gen:20190306084017p:plain

3. 戻り値の返却

処理中にreturn文を書くと戻り値を呼び出し元に返し、関数呼び出しの次の行からプログラムが再開されます。(「値を返す」とは関数呼び出し部がその値に置き換わることと同じでした。)

def my_func3():
    print("my_func3関数が実行されました")
    return 3
    print("このコードは実行されません")

a = my_func3()
print(a)
my_func3関数が実行されました
3
4. 変数の値を書式変換する

文字列に変数の値を埋め込む場合、文字列の連結でも実現できますが、以下のように値の展開をすることもできます。

文字列.format(値 ,...)

書式化された文字列を返します。
文字列には波カッコ「{}」を埋め込むことで、format関数に渡した値をその部分に展開できます。

a = 1000
temp = "変数aの値は{}です"
msg = temp.format(a)
print(msg)
変数aの値は1000です

このformat関数は、random.randint関数のように外部モジュールの関数の呼び出しと似ていますが、最初に来るのが、モジュール名ではありません。上記サンプルでは変数名ですが、文字列であればいいので、変数名である必要もなく

print("変数aの値は{}です".format(1000))

としても同じ結果になります。
またこの際に波カッコの中に特別な指定をすることで変数の値を書式に従って変換することができます。

処理 書式 記述例 結果
桁区切り {:,} "{:,}".format(1000) 1,000
右揃え {:埋める文字*2>揃える幅 "{:>5".format(123) 123
16進数表記 {:x} "{:x}". format(65535) ffff
5. forループを途中で抜ける

for文を使って繰り返し処理ができることはすでに習得しました。(よね?)
今回のケースでは、1000円単位、500円単位、100円単位と割り勘処理を繰り返しますが、うまく割り勘できたらそれ以降の処理は要らないものとしました。具体的には、幹事と他の人の支払い額の差が500円未満の場合です。
繰り返し処理を抜けるにはbreak文を使います。もちろんうまく割り勘できていないケースでは繰り返し処理(割り勘にトライすること)を続ける必要があるので、if文で判定して満たした場合のみbreak文を実行します。
f:id:inato_gen:20190307224625p:plain

*1:書きようによってはできますが、人がやるときと同じようなロジックで考えています

*2:省略するとスペース