可变函数参数默认值的良好用途?

时间:2012-02-06 09:50:01

标签: python arguments default-value mutable

Python中常见的错误是将可变对象设置为函数中参数的默认值。以下是this excellent write-up by David Goodger

的示例
>>> 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']

解释原因的原因是here

现在我的问题是:此语法是否有一个很好的用例?

我的意思是,如果遇到它的每个人都犯了同样的错误,调试它,理解问题,从而试图避免它,这种语法有什么用处?

7 个答案:

答案 0 :(得分:33)

您可以使用它在函数调用之间缓存值:

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

但通常使用类可以做得更好,因为您可以使用其他属性来清除缓存等。

答案 1 :(得分:6)

import random

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

使用random模块,实际上是一个可变单例,作为其默认随机数生成器。

答案 2 :(得分:4)

也许你不会改变mutable参数,但确实需要一个可变参数:

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 :(得分:4)

Canonical answer是这个页面:http://effbot.org/zone/default-values.htm

它还提到了可变默认参数的3个“好”用例:

  • 将局部变量绑定到回调中外部变量的当前值
  • 高速缓存/记忆化
  • 本地重新绑定全局名称(针对高度优化的代码)

答案 4 :(得分:1)

编辑(澄清):可变默认参数问题是更深层次设计选择的症状,即默认参数值作为属性存储在函数对象上。你可能会问为什么做出这个选择;一如既往,这些问题难以正确回答。但它肯定有很好的用途:

优化绩效:

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

在闭包中抓取对象值而不是变量。

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

答案 5 :(得分: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))

答案 6 :(得分:-1)

为回答可变默认参数值的合理使用问题,我提供以下示例:

可变的默认值可用于编写自己创建的易于使用的可导入命令。可变的默认方法相当于在函数中具有私有的静态变量,您可以在第一次调用时对其进行初始化(非常类似于类),而不必求助于全局变量,而不必使用包装器,也不必实例化a导入的类对象。我希望您能同意,它以自己的方式高雅。

请考虑以下两个示例:

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

通过比较这两种方法,在函数中使用可变默认值的优点应该更加清楚。可变的默认方法不需要全局变量,它的内部变量不能直接设置。尽管可变方法在一个周期内接受了一个知识渊博的传递参数,然后将其忽略了,但由于永久将Class方法的内部变量直接暴露给外部,因此对其进行了永久更改。至于哪种方法更容易编程?我认为这取决于您对方法的舒适程度和目标的复杂性。