Почему синтаксис "изменяемый аргумент по умолчанию изменчив" так ужасен, спрашивает новичка Python
Теперь, следуя моей серии "вопросов новичка на Python" и основываясь на другом вопросе.
прерогатива
Перейдите по http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html и прокрутите вниз до "Значения параметров по умолчанию". Там вы можете найти следующее:
def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list
def good_append(new_item, a_list=None):
if a_list is None:
a_list = []
a_list.append(new_item)
return a_list
На этом же примере есть даже "Важное предупреждение" на python.org, хотя на самом деле он не говорит, что "лучше".
Один из способов выразить это
Итак, вопрос здесь такой: почему "хороший" синтаксис по известной проблеме выглядит так ужасно в языке программирования, который поддерживает "элегантный синтаксис" и "простой в использовании"?
редактировать:
Еще один способ выразить это
Я не спрашиваю, почему или как это происходит (спасибо Марку за ссылку).
Я спрашиваю, почему нет более простого альтернативного встроенного языка.
Я думаю, что лучший способ, вероятно, быть в состоянии сделать что-то в def
сам, в котором аргумент имени будет присоединен к "локальному" или "новому" внутри def
изменчивый объект Что-то вроде:
def better_append(new_item, a_list=immutable([])):
a_list.append(new_item)
return a_list
Я уверен, что кто-то может прийти с лучшим синтаксисом, но я также предполагаю, что должно быть очень хорошее объяснение, почему это не было сделано.
8 ответов
Аргументы по умолчанию оцениваются во время def
оператор выполняется, что является, пожалуй, самым разумным подходом: часто это то, что нужно. Если бы это было не так, это могло бы привести к сбивающим с толку результатам, когда среда немного изменилась.
Дифференцировать с помощью магии local
метод или что-то подобное далеко от идеала. Python пытается сделать вещи довольно простыми, и нет никакой очевидной, ясной замены для текущего шаблона, который не прибегает к смешиванию с довольно последовательной семантикой, которую в настоящее время имеет Python.
Это называется "изменяемой ловушкой по умолчанию". Смотрите: http://www.ferg.org/projects/python_gotchas.html.
В принципе, a_list
инициализируется при первой интерпретации программы, а не каждый раз, когда вы вызываете функцию (как вы могли ожидать от других языков). Таким образом, вы не получаете новый список каждый раз, когда вызываете функцию, но вы используете тот же самый.
Я предполагаю, что ответ на вопрос заключается в том, что если вы хотите добавить что-то в список, просто сделайте это, не создавайте функцию для этого.
Это:
>>> my_list = []
>>> my_list.append(1)
Это яснее и проще для чтения, чем:
>>> my_list = my_append(1)
В практическом случае, если вам нужно такое поведение, вы, вероятно, создадите свой собственный класс, который имеет методы для управления его внутренним списком.
Исключительно специфический вариант использования функции, которая позволяет вам при желании передать список для изменения, но генерирует новый список, если вы специально не передаете его, определенно не стоит синтаксиса особого случая. Серьезно, если вы выполняете несколько вызовов этой функции, зачем вам нужен специальный случай первого вызова в серии (с передачей только одного аргумента), чтобы отличить его от любого другого (который будет нуждаться в двух аргументах? чтобы иметь возможность продолжать обогащать существующий список)?! Например, рассмотреть что-то вроде (при условии, конечно, что betterappend
сделал что-то полезное, потому что в текущем примере было бы безумно называть это вместо прямой .append
!-):
def thecaller(n):
if fee(0):
newlist = betterappend(foo())
else:
newlist = betterappend(fie())
for x in range(1, n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist)
это просто безумие, и, очевидно, должно быть, вместо
def thecaller(n):
newlist = []
for x in range(n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist)
всегда использовать два аргумента, избегая повторения и создавая гораздо более простую логику.
Введение синтаксиса в особом случае поощряет и поддерживает особый случай использования, и в действительности нет особого смысла поощрять и поддерживать этот чрезвычайно специфический случай - существующий, совершенно регулярный синтаксис отлично подходит для крайне редких хороших вариантов использования;).
Я отредактировал этот ответ, чтобы включить мысли из множества комментариев, опубликованных в вопросе.
Пример, который вы приводите, ошибочен. Он изменяет список, который вы передаете, как побочный эффект. Если вы намерены работать с этой функцией, не имеет смысла иметь аргумент по умолчанию. Также не имеет смысла возвращать обновленный список. Без аргумента по умолчанию проблема исчезнет.
Если вы намеревались вернуть новый список, вам необходимо сделать копию списка ввода. Python предпочитает, чтобы все было ясно, поэтому вы должны сделать копию.
def better_append(new_item, a_list=[]):
new_list = list(a_list)
new_list.append(new_item)
return new_list
Для чего-то немного другого, вы можете сделать генератор, который может принимать список или генератор в качестве входных данных:
def generator_append(new_item, a_list=[]):
for x in a_list:
yield x
yield new_item
Я думаю, вы ошибочно полагаете, что Python по-разному обрабатывает изменяемые и неизменяемые аргументы по умолчанию; это просто неправда. Скорее неизменность аргумента заставляет вас изменять ваш код тонким способом, чтобы делать правильные вещи автоматически. Возьмите свой пример и примените его к строке, а не к списку:
def string_append(new_item, a_string=''):
a_string = a_string + new_item
return a_string
Этот код не изменяет переданную строку - он не может, потому что строки неизменяемы. Он создает новую строку и назначает a_string для этой новой строки. Аргумент по умолчанию можно использовать снова и снова, потому что он не меняется, вы сделали его копию в начале.
Что если вы говорите не о списках, а об AwesomeSets, классе, который вы только что определили? Вы хотите определить ".local" в каждом классе?
class Foo(object):
def get(self):
return Foo()
local = property(get)
может работать, но очень быстро стареет, очень скоро. Довольно скоро паттерн "если a есть None: a = CorrectObject()" становится второй натурой, и вы не найдете его уродливым - вы найдете его освещающим.
Проблема не в синтаксисе, а в семантике - значения параметров по умолчанию оцениваются во время определения функции, а не во время выполнения функции.
Вероятно, вы не должны определять эти две функции как хорошие и плохие. Вы можете использовать первый со списком или словарями, чтобы реализовать на месте модификации соответствующих объектов. Этот метод может причинить вам головную боль, если вы не знаете, как работают изменяемые объекты, но если вам известно, что вы делаете, это нормально, на мой взгляд.
Таким образом, у вас есть два разных метода для передачи параметров, обеспечивающих различное поведение. И это хорошо, я бы не стал его менять.
Это лучше, чем good_append(), IMO:
def ok_append(new_item, a_list=None):
return a_list.append(new_item) if a_list else [ new_item ]
Вы также можете быть очень осторожны и проверить, что a_list был списком...
Я думаю, что вы путаете элегантный синтаксис с синтаксическим сахаром. Синтаксис python четко связывает оба подхода, просто случается так, что правильный подход выглядит менее элегантным (с точки зрения синтаксиса), чем неправильный подход. Но поскольку неправильный подход, хорошо... неправильный, его элегантность не имеет значения. Что касается того, почему что-то, как вы демонстрируете в better_append, не реализовано, я бы сказал There should be one-- and preferably only one --obvious way to do it.
козыри незначительные выгоды в элегантности.