Python中默认参数的作用域是什么?

34

当你在Python中定义一个带有数组参数的函数时,该参数的作用域是什么?

这个例子来自Python教程:

def f(a, L=[]):
    L.append(a)
    return L

print f(1)
print f(2)
print f(3)

输出:

[1]
[1, 2]
[1, 2, 3]

我不太确定我理解这里发生了什么。这是否意味着数组的作用域在函数外部?为什么数组会记住从一次调用到另一次调用中的值?来自其他编程语言的我,如果变量不是静态的话,我只会期望这种行为。否则,它似乎应该每次重置。实际上,当我尝试以下操作时:

def f(a):
    L = []
    L.append(a)
    return L

我得到了我预期的行为(数组在每次调用时被重置)。

所以,对我来说似乎只需要解释一下def f(a, L=[]):这一行 - L变量的作用域是什么?


9
请点击此链接:https://dev59.com/9nNA5IYBdhLWcg3wAItP该问题讨论Python中可变默认参数的“最小惊奇原则”。 - sth
1
@sth - 谢谢,那里的讨论很棒。这句话给我留下了深刻的印象,也很有帮助 - “[Python] 在函数定义时绑定默认参数,而不是在函数执行时。” - charlie
7个回答

28

作用域与您预期的一样。

也许令人惊讶的是,缺省值只计算一次并被重复使用,因此每次调用函数时,您都会得到相同的列表,而不是初始化为[]的新列表。

该列表存储在f.__defaults__中(在Python 2中为f.func_defaults)。

def f(a, L=[]):
    L.append(a)
    return L

print f(1)
print f(2)
print f(3)
print f.__defaults__
f.__defaults__ = (['foo'],) # Don't do this!
print f(4)

结果:

[1]
[1, 2]
[1, 2, 3]
([1, 2, 3],)
['foo', 4]

但我真的不太理解。那个列表存储在哪里? - charlie
2
@Charlie:接近了,但还不够。变量L仍然具有局部作用域。当调用函数时,L被设置为func_defaults中相应的值,而在这种情况下,它恰好是对可变对象的引用。只有从函数外部才能访问func_defaults中的对象。最好不要去修改它们。 - Mark Byers
4
@charlie:在理论上,你是可以修改它的,可以看看我的更新答案。但是你不应该这样做。这是恶劣的行为,如果你这样做,你的同事将会永远憎恨你。 - Mark Byers
这太疯狂了!为什么Python不每次重新计算默认值?而且我为什么感觉这个问题的答案其实非常明显... - enigmaticPhysicist
@enigmaticPhysicist 计算默认值可能是一项昂贵的任务,而且不是每次都需要计算的。如果您确实需要动态选择默认值,您可以始终使用 None(或其他适当的标记),并在运行时在函数体中检查该标记。 - chepner
显示剩余5条评论

7
< p > 变量 L 的作用域是您预期的。

"问题" 在于您使用 [] 创建的列表。 Python 每次调用函数时不会创建新列表。 因此,每次调用时都将分配给 L 相同的列表,这就是函数“记住”先前调用的原因。

因此,实际上您拥有以下内容:

mylist = []
def f(a, L=mylist):
    L.append(a)
    return L

Python教程这样说

默认值只会被计算一次。当默认值是可变对象(如列表、字典或大多数类的实例)时,这会产生差异。

并建议以下方式编写期望的行为:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

你给出的第一个代码示例很令人困惑,因为它暗示了默认参数“L”具有全局作用域,而实际上它只在定义的函数内部具有作用域。 - manifest
1
@manifest - 我不确定你为什么这么说。对我来说,这个例子很明显地表明每次调用函数时,L都会被赋予一个值,而这个值并不会在每次调用函数时重新创建。 - David Webb

3
甚至比你想象的还要少“魔法”。这相当于


m = []

def f(a, L=m):
    L.append(a)
    return L

print f(1)
print f(2)
print f(3)

m 只被创建一次。


当然,原始代码中m的作用域与L不同。 - Matt Ball
这样做更有意义,因为列表存在于函数之外。因此,它可能存在并保留其值。但是,如果列表定义在函数内部,则在退出函数时应该会丢失其值。这是否意味着Python将行“def f(a, L = [])”解释为两行“L = []”,然后是“def f(a, L)”? - charlie
当然。@Charlie:它在函数定义时创建,而不是函数执行时创建。函数也只是“对象”(诚然有一些“特殊”的东西,并且有语法糖来创建它们),并且如其他评论所示,它们的默认参数存储在该对象中。 - Joe Koberg

2

假设你有以下代码:

def func(a=[]):
    a.append(1)
    print("A:", a)

func()
func()
func()

