Хорошее применение для значений по умолчанию изменяемых аргументов функции?

84

В Python распространенной ошибкой является установка изменяемого объекта в качестве значения аргумента функции по умолчанию. Вот пример из прекрасной статьи Дэвида Гуджера :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

Объяснение, почему это происходит, здесь .

А теперь вопрос: есть ли хороший вариант использования этого синтаксиса?

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

Джонатан
источник
1
Лучшее объяснение, которое я знаю для этого, находится в связанном вопросе: функции - это объекты первого класса, как и классы. Классы имеют изменяемые данные атрибутов; функции имеют изменяемые значения по умолчанию.
Катриэль
10
Такое поведение не является «дизайнерским выбором» - это результат того, как работает язык - начиная с простых принципов работы с минимально возможным количеством исключений. В какой-то момент для меня, когда я начал «думать на Python», такое поведение стало естественным - и я был бы удивлен, если бы этого не произошло
jsbueno
2
Я тоже задавался вопросом. Этот пример распространен по всему Интернету, но он просто не имеет смысла - либо вы хотите изменить переданный список, а значение по умолчанию не имеет смысла, либо вы хотите вернуть новый список, и вам следует немедленно сделать копию при входе в функцию. Не могу представить, чтобы было полезно делать и то, и другое.
Mark Ransom
2
Я только что наткнулся на более реалистичный пример, в котором нет проблемы, на которую я жалуюсь выше. По умолчанию это аргумент __init__функции класса, который устанавливается в переменную экземпляра; это совершенно правильная вещь, и все идет ужасно неправильно с изменяемым значением по умолчанию. stackoverflow.com/questions/43768055/…
Марк Рэнсом

Ответы:

61

Вы можете использовать его для кеширования значений между вызовами функций:

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

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

Дункан
источник
12
... или запоминающийся декоратор.
Дэниел Роузман,
29
@functools.lru_cache(maxsize=None)
Катриэль
3
@katrielalex lru_cache является новым в Python 3.2, поэтому не все могут его использовать.
Дункан
2
К вашему сведению, теперь есть backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
Panda
1
lru_cacheнедоступен, если у вас есть нехэшируемые значения.
Synedraacus
14

Канонический ответ - это страница: http://effbot.org/zone/default-values.htm

Также упоминаются 3 «хороших» варианта использования изменяемого аргумента по умолчанию:

  • привязка локальной переменной к текущему значению внешней переменной в обратном вызове
  • кеш / мемоизация
  • локальное повторное связывание глобальных имен (для высоко оптимизированного кода)
Петр М. - расшифровывается как Моника
источник
12

Возможно, вы не изменяете изменяемый аргумент, но ожидаете изменяемый аргумент:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(Да, я знаю, что вы можете использовать config=()в этом конкретном случае, но я считаю это менее ясным и менее общим.)

Восстановить Монику
источник
3
Также убедитесь, что вы не мутируете и не возвращаете это значение по умолчанию непосредственно из функции, в противном случае некоторый код вне функции может изменить его, и это повлияет на все вызовы функций.
Андрей Семакин
11
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

Использует randomмодуль, фактически изменяемый синглтон, в качестве генератора случайных чисел по умолчанию.

Фред Фу
источник
7
Но это тоже не очень важный вариант использования.
Евгений Сергеев
3
Я думаю, что нет никакой разницы в поведении между Python «получить ссылку один раз» и не-Python «поиском randomодин раз за вызов функции». Оба в конечном итоге используют один и тот же объект.
nyanpasu64
4

РЕДАКТИРОВАТЬ (пояснение): проблема изменяемого аргумента по умолчанию является симптомом более глубокого выбора дизайна, а именно, что значения аргументов по умолчанию хранятся как атрибуты в объекте функции. Вы можете спросить, почему был сделан этот выбор; как всегда, на такие вопросы сложно ответить должным образом. Но он, безусловно, имеет хорошее применение:

Оптимизация для производительности:

