繁体   English   中英

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

[英]Good uses for mutable function argument default values?

Python 中的一个常见错误是将可变 object 设置为 function 中参数的默认值。 以下是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']

发生这种情况的解释在这里

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

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

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

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

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

规范的答案是这个页面: http://effbot.org/zone/default-values.htm

它还提到了可变默认参数的 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=() ,但我发现这不太清楚也不太通用。)

import random

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

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

我知道这是一个旧的,但只是为了它,我想在这个线程中添加一个用例。 我定期为 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))

编辑(澄清):可变默认参数问题是更深层次设计选择的症状,即默认参数值存储为 function object 上的属性。 你可能会问为什么做出这个选择; 与往常一样,此类问题很难正确回答。 但它肯定有很好的用途:

性能优化:

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

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

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

调用代码从未实际使用过的可变默认参数可用于创建标记值。 内置的 Python 深拷贝可以做到这一点

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

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

可变默认值可用于编写易于使用、可导入的您自己创建的命令。 可变的默认方法相当于在 function 中拥有私有的 static 变量,您可以在第一次调用时初始化(非常像一个类),但不必求助于全局变量,不必使用包装器,也不必实例化导入的 class object。 它以自己的方式优雅,我希望你会同意。

考虑这两个例子:

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

If you run this code, you will see that the dittle() function internalizes on the the very first call but not on additional calls, it uses a private static cache (the mutable default) for internal static storage between calls, rejects attempts to hijack static 存储,对恶意输入具有弹性,并且可以根据动态条件采取行动(这里是 function 被调用的次数。)

使用可变默认值的关键是不做任何会重新分配 memory 中的变量的事情,而是始终更改变量。

要真正了解这种技术的潜在威力和实用性,请将第一个程序以“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()

现在这不是尽可能的光滑和干净吗? 这些可变的默认值真的可以派上用场。

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

在思考了我的答案一段时间后,我不确定我是否清楚地区分了使用可变默认方法和完成相同事情的常规方法。

常规方法是使用可导入的 function 包装 Class object (并使用全局)。 所以为了比较,这里有一个基于类的方法,它试图做与可变默认方法相同的事情。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM