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


82

在Python中,将可变对象设置为函数中参数的默认值是一个常见错误。这是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']

为什么发生这种情况的解释在这里

现在我的问题这种语法是否有很好的用例?

我的意思是,如果每个遇到此错误的人都犯了同样的错误,对其进行调试,了解了问题并从此试图避免它,那么这种语法有什么用?


1
我所知道的最好的解释是在链接的问题中:函数是一流的对象,就像类一样。类具有可变的属性数据;函数具有可变的默认值。
卡特里尔2012年

10
这种行为不是“设计选择”,它是语言工作方式的结果,它是从简单的工作原理开始的,除了尽可能少的例外。在某些时候,对我来说,因为我开始“想在Python”这种行为只是成为自然的-我想,如果没有发生感到惊讶
jsbueno

2
我也想知道。此示例遍及整个网络,但这没有任何意义-您想要对传递的列表进行突变而没有默认设置是没有意义的,或者您想返回一个新列表并应立即进行复制在进入功能时。我无法想象两者都有用的情况。
Mark Ransom


2
我只是遇到了一个更现实的示例,它没有上面我抱怨的问题。默认值是__init__类的函数的参数,该参数设置为实例变量。这是一件很想做的事情,而且可变的默认设置全都出错了。stackoverflow.com/questions/43768055/...
马克赎金

Answers:


61

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

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

但是通常使用类可以更好地完成这种事情,因为您可以使用其他属性来清除缓存等。


12
...或记忆修饰器。
Daniel Roseman

28
@functools.lru_cache(maxsize=None)
卡特里尔2012年

3
@katrielalex lru_cache是​​Python 3.2中的新增功能,因此并非所有人都可以使用它。
邓肯

2
仅供参考,现在有backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
熊猫

1
lru_cache如果具有不可散列的值,则不可用。
Synedraacus


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=()在这种特殊情况下使用,但是我发现这不太清楚,也不太笼统。)


2
还要确保您不进行突变并且直接从函数中返回此默认值,否则函数外部的某些代码可以对其进行突变,这将影响所有函数调用。
安德烈·塞玛金

10
import random

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

使用random有效地为可变单例的模块作为其默认随机数生成器。


7
但这也不是一个非常重要的用例。
Evgeni Sergeev

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
@乔纳森:我的意思不是说这些是可变的。这是Python用于存储默认参数的系统(在编译时定义的函数对象上)非常有用。这暗示了可变的默认参数问题,因为在每个函数调用上重新评估参数将使技巧失效。
卡特里尔2012年

2
@katriealex:好的,但是请在回答中这么说,您认为必须重新评估参数,并说明为什么这样做会很糟糕。Nit-pick:默认参数值不在编译时存储,而是在执行函数定义语句时存储。
恢复莫妮卡2012年

@WolframH:是的:P!虽然两者经常重合。
卡特里尔2012年

-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方法的内部变量直接暴露给外部,因此对其进行了永久更改。至于哪种方法更容易编程?我认为这取决于您对方法的舒适程度和目标的复杂性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.