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


Posted in Python onMarch 30, 2015

本文将介绍使用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作者的亲自陈清,这个问题的答案就一直会是一个谜了。

Python 相关文章推荐
Python定时执行之Timer用法示例
May 27 Python
分享Python字符串关键点
Dec 13 Python
Python 列表(List) 的三种遍历方法实例 详解
Apr 15 Python
简单学习Python多进程Multiprocessing
Aug 29 Python
python 筛选数据集中列中value长度大于20的数据集方法
Jun 14 Python
python设计tcp数据包协议类的例子
Jul 23 Python
python 计算两个列表的相关系数的实现
Aug 29 Python
Django框架安装方法图文详解
Nov 04 Python
详解Django admin高级用法
Nov 06 Python
python爬虫爬取笔趣网小说网站过程图解
Nov 18 Python
Java多线程实现四种方式原理详解
Jun 02 Python
Python基于smtplib协议实现发送邮件
Jun 03 Python
使用Python标准库中的wave模块绘制乐谱的简单教程
Mar 30 #Python
Python中使用语句导入模块或包的机制研究
Mar 30 #Python
优化Python代码使其加快作用域内的查找
Mar 30 #Python
Python中分数的相关使用教程
Mar 30 #Python
Python2.x中str与unicode相关问题的解决方法
Mar 30 #Python
分享一个常用的Python模拟登陆类
Mar 29 #Python
python实现查询IP地址所在地
Mar 29 #Python
You might like
解决phpmyadmin 乱码,支持gb2312和utf-8
2006/11/20 PHP
PHP数组无限分级数据的层级化处理代码
2012/12/29 PHP
在Laravel框架里实现发送邮件实例(邮箱验证)
2016/05/20 PHP
PHP迭代与递归实现无限级分类
2017/08/28 PHP
Javascript & DHTML 实例编程(教程)(三)初级实例篇1—上传文件控件实例
2007/06/02 Javascript
用JavaScript调用WebService的示例
2008/04/07 Javascript
70+漂亮且极具亲和力的导航菜单设计国外网站推荐
2011/09/20 Javascript
javascript检测对象中是否存在某个属性判断方法小结
2013/05/19 Javascript
JS.elementGetStyle(element, style)应用示例
2013/09/24 Javascript
httpclient模拟登陆具体实现(使用js设置cookie)
2013/12/11 Javascript
javascript中数组的多种定义方法和常用函数简介
2014/05/09 Javascript
js实现点击获取验证码倒计时效果
2021/01/28 Javascript
浏览器检测JS代码(兼容目前各大主流浏览器)
2016/02/21 Javascript
Bootstrap在线电子商务网站实战项目5
2016/10/14 Javascript
JS Input里添加小图标的两种方法
2017/11/11 Javascript
AngularJS 表单验证手机号的实例(非必填)
2017/11/12 Javascript
react build 后打包发布总结
2018/08/24 Javascript
详解Vue.js中引入图片路径的几种方式
2019/06/17 Javascript
JS严格模式原理与用法实例分析
2020/04/27 Javascript
JavaScript面试中常考的字符串操作方法大全(包含ES6)
2020/05/10 Javascript
vue pages 多入口项目 + chainWebpack 全局引用缩写说明
2020/09/21 Javascript
[44:01]2018DOTA2亚洲邀请赛3月30日 小组赛B组 EG VS paiN
2018/03/31 DOTA
从零开始学Python第八周:详解网络编程基础(socket)
2016/12/14 Python
Python实现MySQL操作的方法小结【安装,连接,增删改查等】
2017/07/12 Python
Python3 实现随机生成一组不重复数并按行写入文件
2018/04/09 Python
对numpy中的transpose和swapaxes函数详解
2018/08/02 Python
浅析Python四种数据类型
2018/09/26 Python
wxpython+pymysql实现用户登陆功能
2019/11/19 Python
Python利用matplotlib绘制散点图的新手教程
2020/11/05 Python
Python高并发和多线程有什么关系
2020/11/14 Python
西班牙在线宠物商店:zooplus.es
2017/02/24 全球购物
Perfume’s Club英国官网:购买香水和护肤品
2019/11/02 全球购物
营销与策划个人求职信
2013/09/22 职场文书
快递业务员岗位职责
2014/01/06 职场文书
销售人员管理制度
2015/08/06 职场文书
大脑的记忆过程在做数据压缩,不同图形也有共同的记忆格式
2022/04/29 数码科技