Почему синтаксис "mutable default fix fix" настолько уродлив, спрашивает новичок python

Теперь после моей серии "вопросов о новинках python" и на основе другого вопроса.

Prerogative

Перейдите в http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables и прокрутите вниз до "Значения параметров по умолчанию". Там вы можете найти следующее:

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 с этим же примером, на самом деле не говорит "лучше".

Один из способов поставить

Итак, вопрос здесь: почему "хороший" синтаксис по известной проблеме уродливый, как на языке программирования, который способствует "элегантному синтаксису" и "легкий в использовании" ?

изменить

Другой способ поставить

Я не спрашиваю почему и как это происходит (спасибо Mark за ссылку).

Я спрашиваю почему нет более простой альтернативы, встроенной в язык.

Я думаю, что лучший способ, вероятно, мог бы сделать что-то в самом def, в котором аргумент name будет привязан к "локальному" или "новому" внутри изменяемого объекта def. Что-то вроде:

def better_append(new_item, a_list=immutable([])):
    a_list.append(new_item)
    return a_list

Я уверен, что кто-то может прийти с лучшим синтаксисом, но я также предполагаю, что должно быть очень хорошее объяснение, почему это не было сделано.

Ответ 1

Аргументы по умолчанию оцениваются в момент выполнения оператора def, что, вероятно, является наиболее разумным подходом: часто требуется то, что требуется. Если это не так, это может привести к запутывающим результатам, когда среда немного изменится.

Дифференциация с помощью магического метода local или что-то вроде этого далека от идеала. Python пытается сделать вещи довольно простыми, и нет очевидной, ясной замены для текущего шаблона, который не прибегает к возиться с довольно последовательной семантикой, которую в настоящее время имеет Python.

Ответ 2

Это называется "изменчивой ловушкой по умолчанию". См.: http://www.ferg.org/projects/python_gotchas.html#contents_item_6

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

Я думаю, что ответ на вопрос заключается в том, что если вы хотите добавить что-то в список, просто сделайте это, не создавайте функцию для этого.

Это:

>>> my_list = []
>>> my_list.append(1)

Яснее и проще читать, чем:

>>> my_list = my_append(1)

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

Ответ 3

Чрезвычайно конкретный вариант использования функции, которая позволяет произвольно передавать список для изменения, но генерирует новый список, если вы специально не пропускаете его, определенно не стоит синтаксис специального случая, Серьезно, если вы делаете несколько вызовов этой функции, почему бы вам не потребоваться специальный случай первого вызова в серии (путем передачи только одного аргумента), чтобы отличить его от всех остальных (для чего потребуется два аргумента чтобы иметь возможность продолжать обогащать существующий список)?! Например, рассмотрим что-то вроде (предполагая, что 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)

всегда использует два аргумента, избегая повторения и создавая гораздо более простую логику.

Введение синтаксиса специального синтаксиса поощряет и поддерживает специальный случайный вариант использования, и нет особого смысла в поощрении и поддержке этого чрезвычайно своеобразного - существующий, совершенно регулярный синтаксис просто отлично для случая использования крайне редко используется, -).

Ответ 4

Я отредактировал этот ответ, чтобы включить мысли из многих комментариев, размещенных в вопросе.

Приведенный вами пример является ошибочным. Он изменяет список, который вы передаете ему как побочный эффект. Если вы планируете использовать эту функцию, было бы бессмысленно иметь аргумент по умолчанию. Также не имеет смысла возвращать обновленный список. Без аргумента по умолчанию проблема исчезает.

Если целью было вернуть новый список, вам нужно сделать копию списка входных данных. 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 этой новой строке. Аргумент по умолчанию можно использовать снова и снова, потому что он не изменяется, вы сделали его копию в начале.

Ответ 5

Что делать, если вы не говорили о списках, а об AwesomeSets, классе, который вы только что определили? Вы хотите определить ".local" в каждом классе?

class Foo(object):
    def get(self):
        return Foo()
    local = property(get)

мог бы работать, но скоро станет старым, очень быстрым. Довольно скоро шаблон "if a is None: a = CorrectObject()" становится второй натурой, и вы не найдете его уродливым - вы найдете его освещающим.

Проблема не в синтаксисе, а в одной из семантики - значения параметров по умолчанию оцениваются во время определения функции, а не во время выполнения функции.

Ответ 6

Вероятно, вы не должны определять эти две функции как хорошие, так и плохие. Вы можете использовать первый со списком или словарями для реализации модификаций соответствующих объектов. Этот метод может дать вам головные боли, если вы не знаете, как работают изменяемые объекты, но если вы знаете, что вы делаете, все в порядке, на мой взгляд.

Итак, у вас есть два разных метода для передачи параметров, обеспечивающих разные типы поведения. И это хорошо, я бы не изменил его.

Ответ 7

Я думаю, вы смешиваете элегантный синтаксис с синтаксическим сахаром. Синтаксис python ясно показывает оба подхода, просто получается, что правильный подход выглядит менее изящным (с точки зрения синтаксиса), чем неправильный подход. Но так как неправильный подход, хорошо... неправильный, его элегантность не имеет значения. Что касается того, почему что-то вроде вас демонстрирует в better_append, не реализовано, я бы предположил, что There should be one-- and preferably only one --obvious way to do it. превосходит незначительные выгоды в элегантности.

Ответ 8

Это лучше, чем 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 был списком...