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

将可变对象设置为函数中参数的默认值是 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']

发生这种情况的原因是 给你

现在回答我的问题: 这种语法有好的用例吗?

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

19686 次浏览

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

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

但是通常这种事情在类中做得更好,因为你可以使用额外的属性来清除缓存等等。

import random


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

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

编辑(说明) : 可变的默认参数问题是更深层次设计选择的征兆,即默认参数值存储为函数对象的属性。你可能会问为什么要做出这样的选择; 像往常一样,这样的问题很难得到恰当的回答。但它肯定有很好的用途:

优化业绩:

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

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

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

也许您不会对可变参数进行变异,但是可以期待一个可变参数:

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=(),但是我发现它不那么清楚,也不那么一般。)

标准答案是这一页: http://effbot.org/zone/default-values.htm

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

  • 在回调中将局部变量绑定到外部变量的 现值
  • 缓存/制表
  • 全局名称的本地重新绑定(用于高度优化的代码)

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

可变的默认值对于编写易于使用、可导入的您自己创建的命令非常有用。可变的默认方法相当于在一个函数中拥有私有的静态变量,你可以在第一次调用时初始化它(非常像一个类) ,但是不需要求助于全局变量,不需要使用包装器,也不需要实例化导入的类对象。它本身就很优雅,我希望你也这么认为。

考虑以下两个例子:

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 对象(并使用全局对象)。因此,为了比较,这里有一个基于 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()

将这个基于 class 的程序保存到你的工作目录中,命名为 DITTLE.py 然后运行以下代码(与前面的代码相同)

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


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

通过比较这两种方法,在函数中使用可变默认值的优点应该更加明显。可变的默认方法不需要全局变量,它的内部变量不能直接设置。虽然易变的方法在一个循环中接受了一个知识传递的参数,然后对它置之不理,Class 方法却被永久地改变了,因为它的内部变量直接暴露在外部。至于哪种方法更容易编程?我认为这取决于你对方法的适应程度和你目标的复杂性。

我知道这是一个旧的,但只是为了它的地狱,我想添加一个用例到这个线程。我经常为 TensorFlow/Kera 编写自定义函数和图层,将脚本上传到服务器,在那里训练模型(使用自定义对象) ,然后保存模型并下载它们。为了加载这些模型,我需要提供一个包含所有这些自定义对象的字典。

在像我这样的情况下,您可以向包含这些自定义对象的模块添加一些代码:

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

此外,假设我想将自定义丢失函数存储在不同于自定义 Kera 层的字典中。使用 funtools.part 可以方便地访问一个新的装饰器

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

可变的默认参数(调用代码从未实际使用过)可用于创建哨兵值。内置的 Python 深度拷贝 会这样

可变参数用于确保该值对于该函数是唯一的: 因为在编译 deepcopy时必须创建一个新列表,否则无法访问该列表,所以该对象不能出现在其他任何地方。不可变对象倾向于被实习,并且很容易创建一个空列表。通常,像这样的前哨对象将被显式地单独创建,但是我认为这种方式避免了名称空间污染(即使使用前导-下划线名称)。