Аргументы по умолчанию в Python ведут себя так, как вы ожидаете?

Автор Чип Кент

При разработке программного обеспечения вы должны разрабатывать API и пользовательские интерфейсы, которые следуют Принципу наименьшего удивления (POLA). По сути, вы хотите разработать программное обеспечение, которое ведет себя так, как ожидает большинство пользователей. Удивленный пользователь либо недоволен, либо создает программное обеспечение с ошибками.

Во время недавней разработки Python я потратил час на поиск ошибки. Когда я обнаружил проблему, я был удивлен.
Как вы думаете, что выводит этот код?

def f(a=[]):
  a.append(3)
  return len(a)
print(f())
print(f())
print(f())

Если вы похожи на меня, вы ожидаете увидеть:

На самом деле вы видите:

Как это может быть? Звонок f() эквивалентен звонку f(a=[]).

Небольшое изменение кода иллюстрирует происходящее.

def f(a=[]):
  a.append(3)
  print(f"A={a}")
  return len(a)
print(f())
print(f())
print(f())

Теперь вы можете видеть, что вызов f() не эквивалентен вызову f(a=[])! Один и тот же объект аргумента по умолчанию используется для каждого вызова f(). Это эквивалентно:

def f(a=[]):
  a.append(3)
  return len(a)
b = []
print(f(b))
print(f(b))
print(f(b))

Для меня это нарушает Принцип наименьшего удивления (POLA) и стоило мне часа отладки.

Теперь, когда мы понимаем, что делает Python, функцию можно переписать так, чтобы она вел себя так, как ожидалось.

def f(a=None):
  if a is None:
    a = []
  
  a.append(3)
  return len(a)
print(f())
print(f())
print(f())

Теперь, когда вы знаете, на что обращать внимание, надеюсь, вы сможете избежать той же ловушки. Удивляет ли вас такое дизайнерское решение в Python? Есть ли еще какие-то дизайнерские решения в Python, которые вас удивляют? Дайте нам знать в нашем обсуждении Gitter или сообществе Slack.