深入讨论Python函数的参数的默认值所引发的问题的原因

本文将介绍使用mutable对象作为Python函数参数默认值潜在的危害,以及其实现原理和设计目的
陷阱重现

我们就用实际的举例来演示我们今天所要讨论的主要内容。

下面一段代码定义了一个名为 generate_new_list_with 的函数。该函数的本意是在每次调用时都新建一个包含有给定 element 值的list。而实际运行结果如下:

Python 2.7.9 (default, Dec 19 2014, 06:05:48)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def generate_new_list_with(my_list=[], element=None):
...   my_list.append(element)
...   return my_list
...
>>> list_1 = generate_new_list_with(element=1)
>>> list_1
[1]
>>> list_2 = generate_new_list_with(element=2)
>>> list_2
[1, 2]
>>>

可见代码运行结果并不和我们预期的一样。list_2在函数的第二次调用时并没有得到一个新的list并填入2,而是在第一次调用结果的基础上append了一个2。为什么会发生这样在其他编程语言中简直就是设计bug一样的问题呢?
准备知识:Python变量的实质

要了解这个问题的原因我们先需要一个准备知识,那就是:Python变量到底是如何实现的?

Python变量区别于其他编程语言的申明&赋值方式,采用的是创建&指向的类似于指针的方式实现的。即Python中的变量实际上是对值或者对象的一个指针(简单的说他们是值得一个名字)。我们来看一个例子。

p = 1
p = p+1

对于传统语言,上面这段代码的执行方式将会是,先在内存中申明一个p的变量,然后将1存入变量p所在内存。执行加法操作的时候得到2的结果,将2这个数值再次存入到p所在内存地址中。可见整个执行过程中,变化的是变量p所在内存地址上的值

面这段代码中,Python实际上是现在执行内存中创建了一个1的对象,并将p指向了它。在执行加法操作的时候,实际上通过加法操作得到了一个2的新对象,并将p指向这个新的对象。可见整个执行过程中,变化的是p指向的内存地址
函数参数默认值陷阱的根本原因

一句话来解释:Python函数的参数默认值,是在编译阶段就绑定的。

现在,我们先从一段摘录来详细分析这个陷阱的原因。下面是一段从Python Common Gotchas中摘录的原因解释:

Python's default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.

可见如果参数默认值是在函数编译compile阶段就已经被确定。之后所有的函数调用时,如果参数不显示的给予赋值,那么所谓的参数默认值不过是一个指向那个在compile阶段就已经存在的对象的指针。如果调用函数时,没有显示指定传入参数值得话。那么所有这种情况下的该参数都会作为编译时创建的那个对象的一种别名存在。

如果参数的默认值是一个不可变(Imuttable)数值,那么在函数体内如果修改了该参数,那么参数就会重新指向另一个新的不可变值。而如果参数默认值是和本文最开始的举例一样,是一个可变对象(Muttable),那么情况就比较糟糕了。所有函数体内对于该参数的修改,实际上都是对compile阶段就已经确定的那个对象的修改。

对于这么一个陷阱在 Python官方文档中也有特别提示:

Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:
如何避免这个陷阱带来不必要麻烦

当然最好的方式是不要使用可变对象作为函数默认值。如果非要这么用的话,下面是一种解决方案。还是以文章开头的需求为例:

def generate_new_list_with(my_list=None, element=None):
  if my_list is None:
    my_list = []
  my_list.append(element)
  return my_list

为什么Python要这么设计

这个问题的答案在StackOverflow上可以找到答案。这里将得票数最多的答案最重要的部分摘录如下:

Actually, this is not a design flaw, and it is not because of internals, or performance.

It comes simply from the fact that functions in Python are first-class objects, and not only a piece of code.

As soon as you get to think into this way, then it completely makes sense: a function is an object being evaluated on its definition; default parameters are kind of “member data” and therefore their state may change from one call to the other – exactly as in any other object.

