LoginSignup
5
4

More than 5 years have passed since last update.

Pythonのデフォルト引数は1回しか評価されない問題

Last updated at Posted at 2017-11-07

問題

早速ですが、この記事にたどり着いた方に問題です。以下のmondai.pypythonコマンドで実行した時に出力される数字はいくつでしょうか?

mondai.py
class TrainCar(object):
    def __init__(self, color_type=None, passenger_list=[]):
        self.color_type = color_type
        self.passenger_list = passenger_list


class Train(object):
    def __init__(self):
        self.cars = []

    @property
    def passenger_count(self):
        return sum([len(car.passenger_list) for car in self.cars])


if __name__=="__main__":
    train = Train()
    for i in range(0, 10):
        train_car = TrainCar(color_type="keiyo")
        train_car.passenger_list.append("Taro Yamada")
        train.cars.append(train_car)

    print(train.passenger_count)

上のプログラムでは、2つのクラスが定義されており、それらクラスの関係は Train(電車クラス) has many TrainCar(車両クラス) となっています。また、車両(TrainCarインスタンス) はpassenger_list(乗客リスト) を持てます。

つまり、変数trainは10両編成の電車で、それぞれの車両に"Taro Yamada"さんが1人ずつ乗るわけですね。
で、出力される数字はその合計(電車に乗っている全乗客)人数だから、10 が正解ですね!

・・・と、思っていた時代もありました。

100 という答えにたどり着いた方、おめでとうございます。立派なニシキヘビ使いになることができた自分へのご褒美として、今夜はコタツでハーゲンダッツを食べましょう。😄
あ、安心してください。お代は、この電車に乗っている100人もの"Taro Yamada"さんのうちの誰かがきっと払ってくれます。👍

解説

さて、なぜこんなにも多くの"Taro Yamada"さんが乗車していたのか?

先の問題のプログラムでは、「車両を用意し、そこに"Taro Yamada"さんを1人乗せ」る行為を10回行なっていました。
と日本語で書くと、車両1つにつき"Taro Yamada"さん1人が乗車しているから10両編成だと合計10人であると錯覚します。むしろ、それが自然です。・・・そう、用意した車両に誰も人が乗ってなければね。

実は、Pythonのデフォルト引数は1回しか評価されないのです。
つまり、train_car = TrainCar(color_type="keiyo")としただけではtrain_car.passenger_list = []とはならないのです。

よって、for文の中でtrain_car.passenger_list.append("Taro Yamada")が実行されるたびに、前の車両に乗ってた"Taro Yamada"さんはリセットされないがために、単純に1人ずつ増えていくことになります、なので、1両目のTrainCarインスタンスには1人、2両目には2人・・・となり、10両編成にすると 1+2+...+10 で 55 人となるわけです。が、あれ、まだ違うぞ。。。?
そういえば、Pythonの引数は参照渡しでした。なので、先頭から最後尾までの車両train.cars内のpassenger_listは、全て同じになってしまってます。つまり全車両に10人乗っているので、10*10の 100 人が正解です。ナンテコッタ!😇

新たな疑問とその答え

ここで新たな疑問が生まれました。それは、mondai.py内においてcolor_type="keiyo"の方は評価されているかどうか。という疑問です。

ということで、この疑問を解消するために、mondai.pyを以下のように書き換えて実行してみましょう。

mondai_2.py
class TrainCar(object):
    def __init__(self, color_type=None, passenger_list=[]):
        self.color_type = color_type
        self.passenger_list = passenger_list


class Train(object):
    def __init__(self):
        self.cars = []

    @property
    def passenger_count(self):
        return sum([len(car.passenger_list) for car in self.cars])


if __name__=="__main__":
    train = Train()
    for i in range(0, 10):
        color_type = "keiyo" if i < 6 else "chuo"
        train_car = TrainCar(color_type=color_type)
        train_car.passenger_list.append("Taro Yamada")
        train.cars.append(train_car)

    print(train.passenger_count)
    print([car.color_type for car in train.cars])

7両目以降は色を変えておきましょう。新しく追加する色は、似たものが良いです。"Taro Yamada"さんに不審に思われて乗車拒否されたら計画が狂ってしまいますからね。
そして、実行して結果を確認してみましょう。

100
['keiyo', 'keiyo', 'keiyo', 'keiyo', 'keiyo', 'keiyo', 'chuo', 'chuo', 'chuo', 'chuo']

期待通り、7両目以降のcolor_typekeiyoからchuoになりましたね!
どうやら、引数を指定したcolor_type方は評価されます。👍
(まあ、そうじゃないと困りますが。)

補足

実は、Pythonのチュートリアル 内の 重要な警告 にこの内容が書かれていました。とほほ。。。

追記

2017年11月7日
100が出力される根拠となる計算式が間違っていたので、修正しました。間違いをご指摘くださった方々、感謝申し上げます。
2017年11月10日
解説に「Pythonの引数は参照渡し」であることを追加(参考:Pythonの引数はすべて参照渡し

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4