Хороші способи використання змінних значень аргументів функції за замовчуванням?


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
FYI зараз існує 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
Я думаю, що немає ніякої різниці в поведінці між "отримати посилання один раз" і не "пошуком randomодин раз на Python за виклик функції". Обидва в результаті використовують один і той же об’єкт.
nyanpasu64

4

EDIT (роз’яснення): Змінна проблема за замовчуванням є симптомом більш глибокого вибору дизайну, а саме того, що значення аргументів за замовчуванням зберігаються як атрибути на об’єкті функції. Ви можете запитати, чому цей вибір був зроблений; як завжди, на такі запитання важко відповісти належним чином. Але це, безумовно, має гарне використання:

Оптимізація продуктивності:

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

Захоплення значень об'єкта в закритті замість змінної.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)

7
цілі числа та вбудовані функції не можна змінювати!
Відновити Моніку

2
@ Джонатан: У решті прикладу досі немає змінних аргументів за замовчуванням, або я просто не бачу його?
Поновити Моніку

2
@ Джонатан: моя суть не в тому, що вони змінюються. Це те, що система, яку Python використовує для зберігання аргументів за замовчуванням - може бути корисною для об’єкта функції, визначеного під час компіляції. Це передбачає зміну аргументу за замовчуванням, оскільки повторна оцінка аргументу при кожному виклику функції зробить фокус марним.
Катріель

2
@katriealex: Добре, але, будь ласка, скажіть так у своїй відповіді, що ви вважаєте, що аргументи повинні бути переглянуті, і що ви показуєте, чому це було б погано. Nit-pick: значення аргументів за замовчуванням зберігаються не під час компіляції, а коли виконується оператор визначення функції.
Поновити Моніку

@WolframH: true: 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()

Тепер хіба це не так гладко та чисто? Ці змінні за замовчуванням дійсно можуть стати в нагоді.

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

Поміркувавши над моєю відповіддю деякий час, я не впевнений, що я зробив різницю між використанням змінного методу за замовчуванням та звичайним способом досягнення того ж самого.

Звичайним способом є використання імпортованої функції, яка обгортає об’єкт класу (і використовує глобальний). Отже, для порівняння, тут метод на основі класу, який намагається робити те саме, що і змінний метод за замовчуванням.

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()

Порівнюючи два методи, переваги використання змінної за замовчуванням у функції повинні бути чіткішими. Змінюваний метод за замовчуванням не потребує глобалів, його внутрішні змінні неможливо встановити безпосередньо. І в той час як мутабельний метод прийняв добре переданий аргумент за один цикл, а потім відкинув його, метод Класу був остаточно змінений, оскільки його внутрішня змінна знаходиться безпосередньо назовні. Щодо того, який метод простіше програмувати? Я думаю, що це залежить від вашого рівня комфорту з методами та складності ваших цілей.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.