«Наименьшее удивление» и изменчивый аргумент по умолчанию - PullRequest
2345 голосов
/ 15 июля 2009

Любой, кто возился с Python достаточно долго, был укушен (или разорван на части) следующей проблемой:

def foo(a=[]):
    a.append(5)
    return a

Новички в Python ожидают, что эта функция всегда будет возвращать список только с одним элементом: [5]. Вместо этого результат совсем другой и очень удивительный (для новичка):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Мой менеджер однажды впервые столкнулся с этой функцией и назвал ее «драматическим недостатком дизайна» языка. Я ответил, что у поведения есть объяснение, лежащее в основе, и оно действительно очень загадочное и неожиданное, если вы не понимаете внутренностей. Однако я не смог ответить (сам себе) на следующий вопрос: в чем причина привязки аргумента по умолчанию при определении функции, а не при выполнении функции? Я сомневаюсь, что опытное поведение имеет практическое применение (кто на самом деле использовал статические переменные в C, без выявления ошибок?)

Редактировать :

Бачек сделал интересный пример. Вместе с большинством ваших комментариев и, в частности, с Утаалом, я уточнил:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Мне кажется, что проектное решение было относительно того, куда поместить область параметров: внутри функции или "вместе" с ней?

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

Фактическое поведение является более последовательным: все, что в этой строке оценивается при выполнении этой строки, что означает определение функции.

Ответы [ 31 ]

0 голосов
/ 10 ноября 2017

Архитектура

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

def a(b=[]):
    pass

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

Эта функция предназначена для двух вещей. Создайте новый список и выполните функцию, скорее всего, в указанном списке.

Функции, которые выполняют две вещи, являются плохими функциями, как мы узнаем из практики чистого кода.

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

Но подождите, вы говорите, мне нравятся мои однострочники.

Ну, угадай, что? Код - это больше, чем просто способ управления поведением оборудования. Это способ:

  • общение с другими разработчиками, работающими над тем же кодом.

  • возможность изменять поведение оборудования при возникновении новых требований.

  • возможность понять ход выполнения программы после повторного ввода кода через два года для внесения изменений, упомянутых выше.

Не оставляйте бомбы замедленного действия для себя, чтобы забрать их позже.

Чтобы разделить эту функцию на две функции, нам нужен класс

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

Выполнено

a = ListNeedsFives()
a.foo()
a.b

И почему это лучше, чем объединять весь вышеприведенный код в одну функцию.

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Почему бы не сделать это?

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

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

Метод foo() не возвращает список, почему бы и нет?

Возвращая автономный список, вы можете предположить, что безопасно делать то, что вам нравится. Но это может быть не так, поскольку он также является общим для объекта a. Принуждение пользователя ссылаться на него как a.b напоминает ему о том, где находится список. Любой новый код, который хочет изменить a.b, естественно будет помещен в класс, которому он принадлежит.

функция подписи def dontdothis(b=None): не имеет ни одного из этих преимуществ.

...