検索
連載

[Pythonクイズ]デフォルト引数値の指定方法、カンペキに理解した! ホント?Pythonステップアップクイズ

Pythonの関数ではパラメーターにデフォルト引数値を指定できますが、これが問題の種になることもあります。皆さんはその指定方法をカンペキに理解していますか? それとも?

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「Pythonステップアップクイズ」のインデックス

連載目次

デフォルト引数値の指定が適切ではないものはどれかな?
デフォルト引数値の指定が適切ではないものはどれかな?

【問題】

 以下に示す4つの関数定義にはデフォルト引数値の指定の仕方が適切でないものがある。適切でないものには×を付けてみよう。なお、以下では関数定義の本文にはpass文のみが記述されているが、実際には意味のあるコードが書かれていると考えてほしい。

# 選択肢1
def foo(x, value=0):
    pass


# 選択肢2
def bar(x, items=[]):
    pass


# 選択肢3
def baz(x, name=None):
    pass


# 選択肢4
from datetime import datetime

def qux(x, d=datetime.now()):
    pass

デフォルト引数値の指定が適切ではないものはどれかな?


かわさき

 どうもHPかわさきです。

 前回にここで「問題の画像の左下に難易度を入れるからチェックしてくださいね」と言いましたが、その試みはちょっと変更されて、ここで難易度をお知らせすることにしてみます(今回は)。左下だとコードを表示する領域も少なくなるし、そもそも難易度の入れ方があまりよくなくて、皆さんの目を引かないんじゃないかという意見が編集会議で出てきたんですよね(もっとよく考えてから実施すべきでした。汗)。

 というわけで、難易度を判定してもらいました。今回はお試しということもあって、3つのLLMによる判定の平均値をグラフにしました。それでは発表です。じゃじゃーん。

総合判定は……
総合判定は……

 総合判定は6.7。内訳は次のようになっています。

  • ChatGPT:6/10
  • Claude:6/10
  • Gemini:8/10

 ChatGPTさんは無難な評価。Geminiさんが少しお高めです。そして、Claudeさんがいうところでは、Python初級者にとっては7から8、中級者にとっては3か4。と結構な振り幅だったので、その間を取って6になっています。 今回は難易度を(またもやラクをして)棒グラフとしてしまいましたが、この評価方法もいろいろと考えているところです。


【答え】

 デフォルト引数値の指定が適切でないものは選択肢2と選択肢4です。

 Pythonでは関数のデフォルト引数値として指定した値(式)は、def文で関数を定義する際に一度だけ評価され、その評価結果がそのパラメーターのデフォルト引数値として使われます。これは、デフォルト引数値を持つパラメーターに引数を与えなかった場合、関数を複数回呼び出すと、そのデフォルト引数値が共用されるということです。このことに注意が必要です。

 例えば、選択肢2でitemsパラメーターを省略すると、デフォルト引数値である空のリストが複数回の関数呼び出しで共用されることになります。これが想定外の動作を招くかもしれません。

 また、選択肢4では関数を呼び出すごとにdatetime.now関数が呼び出されることを期待してプログラマーがこのようなコードを書いたのかもしれませんし、コードを読む側もそうした動作であると期待するかもしれません。しかし、datetime.now関数が呼び出されるのは、関数定義のときだけなので、dパラメーターの指定を省略すると、関数定義時の日付と時刻が常に使われてしまいます(これが想定通りなら文句の付けようがありませんが)。

デフォルト引数値の初期化は関数定義時に一度だけ行われることに注意
デフォルト引数値の初期化は関数定義時に一度だけ行われることに注意

 以下ではこのことについて少し詳しく見てみましょう。

【解説】

 既に述べていますが、関数のパラメーターがデフォルト引数値を持つ場合、その値は関数の定義時に一度だけ初期化されます。数値や文字列、Noneのようなイミュータブル(変更不可能)なオブジェクトがデフォルト引数値として与えられている場合、通常は問題となることはないでしょう。選択肢1と選択肢3はそうしたデフォルト引数値の指定例といえます。

 なぜデフォルト引数値がイミュータブルなオブジェクトなら問題ないかというと、デフォルト引数値は(そのパラメーターに引数が与えられなかった場合に)複数回の関数呼び出しの中で共用されるからです。イミュータブルなオブジェクトが共用される限りは、デフォルト引数値の値が変わることはありません。

 例えば、選択肢3のbaz関数が次のような定義だったとします。

def baz(x, name=None):
    if name is not None:
        print(f'hello, {name}. your number is {x}')
    else:
        print('hello')

nameパラメーターが省略されたかどうかで処理を分岐する

 この例ではnameパラメーターが省略されたかどうかで、処理を切り分けています。nameパラメーターの値がイミュータブルなので、このコードが意味するところは常に一定です。