def foo(sin=math.sin): ...

Получение значений объекта в замыкании вместо переменной.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)
Катриэль
источник
7
целые числа и встроенные функции неизменяемы!
Восстановить Монику
2
@Jonathan: В оставшемся примере все еще нет изменяемого аргумента по умолчанию, или я его просто не вижу?
Восстановить Монику
2
@Jonathan: я не хочу сказать, что они изменчивы. Дело в том, что система, которую Python использует для хранения аргументов по умолчанию - для объекта функции, определенного во время компиляции, - может быть полезной. Это подразумевает проблему с изменяемым аргументом по умолчанию, поскольку повторная оценка аргумента при каждом вызове функции сделает уловку бесполезной.
Катриэль
2
@katriealex: Хорошо, но, пожалуйста, скажите об этом в своем ответе, что вы предполагаете, что аргументы должны быть переоценены, и что вы показываете, почему это было бы плохо. Nit-pick: значения аргументов по умолчанию сохраняются не во время компиляции, а при выполнении оператора определения функции.
Восстановить Монику
@WolframH: правда: P! Хотя эти два часто совпадают.
Катриэль
0

Я знаю, что это старый, но, черт возьми, я хотел бы добавить в этот поток вариант использования. Я регулярно пишу собственные функции и слои для TensorFlow / Keras, загружаю свои скрипты на сервер, обучаю модели (с пользовательскими объектами) там, а затем сохраняю модели и загружаю их. Затем, чтобы загрузить эти модели, мне нужно предоставить словарь, содержащий все эти настраиваемые объекты.

Что вы можете сделать в таких ситуациях, как моя, - это добавить код в модуль, содержащий эти настраиваемые объекты:

custom_objects = {}

def custom_object(obj, storage=custom_objects):
    storage[obj.__name__] = obj
    return obj

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

@custom_object
def some_function(x):
    return 3*x*x + 2*x - 2

Более того, скажем, я хочу хранить свои пользовательские функции потерь в другом словаре, чем мои пользовательские слои Keras. Использование functools.partial дает мне легкий доступ к новому декоратору

import functools
import tf

custom_losses = {}
custom_loss = functools.partial(custom_object, storage=custom_losses)

@custom_loss
def my_loss(y, y_pred):
    return tf.reduce_mean(tf.square(y - y_pred))
Саймон
источник
-1

В ответ на вопрос о том, как правильно использовать изменяемые значения аргументов по умолчанию, я предлагаю следующий пример:

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

Рассмотрим эти два примера:

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

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

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

Чтобы действительно увидеть потенциальную мощь и полезность этого метода, сохраните эту первую программу в вашем текущем каталоге под именем «DITTLE.py», а затем запустите следующую программу. Он импортирует и использует нашу новую команду dittle (), не требуя запоминания каких-либо шагов или программирования обручей для перехода.

Вот наш второй пример. Скомпилируйте и запустите это как новую программу.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Разве это не так гладко и чисто, насколько это возможно? Эти изменяемые значения по умолчанию действительно могут пригодиться.

========================

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

Обычный способ - использовать импортируемую функцию, которая обертывает объект Class (и использует глобальный). Итак, для сравнения, здесь метод на основе классов, который пытается делать то же самое, что и изменяемый метод по умолчанию.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

Сохраните эту программу на основе классов в вашем текущем каталоге как DITTLE.py, затем запустите следующий код (который такой же, как и раньше).

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Сравнивая два метода, преимущества использования изменяемого значения по умолчанию в функции должны быть яснее. Изменяемый метод по умолчанию не требует глобальных переменных, его внутренние переменные не могут быть установлены напрямую. И хотя изменяемый метод принимал переданный аргумент для одного цикла, а затем игнорировал его, метод Class был навсегда изменен, потому что его внутренняя переменная напрямую открыта извне. А какой метод проще программировать? Я думаю, это зависит от вашего уровня комфорта с методами и сложности ваших целей.

user10637953
источник