新手请教关于 Python 函数参数默认值设计的问题

2019-06-16 23:25:11 +08:00
 palmers

举栗子:

def gen_list_with(elements = [], e=None):
    elements.append(e)
    return elements
    

rs = gen_list_with( e = 'world')

print(rs)

rs = gen_list_with(e = 'python')    
print(rs)
//输出
['world']
['world', 'python']

我疑惑的是:
1. 方法或函数的形参都是局部的,随着执行完毕,出栈后对应的执行环境都会被销毁,为什么还会出现这种情况呢?
2. 这种情况在给 elements 指定值的情况下会消除, 为什么呢? 比如:

rs = gen_list_with(elements = ['init'], e = 'world')
rs = gen_list_with(e = 'python')    
print(rs)
//输出
['python']

我只知道是因为函数形参使用了可变对象的原因, 但是为什么这么设计, 暂时还没有找到比较权威的说明,麻烦大家给解答一下, 或者给我一份官方或 python 作者这么设计的原因说明文档, 谢谢了

2875 次点击
所在节点    Python
21 条回复
makdon
2019-06-16 23:33:40 +08:00
默认参数只初始化一次
mooncakejs
2019-06-16 23:35:40 +08:00
Python 的大坑。 就算怎么解释都是大坑。
palmers
2019-06-16 23:36:14 +08:00
@makdon 您能说的详细一点 我再 python 官方文档上也看到了您说的这句话, 但是没有很详细的说明
yxcxx
2019-06-17 00:47:49 +08:00
```python
def gen_list_with(elements = [], e=None):
elements.append(e)
print(id(elements))
return elements

rs = gen_list_with( e = 'world')

print(rs)

rs = gen_list_with(e = 'python')
print(rs)
```

140020230277000
['world']
140020230277000
['world', 'python']
makdon
2019-06-17 00:51:17 +08:00
官方的话,我印象中 Guido van rossum 似乎在博客还是采访中提到过这个的设计,但是我刚刚找了一圈没找到,也可能是记错了。
可以参考一下[这个讨论]( https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument)
还有这个“您”字我受不起受不起
so1n
2019-06-17 00:56:10 +08:00
引用内存地址不变,你可以 print gen_list_with.__defaults__,里面就是你的参数了,
andylsr
2019-06-17 01:00:39 +08:00
这里传入的是变量的引用,而不是副本,两次 elements 其实使用的是同一个对象
palmers
2019-06-17 01:01:25 +08:00
@makdon 😁 好的 谢谢你了 我在一篇博客上也见到说在 stackoverflow 有这方面的讨论但是 也都是争论
palmers
2019-06-17 01:03:05 +08:00
@so1n @andylsr 我主要是不太明白 这种设计命名有缺陷为什么还要这么设计,c java c++ 等 都是方法出栈都会销毁执行环境 我记忆中从不会有这种特性存在的
palmers
2019-06-17 01:05:16 +08:00
@makdon 你找这个连接挺好的 谢谢了
silkriver
2019-06-17 08:25:02 +08:00
HelloAmadeus
2019-06-17 09:39:53 +08:00
你把默认参数变量考虑成为用 static 修饰的变量可能更好理解一点
lowman
2019-06-17 09:53:35 +08:00
如果从 C 去理解, 这些数据应该保存在静态存储区里, 而函数的局部变量保存在动态储存区里. 函数初始化的时候应该已经为这个变量分配了内存, 而且不会随着函数执行的结束而销毁. 如果从这点来看, 如果在程序的函数中过多得使用命名参数, 会占用更多的内存. 不知道是不是这样........
fourstring
2019-06-17 12:17:27 +08:00
“方法或函数的形参都是局部的,随着执行完毕,出栈后对应的执行环境都会被销毁,为什么还会出现这种情况呢?”这句话是从 C/C++的设计来理解的。Python 里会有这种问题是因为 Python 中函数是所谓的一类对象,你可以就把它当成函数类的一个对象,而所谓的函数类,也没有什么特别的,就是定义了几个特殊方法如__call__等。这样就很好理解,因为定义函数时的签名列表是这个对象中的实例变量,只要这个函数对象没有被销毁,其实例变量自然也不会被销毁。
fourstring
2019-06-17 12:24:18 +08:00
另外再说两点。第一,这样设计有没有好处?当然有,而且还很大。函数作为对象而非 C/C++中指向特定内存地址的代码在编程中有很实际的意义。函数作为对象直接让函数式编程成为了可能,因为后者的一大基础就是所谓的高阶函数。此外,即使不使用函数式编程的范式,装饰器这样的特性应该是每个 Python 程序员都会用到的,而函数作为对象正是装饰器之所以能存在之原因。
第二,对 Python 中对象的行为不理解的话,可以阅读 Python Language Reference 中的 Data Model 一章。这一章除了是参考文档之外,更是一份对 Python 的哲学的解读。对 Python 的语言设计本身有看法的话,应该在先读过这一章之后才能评价自己的看法是否有道理可言。
fourstring
2019-06-17 12:32:12 +08:00
虽然 Python 的标准实现是 CPython,有些特别的问题也涉及到解释器本身的代码和优化,但是从理念上来说,不应该把 Python 看成一种快速写 C 代码的工具,也不应该用 C/C++的观念来看待 Python。Python 的哲学很多地方有其特质,我觉得这某种程度上也是它受欢迎的原因之一吧。
palmers
2019-06-17 16:20:27 +08:00
@fourstring 谢谢你的耐心解答, 我之前使用最多的是 java 和 js 系语言,所以本能的从这些语言特性来学习 python 了 再结合 @makdon 我基本能理解 在 python 中 函数作为一类对象存在, 在上面的文档中也能体会到这么设计的好处, 但是我还是有很多疑问,比如,因为这种设计带来的副作用(缓存了上一次调用)为什么一直没有消除呢? 由于我现在还是一个很新的新手很多概念非常的不清楚 我估计继续讨论也没有太大价值, 就不讨论了 后面深入学习后如果还不理解 我再上 V2EX 请教你们 谢谢了
kaneg
2019-06-17 19:59:02 +08:00
默认参数应该是不可变的,否则是累加的,空数组这个坑很多人都踩过,正确做法是用 None
siteshen
2019-06-18 09:28:57 +08:00
# 因为表达式 `[]` 是在编译期执行的,函数得到的是表达式的值 `[]` (空数组),而不是表达式 `[]`。因为
# 空数组的表达式和值同型,可能容易忽略值和表达式的区别,但下面这个例子,应该能说明函数定义时得到的
# 是值,而不是表达式。
#
# 如果不这么设计会怎么样?函数需要保存表达式及上下文,并且在调用时执行表达式,会……很复杂。

from datetime import datetime


def print_time(time=datetime.now()):
print('time is', time)


print_time()
print_time()
siteshen
2019-06-18 09:30:57 +08:00
@siteshen #19 另外建议直接写无副作用的代码,根本不给「副作用」坑你的机会。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/574549

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX