可变函数参数默认值的好用途?
Posted
技术标签:
【中文标题】可变函数参数默认值的好用途?【英文标题】:Good uses for mutable function argument default values? 【发布时间】:2012-02-27 19:41:47 【问题描述】:将可变对象设置为函数中参数的默认值是 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。
现在我的问题是:这种语法有很好的用例吗?
我的意思是,如果遇到它的每个人都犯了同样的错误,调试它,理解问题并从那里尝试避免它,那么这种语法有什么用?
【问题讨论】:
我所知道的最好的解释是在链接的问题中:函数是一流的对象,就像类一样。类具有可变的属性数据;函数具有可变的默认值。 这种行为不是“设计选择”——它是语言工作方式的结果——从简单的工作原理开始,尽可能少的例外。对我来说,当我开始“用 Python 思考”时,这种行为变得很自然——如果它没有发生,我会感到惊讶 我也想过这个问题。这个例子遍布整个网络,但它没有任何意义——要么你想改变传递的列表并且有一个默认值没有意义,要么你想返回一个新的列表,你应该立即制作一个副本进入功能后。我无法想象两者都有用的情况。 我刚刚遇到了一个更现实的例子,它没有我上面抱怨的问题。默认值是类的__init__
函数的参数,它被设置为实例变量;这是一件非常有效的事情,而可变的默认值会出现可怕的错误。 ***.com/questions/43768055/…
@MarkRansom:根据您的定义,(确定性)计算机上不会有任何错误。当您花足够的时间研究内部结构时,每个错误都是有意义的。老实说,这种行为是 Python 中为数不多的设计缺陷之一。
【参考方案1】:
您可以使用它在函数调用之间缓存值:
def get_from_cache(name, cache=):
if name in cache: return cache[name]
cache[name] = result = expensive_calculation()
return result
但通常这类事情用类做得更好,因为你可以有额外的属性来清除缓存等。
【讨论】:
... 或记忆装饰器。@functools.lru_cache(maxsize=None)
如果您有不可散列的值,lru_cache
不可用。
@Synedraacus:这个食谱也是。
@matrineau 不一定。如果您的一些参数是可哈希的,而其他参数不是,您可以使用此配方仅兑现可哈希的参数。 lru_cache
要求 所有 参数都是可散列的。【参考方案2】:
标准答案是这个页面:http://effbot.org/zone/default-values.htm
它还提到了可变默认参数的 3 个“好”用例:
将局部变量绑定到回调中外部变量的当前值 缓存/记忆 全局名称的本地重新绑定(用于高度优化的代码)【讨论】:
似乎“将局部变量绑定到回调中外部变量的当前值”只是 Python 中另一个设计缺陷的解决方法。【参考方案3】:也许您不会改变可变参数,但确实期望可变参数:
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=()
,但我发现不太清楚,也不太通用。)
【讨论】:
还要确保你不变异并且不返回这个默认值直接从函数中,否则函数之外的一些代码可以变异它会影响所有的函数调用。【参考方案4】:import random
def ten_random_numbers(rng=random):
return [rng.random() for i in xrange(10)]
使用random
模块,实际上是一个可变单例,作为其默认随机数生成器。
【讨论】:
但这也不是一个非常重要的用例。 我认为 Python 的“获取一次引用”和非 Python 的“每次函数调用查找random
一次”之间的行为没有区别。两者最终都使用同一个对象。
random
不是可变的。 import random
然后print(hash(random))
。模块、类(type
s,不是实例)和函数被认为是不可变的。这就是许多记忆和依赖注入机制的工作原理。
注意:python 中的“可变”有一些非常具体的含义(从技术上讲,一切都是“可变的”,它是 python)。也许“冻结”是一个更好的术语。任何遵循Hashable
(__hash__
/__eq__
) 接口的东西都可以被认为是冻结的。仅仅因为一个对象具有副作用并不使其可变:socket.socket
是具有副作用的一流可散列对象的另一个示例。【参考方案5】:
我知道这是一个旧的,但只是为了它,我想在这个线程中添加一个用例。我经常为 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】:编辑(澄清):可变默认参数问题是更深层次设计选择的症状,即默认参数值存储为函数对象的属性。你可能会问为什么做出这个选择;与往常一样,此类问题很难正确回答。但它肯定有很好的用途:
优化性能:
def foo(sin=math.sin): ...
在闭包中获取对象值而不是变量。
callbacks = []
for i in range(10):
def callback(i=i): ...
callbacks.append(callback)
【讨论】:
整数和内置函数不可变! @Jonathan:在剩下的例子中仍然没有可变的默认参数,还是我没有看到它? @Jonathan:我的意思不是这些是可变的。这是 Python 用来存储默认参数的系统——在函数对象上,在编译时定义——可能很有用。这意味着可变的默认参数问题,因为在每个函数调用上重新评估参数将使这个技巧无用。 @katriealex:好的,但请在你的回答中这样说,你认为必须重新评估论点,并说明为什么这样做会很糟糕。 Nit-pick:默认参数值不在编译时存储,而是在函数定义语句执行时存储。 @WolframH:真的:P!虽然两者经常重合。【参考方案7】:为了回答可变默认参数值的良好用途问题,我提供以下示例:
可变默认值可用于编写易于使用、可导入的您自己创建的命令。可变的默认方法相当于在函数中拥有私有的静态变量,您可以在第一次调用时初始化(非常像一个类),但不必求助于全局变量,不必使用包装器,也不必实例化导入的类对象。正如我希望你会同意的那样,它以自己的方式优雅。
考虑这两个例子:
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()
通过比较这两种方法,在函数中使用可变默认值的优势应该更加明显。可变默认方法不需要全局变量,它的内部变量不能直接设置。虽然 mutable 方法在一个循环中接受了一个知识渊博的传递参数然后不理会它,但 Class 方法被永久更改了,因为它的内部变量直接暴露在外部。至于哪种方法更容易编程?我认为这取决于您对方法的熟悉程度以及您目标的复杂程度。
【讨论】:
我不知道为什么你在第二个例子中需要global
。尽管如此,我认为第二个示例比第一个示例更具可读性。即使在功能上最终结果是相同的,使用class
向读者发出信号,“我有一些想要保持在一起的状态”。但是,你确实回答了这个问题,所以我给你道具。我实际上会说这是一个很好的反例,说明为什么实际使用可变参数几乎总是一个坏主意。以上是关于可变函数参数默认值的好用途?的主要内容,如果未能解决你的问题,请参考以下文章