baz(10# hello
baz(100, 'deep'# hello, deep. your number is 100

nameパラメーターの指定の有無で処理が切り替わる

 選択肢1も同様です。valueパラメーターの値が省略されたら、それは常に0が与えられたものと見なされます。

 しかし、ミュータブルなオブジェクトをデフォルト引数値にすると、関数を呼び出すたびにデフォルト引数値の値が変化してしまうかもしれません。例えば、選択肢2のbar関数が次のような定義だったとします。

def bar(x, items=[]):
    items.append(x)
    print(items)

itemsパラメーターが参照するリストにxを追加

 この関数ではitemsパラメーターに渡されたリストにxパラメーターの値を追加して、その結果をコンソールに出力しています。実際の動作を試してみましょう。

bar(10# [10]
bar(20# [10, 20]

これは想定通りの動作?

 ここではitemsパラメーターに引数を渡さずにbar関数を2回呼び出しています。「bar(20)」という呼び出しでは、itemsパラメーターの指定を省略しているので、空のリストに20が追加され、「[20]」と表示されると思うかもしれません。しかし、実際にはそうはならずに「[10, 20]」と出力されています。これはその前の「bar(10)」により、itemsパラメーターのデフォルト引数値であるリストに要素が追加され、そのリストが次の「bar(20)」呼び出しでも共用され、そこに要素が追加されてしまっているから発生したことです。

 関数には「__defaults__」という属性があります。この属性はデフォルト引数値を格納するタプルとなっています。今の状況でこの属性を表示してみましょう。

print(bar.__defaults__)  # ([10, 20], )

デフォルト引数値を調べてみる

 すると、確かにデフォルト引数値として指定したはずの空のリストに要素が追加されていることが分かります。

 デフォルト引数値をミュータブル(変更可能)なオブジェクトとすると、このような「思っていたのとは違う!」という状況を生むことがあります。

 続けて、選択肢4についても考えてみましょう。実はdatetimeオブジェクトはイミュータブルなオブジェクトです。イミュータブルなオブジェクトなら「通常は問題となることはない」と先ほど言いましたが、ここではこれが問題になっています。選択肢4のqux関数が次のようになっていたとします。

from datetime import datetime

def qux(x, d=datetime.now()):
    print(x, d)

xとdの値を出力するだけ

 qux関数を定義した人(筆者です)は、関数呼び出しのたびにdパラメーターの指定を省略したら、その時点でdatetime.now関数が呼び出されるものと考えているのかもしれません。しかし、既に述べた通り、デフォルト引数値として指定した値(式)が評価されるのは関数定義時の一度だけです。これが「なぜでしょう」の答えです。

 そのため、この関数を呼び出すと次のようになります。

qux(10# 10 2025-07-11 10:40:24.643343
qux(20# 20 2025-07-11 10:40:24.643343

dパラメーターの指定を省略すると、常に同じタイムスタンプが表示される

 ご覧の通り、常に同じタイムスタンプが表示されてしまいます。

 今見た2つの問題点は「デフォルト引数値として指定した値(式)は関数定義時に一度だけ評価される」ことが原因です。

  • 複数回の関数呼び出しでリストが共用されないようにする。でも、デフォルト引数値は空のリストにしたい
  • dパラメーターの指定を省略して関数を呼び出したときには、常にdatetime.now関数が呼び出されるようにしたい

 というのであれば、デフォルト引数値の指定の仕方を変えることになります。以下の定義を見てください。

def bar(x, items=None):
    if items is None:
        items = []
    items.append(x)
    print(items)


from datetime import datetime

def qux(x, d=None):
    if d is None:
        d = datetime.now()
    print(x, d)

デフォルト引数値の値をNoneとして、関数本体でそれがNoneかどうかを調べる

 ポイントはデフォルト引数値をNoneとすることと、それがNoneかどうかを調べて、Noneであれば望みの値をパラメーターに代入してやることです。

 こうすれば、リストを関数呼び出しの間で共有したり、関数定義時に初期化された日付や時刻が常に表示されたりすることを防げます。実際に呼び出してみた例を以下に示します。

bar(10# [10]
bar(20# [20]
qux(10# 10 2025-07-11 11:00:39.700859
qux(20# 20 2025-07-11 11:00:39.700907

リストは共有されなくなり、日付と時刻は関数呼び出し時に生成されるようになった

 せっかくなので、bar関数のデフォルト引数値がどうなっているかも見てみましょう。

print(bar.__defaults__)  # (None,)

デフォルト引数値はNoneになっている

 デフォルト引数値としてリストなどのミュータブルなオブジェクトを使いたくなることはよくあります(例えば、再帰関数で計算結果をメモとして使いたい場合など)。そうしたときには、パラメーターのデフォルト引数値としてはNoneを指定して、関数の本体で明示的にその値を初期化することにしましょう。

 また、多くの場合、デフォルト引数値を指定するのに「d=datetime.now()」のように関数呼び出しを含めるのはあまり良い習慣とは言えません。これは、上のコードでも見たように「この関数を呼び出すごとに、datetime.now関数が呼び出される」かのようにコードを読めてしまうからです。もちろん、よく訓練されたPythonプログラマーであれば「怪しい!」と気付くかもしれません。それでも、そう思わせるよりは、デフォルト引数値はNoneとするのがPython流の書き方といえるでしょう。


かわさき

 さて、今回の難易度は6.7ということでしたが、どうでしょう。難しかったですか? そうでもないですか? もっと簡単な方がいいですか? もっと難しい方がいいですか? ご意見、お待ちしっていまっすーーーー。☆やりんごでの表現については、筆者のPhotoshopの腕前が上がるまでお待ちください(自分でやっているんですよ)。

 関数については「Python入門」の以下の記事を参考にしてくださいね。


「Pythonステップアップクイズ」のインデックス

Pythonステップアップクイズ

Copyright© Digital Advantage Corp. All Rights Reserved.

[an error occurred while processing this directive]