不得不警惕的Python函数默认值

BG90

Posted by Blue Geek on April 17, 2020

不得不警惕的Python函数默认值

By 青衣极客 Blue Geek In 2020-04-17

适当设置一些函数的参数可以提升其灵活性和通用性,但是也带来了一个问题,即每次调用都要设置一大堆,有时还忘了该怎么设置参数。对于这种情况,函数默认值是一个很不错的语法点,也是Python开发者常用的功能。不过,对于一些特殊的默认值,可能不像我们想象中的那样表现。

披着羊皮的狼

1. 一个流行的问题

前段时间,一个关于Python程序结果猜测的问题在互联网上流行,很多人都猜错了,而且还不知道到底错在哪里了。大家也可以来猜一猜这段代码的运行结果。

def func(x=[]):
    x.append(1)
    return x
print(func(), func())

有以下4个选项:

A. [1] [1]

B. [1] [1, 1]

C. [1, 1] [1]

D. [1, 1] [1, 1]

按照基本的程序设计思路来说,这段程序输出的结果应该是 “A”,但很显然这并不是正确的结果。如果我们再仔细地读一读这个函数的实现,可能会认为结果应该是 “B”。很可惜,Python解释器给出的结果是 “D”。 这就引出一个很值得思考的问题,即Python函数默认值的运行方式。

2. 一个正常的函数默认值

对于一个普通的函数,即不依赖全局变量的函数,我们常常要求这样的函数在输入相同的情况下,给出相同的输出。 默认值只是增加了运用的灵活性和通用性,却不应该改变这一个基本的函数设计思路。

正常的调用状态

一个简单的例子如下

def func(x=2):
    x = 1
    return x
print(func(), func())

这段程序的输出为 1 1,就是非常符合我们的预期,也是正常函数应该有的特点,相同调用返回相同结果。但是从那个 “流行的问题” 中可以看出在有些默认值下,并不能保证这个最基本的函数特点。

3. 复合类型的默认值

“流行的问题”和“正常的函数默认值”这两段程序最大的区别在于默认值的数据类型。Python函数是一个对象,并且在程序启动的时候就已经构造完成了。也就是说,函数的默认值在程序启动的时候就已经设置完成了,而不是等到调用的时候才被设置。 这对于基本的数据类型和常量数据是完全合理的,因为可以降低运行时的开销。但是,对于复合数据就有问题了,因为复合数据往往具有记忆性,比如 “流行问题” 中的 list 类型的默认值。

有记忆的调用状态

正是复合数据类型的这种记忆性破坏了不同调用之间的独立性。 但是还有一个问题,按照这个逻辑,第一段程序的结果应该是 “B”,解释器为什么会给出 “D” 这个结果呢?

在第一段程序中,函数返回了这个复合结构的默认值变量,实际上返回的是一个 list对象 的引用,也就是说,无论这个函数被调用多少次,每次输出的都是指向同一块内存的引用。print 函数运行的时候,这两个函数都运行结束了,自然而然,引用所指向的内容是两次函数调用之后的状态。

到此,关于那个“流行问题”的解释总算清晰了,但这并不是一个合理的语法点,也不值得利用。

4. 一点建议

正是由于Python程序的灵活给了开发者极大的便利,这也对开发者的自觉性有了更高的要求。对于函数默认值的使用也应当有一些节制,事实上一个代码风格的提示工具已经指出了这一点。这里再提出以下几点建议:

  1. 最好不要将函数默认值设置为复合类型
  2. 必须设置复合类型的时候,应该保证该数据对象是只读的
  3. 不要将复合类型的默认值参数返回到函数之外

【青衣极客】公众号



COMMENT

博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题

"BG90-不得不警惕的Python函数默认值"