In any case, Effbot has a very nice explanation of the reasons for this behavior in Default Parameter Values in Python.

I found it very clear, and I really suggest reading it for a better knowledge of how function objects work.

在这个回答中,答题者认为出于Python编译器的实现方式考虑,函数是一个内部一级对象。而参数默认值是这个对象的属性。在其他任何语言中,对象属性都是在对象创建时做绑定的。因此,函数参数默认值在编译时绑定也就不足为奇了。
然而,也有其他很多一些回答者不买账,认为即使是first-class object也可以使用closure的方式在执行时绑定。

This is not a design flaw. It is a design decision; perhaps a bad one, but not an accident. The state thing is just like any other closure: a closure is not a function, and a function with mutable default argument is not a function.

甚至还有反驳者抛开实现逻辑,单纯从设计角度认为:只要是违背程序猿基本思考逻辑的行为,都是设计缺陷!下面是他们的一些论调:

> Sorry, but anything considered “The biggest WTF in Python” is most definitely a design flaw. This is a source of bugs for everyone at some point, because no one expects that behavior at first – which means it should not have been designed that way to begin with.

The phrases “this is not generally what was intended” and “a way around this is” smell like they're documenting a design flaw.

好吧,这么看来,如果没有来自于Python作者的亲自陈清,这个问题的答案就一直会是一个谜了。

(0)

