Python可变的默认参数:为什么?


20

我知道默认参数是在函数初始化时创建的,而不是在每次调用函数时创建的。请参见以下代码:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

我不明白的是为什么要这样实施。与每次调用时的初始化相比,此行为提供什么好处?


这已经在堆栈溢出的惊人细节中讨论过:stackoverflow.com/q/1132941/5419599
通配符

Answers:


14

我认为原因是实现简单。让我详细说明。

函数的默认值是您需要评估的表达式。在您的情况下,它是一个不依赖于闭包的简单表达式,但可以是包含自由变量-的东西def ook(item, lst = something.defaultList())。如果要设计Python,则可以选择-在定义函数时还是在每次调用函数时对其进行评估。Python选择第一个(与Ruby不同,后者与第二个选项一起使用)。

有一些好处。

首先,您可以提高速度和内存。在大多数情况下,您将拥有不可变的默认参数,并且Python只能构造一次,而不是在每次函数调用时都可以构造它们。这样可以节省(一些)内存和时间。当然,它不能与可变值一起很好地工作,但是您知道如何解决。

另一个好处是简单。很容易理解表达式的计算方式-定义函数时,它使用词法作用域。如果采用其他方式,则词法作用域可能会在定义和调用之间发生变化,从而使其调试起来更加困难。在这些情况下,Python变得非常简单易行。


3
有趣的是-尽管通常这是Python最少惊讶的原则。从形式上的模型复杂性的意义上讲,有些事情很简单,但并不明显且令人惊讶,我认为这很重要。
2012年

1
这里最令人惊讶的是:如果您具有“每次调用时求值”语义,那么如果闭包在两个函数调用之间发生变化,则可能会出错(这很可能)。与知道对其进行一次评估相比,这可能会更加令人惊讶。当然,有人可以辩称,当您来自其他语言时,除了每次调用评估语义之外,这是令人惊讶的,但是您可以看到它是双向的:)
Stefan Kanev 2012年

关于范围的要点
0xc0de 2012年

我认为范围实际上是更重要的一点。由于默认值不限于常量,因此您可能需要调用站点中不在范围内的变量。
Mark Ransom

5

一种表达方式是,lst.append(item) 改变lst参数。lst仍然引用相同的列表。只是该列表的内容已被更改。

基本上,Python根本没有(我记得)任何常量或不可变的变量-但它确实具有一些常量,不可变的类型。您不能修改整数值,只能替换它。但是您可以修改列表的内容而无需替换它。

像整数一样,您不能修改引用,只能替换它。但是您可以修改所引用对象的内容。

至于一次创建默认对象,我想这主要是为了优化,以节省对象创建和垃圾回收的开销。


恰好+1。了解间接层很重要-变量不是值;而是引用一个值。来改变一个变量,该值可以被交换,或突变的(如果它是可变的)。
乔纳斯·普拉卡

当遇到任何涉及python中变量的棘手问题时,我发现将“ =”视为“名称绑定运算符”会很有帮助;无论我们绑定的对象是新的(新鲜对象还是不可变类型实例),名称始终是反弹。
StarWeaver 2014年

4

与每次调用时的初始化相比,此行为提供什么好处?

如示例所示,它使您可以选择所需的行为。因此,如果您希望默认参数是不可变的,则可以使用不可变的值,例如None1。如果您想使默认参数可变,则使用可变的东西,例如[]。这只是灵活性,尽管可以承认,如果您不知道,它可能会咬人。


2

我认为真正的答案是:Python是作为一种程序语言编写的,事后才采用功能方面。您正在寻找的是将参数默认设置作为闭包完成,而Python中的闭包实际上只是半熟而已。有关此尝试的证据:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

这给出[2, 2, 2]了您期望产生真正闭合的位置[0, 1, 2]

如果Python能够将参数默认值包装在闭包中,那么我有很多事情想做。例如:

def foo(a, b=a.b):
    ...

会非常方便,但是“ a”在函数定义时不在范围内,因此您不能这样做,而不得不做一些笨拙的事情:

def foo(a, b=None):
    if b is None:
        b = a.b

几乎是同一件事...几乎。



1

记忆是一个巨大的好处。这是一个标准示例:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

为了比较:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

ipython中的时间测量:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

发生这种情况是因为Python中的编译是通过执行描述性代码执行的。

如果有人说

def f(x = {}):
    ....

很明显,您每次都需要一个新数组。

但是,如果我说:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

在这里,我想我想将内容创建到各种列表中,并且当我不指定列表时就具有一个全局全局对象。

但是编译器将如何猜测呢?那么为什么要尝试呢?我们可以依靠它是否命名,有时可能会有所帮助,但实际上只是猜测。同时,有充分的理由不尝试-一致性。

实际上,Python只是执行代码。已经为变量list_of_all分配了一个对象,因此该对象通过引用传递到默认x的代码中,其方式与对任何函数的调用将获得对此处命名的本地对象的引用相同。

如果我们想将未命名的情况与命名的情况区分开,那将涉及编译时执行分配的代码,其方式与运行时执行的方式大不相同。因此,我们不做特殊情况。


-5

发生这种情况是因为Python中的函数是一流的对象

执行功能定义时,将评估默认参数值。这意味着在定义函数时,表达式将被计算一次,并且每个调用都使用相同的“预先计算”值。

继续说明,编辑参数值会修改后续调用的默认值,并且只需一个简单的解决方案即可使用None(无默认值),并在函数体内进行显式测试,以确保不会出现意外情况。

这意味着def foo(l=[])在调用时该函数成为该函数的实例,并被重用于以后的调用。将功能参数视为对象属性的一部分。

Pro可以利用它使类具有类似于C的静态变量。因此,最好声明默认值None并根据需要对其进行初始化:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

产量:

[5] [5] [5] [5]

而不是意料之外的:

[5] [5,5] [5,5,5] [5,5,5,5]


5
可以。您可以不同地定义函数(是否为一等),以针对每个调用再次评估默认参数表达式。之后的所有内容,即大约90%的答案,完全在问题旁边。-1

1
因此,然后与我们分享有关如何定义函数以评估每个调用的默认arg的知识,我想知道一种比Python Docs建议的更简单的方法。
2012年

2
在语言设计层面上,我的意思是。Python语言定义当前指出,默认参数按原样对待。同样可以很好地说明以其他方式对待默认参数。IOW,您正在回答“事情为什么如此”的问题。

Python可以实现类似于Coffeescript的默认参数。它将插入字节码以检查缺少的参数,如果缺少参数,则对表达式进行求值。
Winston Ewert
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.