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