相关推荐

  • Python使用函数默认值实现函数静态变量的方法

    本文实例展示了Python使用函数默认值实现函数静态变量的方法,具体方法如下: 一.Python函数默认值 Python函数默认值的使用可以在函数调用时写代码提供方便,很多时候我们只要使用默认值就可以了. 所以函数默认值在python中用到的很多,尤其是在类中间,类的初始化函数中一帮都会用到默认值. 使用类时能够方便的创建类,而不需要传递一堆参数. 只要在函数参数名后面加上 "=defalut_value",函数默认值就定义好了.有一个地方需要注意的是,有默认值的参数必须在函数参数列表

  • python中函数默认值使用注意点详解

    当在函数中定义默认值时,值初始化只会进行一次,就是执行到def methodname时执行.看下面代码: from datetime import datetime def test(t=datetime.today()): print t if __name__ == "__main__": test() test() 两次方法调用输出的时间都为同一个值,而不是我们预想当前执行时间.对于上面这种情况,建议用下面的方式实现: from datetime import datetime

  • 深入讨论Python函数的参数的默认值所引发的问题的原因

    本文将介绍使用mutable对象作为Python函数参数默认值潜在的危害,以及其实现原理和设计目的 陷阱重现 我们就用实际的举例来演示我们今天所要讨论的主要内容. 下面一段代码定义了一个名为 generate_new_list_with 的函数.该函数的本意是在每次调用时都新建一个包含有给定 element 值的list.而实际运行结果如下: Python 2.7.9 (default, Dec 19 2014, 06:05:48) [GCC 4.2.1 Compatible Apple LLV

  • Python定义函数时参数有默认值问题解决

    这篇文章主要介绍了Python定义函数时参数有默认值问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在定义函数的时候,如果函数的参数有默认值,有两种类型的参数,一种是整数,字符串这种不可变类型,另一种是列表这种可变类型,对于第一种情况没有什么特殊的地方,但是对于可变类型,有一个微妙的小陷阱. 可变类型以及小陷阱: def append_item(item, list1=[]): list1.append(item) return lis

  • ES6知识点整理之函数数组参数的默认值及其解构应用示例

    本文实例讲述了ES6知识点整理之函数数组参数的默认值及其解构应用.分享给大家供大家参考,具体如下: 在ES6中, 函数的参数也可以使用解构赋值和默认值的设置,下面我们来看下 在ES6之前设置函数默认值的写法 function test(x,y) { x = x || 12; y = y || 22; console.log(x,y); } test(); // 12 22 test(1,2) // 1 2 在ES6中给函数参数赋默认值 function test(x=12, y=22) { co

  • Python函数中参数是传递值还是引用详解

    在 C/C++ 中,传值和传引用是函数参数传递的两种方式,在Python中参数是如何传递的?回答这个问题前,不如先来看两段代码. 代码段1: def foo(arg): arg = 2 print(arg) a = 1 foo(a) # 输出:2 print(a) # 输出:1 看了代码段1的同学可能会说参数是值传递. 代码段2: def bar(args): args.append(1) b = [] print(b)# 输出:[] print(id(b)) # 输出:4324106952 b

  • Python中自定义函方法与参数具有默认值的函数

    目录 一.Python中自定义函数的方法 1自定义函数的语法 2自定义函数的实现 3自定义函数的调用 二.Python中参数具有默认值的函数 1参数是常量默认值的函数 2参数是常量默认值的函数 一.Python中自定义函数的方法 在Python编程中,可以使用已经定义好的函数,也可以自定义函数实现某些特殊的功能. 1 自定义函数的语法 在Python中,自定义函数的语法如下所示: def 函数名(参数):     函数体 其中,def是关键字:之后跟的是函数名,通过函数名来调用该函数:函数名之后

  • 详解Python函数可变参数定义及其参数传递方式

    Python函数可变参数定义及其参数传递方式详解 python中 函数不定参数的定义形式如下 1. func(*args)  传入的参数为以元组形式存在args中,如: def func(*args): print args >>> func(1,2,3) (1, 2, 3) >>> func(*[1,2,3]) #这个方式可以直接将一个列表的所有元素当作不定参数 传入(1, 2, 3) 2.func( **kwargs) 传入的参数为以字典形式存在args中,如: d

  • Python函数的参数常见分类与用法实例详解

    本文实例讲述了Python函数的参数常见分类与用法.分享给大家供大家参考,具体如下: 1.形参与实参是什么? 形参(形式参数):指的是 在定义函数时,括号内定义的参数,形参其实就是变量名 实参(实际参数):括号内传入的值,实参其实就是变量的值 x,y是形参 def func(x,y): print(x) print(y) func(20,30) #20,30是实参 注意:实参值(变量的值)与形参(变量名)的绑定关系只在函数调用时才会生效/绑定在函数调用结束后就立刻解除绑定. 2.函数的参数 位置

  • python 函数定位参数+关键字参数+inspect模块

    目录 函数内省(function introspection) 定位参数和仅限关键字参数 inspect模板 函数内省(function introspection) 除了__doc__属性, 函数对象还有很多属性,对于下面的函数,可以使用dir()查看函数具有的属性: >>> dir(factorial) ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__

  • golang给函数参数设置默认值的几种方式小结(函数参数默认值

    目录 前言 强制改变 使用可变参数语法糖 利用结构体的config 转换函数的全部参数 补充知识:Golang中设置函数默认参数的优雅实现 总结 前言 这个问题相当麻烦,根据golang-nuts/google groups中的这篇文章,golang现在与将来都不会支持参数默认值.Go始终在使得自己变得尽可能的简单,而增加这种额外的支持会使parser变得更复杂. 设置参数值的好处: 可以缺省部分参数. 可以提供一种默认的,行之有效的配置. 但是参考资料中提到了几种实现默认值的方法: 强制改变

  • Python函数可变参数定义及其参数传递方式实例详解

    本文实例讲述了Python函数可变参数定义及其参数传递方式.分享给大家供大家参考.具体分析如下: python中 函数不定参数的定义形式如下: 1.func(*args) 传入的参数为以元组形式存在args中,如: def func(*args): print args >>> func(1,2,3) (1, 2, 3) >>> func(*[1,2,3]) #这个方式可以直接将一个列表的所有元素当作不定参数 传入(1, 2, 3) 2.func( **kwargs)

随机推荐