您可以使用Python的缩进来帮助您理解正在发生的事情。所有与左边缘对齐的内容在文件被执行时都会被执行。缩进的所有内容都编译成一个代码对象,该对象在调用func()时执行。因此,函数在程序执行时被定义并设置其默认参数(因为def语句与左侧对齐)。
默认参数的处理是一个有趣的问题。在Python 3中,它将大部分有关函数的信息放在两个位置:func.__code__func.__defaults__。在Python 2中,func.__code__func.func_codefunc.__defaults__func.func_defaults。包括2.6在内的Python 2的后续版本都具有这两组名称,以帮助从Python 2过渡到Python 3。我将使用更现代的__code____defaults__。如果您卡在旧版的Python上,则概念相同;只是名称不同。
默认值存储在func.__defaults__中,并在每次调用函数时检索。
因此,当您定义以上函数时,函数体得到编译并存储在__code__变量中,以便稍后执行,而默认参数则存储在__defaults__中。当您调用函数时,它使用__defaults__中的值。如果这些值由于任何原因被修改,则只有修改后的版本可供使用。
在交互式解释器中定义不同的函数,并查看您可以了解Python如何创建和使用函数的内容。

1

这个问题的答案在这里。简而言之:

Python中的函数是一种对象。因为它们是一种对象,所以在实例化时它们的行为就像对象一样。如果使用可变属性作为默认参数定义函数,则该函数与具有静态属性的可变列表的类完全相同。

Lennart Regebro提供了很好的解释,Roberto Liffredo的答案也非常出色。

为了适应Lennart的回答...如果我有一个BananaBunch类:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)


bunch = BananaBunch()
>>> bunch
<__main__.BananaBunch instance at 0x011A7FA8>
>>> bunch.addBanana(1)
>>> bunch.bananas
[1]
>>> for i in range(6):
    bunch.addBanana("Banana #" + i)
>>> for i in range(6):
    bunch.addBanana("Banana #" + str(i))

>>> bunch.bananas
[1, 'Banana #0', 'Banana #1', 'Banana #2', 'Banana #3', 'Banana #4', 'Banana #5']

// And for review ... 
//If I then add something to the BananaBunch class ...
>>> BananaBunch.bananas.append("A mutated banana")

//My own bunch is suddenly corrupted. :-)
>>> bunch.bananas
[1, 'Banana #0', 'Banana #1', 'Banana #2', 'Banana #3', 'Banana #4', 'Banana #5', 'A mutated banana']

这如何应用于函数呢?Python中的函数是对象。这值得重复。Python中的函数是对象

因此,当您创建一个函数时,您正在创建一个对象。当您给函数赋予可变默认值时,您正在使用可变值填充该对象的属性,并且每次调用该函数时,您都在操作相同的属性。因此,如果您使用可变调用(例如append),则您正在修改同一对象,就像向bunch对象添加香蕉一样。


(感谢提供的SO链接,非常好!)但是在函数内声明的变量也应该像属性/成员级变量一样,并且应该在每次调用时保持不变。不是吗? - charlie
@charlie,不完全正确。function对象包含默认参数的属性,但不包含在其中声明的其他变量的属性。请参见@Mark Byers的答案以获取更详细的解释。 - Sean Vieira
另外,给我点反馈,告诉我为什么我错了,否则我无法改进这篇文章。;-) - Sean Vieira

0

这里的“问题”是L=[]只被评估了一次,也就是说,在文件编译时。Python逐行扫描文件并将其编译。当它到达带有默认参数的def时,它会创建该列表实例一次。

如果你把L=[]放在函数代码内部,实例就不会在“编译时间”(实际上编译时间也可以称为运行时间的一部分)创建,因为Python编译函数的代码但不调用它。所以每次调用函数时都会得到一个新的列表实例,因为创建是在每次调用函数时完成的(而不是在编译期间完成的)。

解决这个问题的方法是不要使用可变对象作为默认参数,或者只使用固定的实例,如None

def f(a, L = None):
    if l == None:
        l = []
    L.append(a)
    return L

请注意,在您描述的两种情况中,L 的作用域是函数作用域。

-1

你必须记住Python是一种解释性语言。在这里发生的情况是,当函数“f”被定义时,它创建列表并将其分配给函数“f”的默认参数“L”。稍后,当您调用此函数时,相同的列表将用作默认参数。简而言之,“def”行上的代码只在定义函数时执行一次。这是一种常见的Python陷阱,我自己也曾掉进去。

def f(a, L=[]):
    L.append(a)
    return L

print f(1)
print f(2)
print f(3)

这里的其他答案中已经提出了一些习语来解决这个问题。我建议使用以下方法:

def f(a, L=None):
    L = L or []
    L.append(a)
    return L

这里使用了或短路运算符,要么使用传递的“L”,要么创建一个新列表。

对于您的作用域问题,答案是“L”仅在函数“f”内具有作用域,但由于默认参数只分配一次给单个列表而不是每次调用函数时都分配,因此它表现得好像默认参数“L”具有全局作用域。


请注意,您只需要在非文字或列表/字典/类等情况下使用建议的习语。 - manifest
滥用or的短路行为比明确使用if语句(if L is not None:)不够可读。例如,如果我传递了一个空的类似列表类型的实例,我希望f会使用它,但它不会,虽然对于非空实例它会这样做。 - Mike Graham
啊,在列表的情况下,你是正确的。我猜我从来没有遇到过这个问题,因为当我将一个列表作为参数传递时,我不希望在函数/方法调用之后再使用它(就像在C语言中使用指针一样)。至于可读性,我觉得它非常易读,而如果你有5个默认参数,你需要5个if语句填满函数的上半部分,这看起来很丑陋。当然,这都是主观的。但还是谢谢你提供的有益评论。 - manifest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接