小知识点 — Remember that arguments are passed by assignment in Python
Python中的赋值与参数传递
问题提出
def func(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
以上代码输出为多少?
答案是:[20, 2, 3], 过了就不用看啦
基本规则
Remember that arguments are passed by assignment in Python.
函数中参数通过赋值传递
赋值
对Python赋值的两种理解
直观印象:变量是一个container,给变量赋值类似于往container中填入一个对象,再次赋值就是把container中的对象进行替换
正确理解:Python中的变量可理解为label,给变量赋值类似于把label贴在某个对象上,再次赋值就是把当前的label贴在另一个对象上
以上两种理解的差异点:前者,变量作为固定存在,赋值只改变固定存在中的对象内容;后者,变量仅仅作为一个label,赋值等价于往各种被创建的对象上贴标签
Python中万物解对象,包括函数、类、模块,字符串’hello’,数字1、2、3等。
在 Python 中,前一个理解是不准确的!在 Python 中,前一个理解是不准确的!在 Python 中,前一个理解是不准确的!
《fluent python》中的经典例题
a = [1, 2, 3]
b = a
a.append(4)
b
[1, 2, 3, 4]
使用标识符identifier说明这一点
a = [1, 2, 3]
print( "a--->",id(a) )
a = [1, 2, 3]
print( "a--->",id(a) )
a---> 2322204749960
a---> 2322204695304
a = [1, 2, 3]
print( "a--->",id(a) )
b = [3,2,1]
print( "b--->",id(b) )
c = [13,12,11]
print( "c--->",id(c) )
a = c
print( "a--->",id(a) )
a---> 2322208596488
b---> 2322207525576
c---> 2322208596872
a---> 2322208596872
前者,虽然内容一样,但是本质上是new了两个identifier,所以id()数值不一样;后者a=c 使得 a、c变量贴到了同一个对象上,所以label a和c 对应的identifier值一样
在Python中,使用语法糖 { }、[ ]、( ) 等替代了dict、set、list、tuple等,在Java中实例化对象直接使用new,更impressive
给变量重新赋值,label就被从原来的对象上撕下来贴到了新的对象上
参数传递
Python中参数传递的两个基本考虑点:(1)Python中的参数是通过赋值传递;(2)作用域问题
def fn(x):
x = 3
a = 1
fn(a)
print(a)
1
以上代码的输出结果为1,a 没有变化。上述过程的实现过程如下:
- Step 1、调用 fn(a) 相当于做了一次x=a,即把 label x 贴在a 所映射到对象上;
- Step 2、 在作用域 fn 内部,把label x 从a 所映射到对象上撕下来、并贴到3这个新对象上。
换一种 list 数据类型效果相同
def fn(x):
x = [4,5,6]
a = [1,2,3]
fn(a)
print(a)
[1, 2, 3]
再看一种各种推文教材上常用的例子,涉及到list是mutable对象这个知识点。其中,a和b标签均被贴到同一个list对象上,b[1] 是对所指对象的改动,而不是对b 这个label 本身的改动 ( b中存储的标识符 identifier 不改变,所映射的 identifier不改变 )
a = [1,2,3]
print('a', a, id(a))
b = a
print('b', b, id(b))
b[1] = 5
print('a', a, id(a))
print('b', b, id(b))
a [1, 2, 3] 2713876240136
b [1, 2, 3] 2713876240136
a [1, 5, 3] 2713876240136
b [1, 5, 3] 2713876240136
问题solution
(1)先看文首提出的问题一个变体
def func(m):
m[0] = 20
# m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
答案:l = [20, 2, 3]。上述过程的实现过程如下:
Step 1、在内存中new 一个list 对象 [1,2,3],并将标签label I 贴到这个list上
Step 2、调用func(l),即赋值操作 m = I, 在作用域func 中对 刚才new出来的list对象贴上 m 标签
Step 3、 m[0] = 20 表示更改刚才list对象中的第0项element值,并退出func作用域
Step 4、print('l =', l) 表示输出打印刚才的 list对象
(2)再看文首提出的问题
def func(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
答案:l = [20, 2, 3]。上述过程的实现过程如下:
Step 1、在内存中new 一个list 对象 [1,2,3],并将标签label I 贴到这个list上
Step 2、调用func(l),即赋值操作 m = I, 在作用域func 中对 刚才new出来的list对象贴上 m 标签
Step 3、 m[0] = 20 表示更改刚才list对象中的第0项element值
Step 4、重新在内存中new一个list对象 [4, 5, 6],同时把m标签从第一个list对象上撕下来贴到新new出来的list对象上,并退出func作用域
Step 5、print('l =', l) 表示输出打印第一个 list对象
(3)再看文首提出问题的令一个变体
def func(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
l=func(l)
print('l =', l)
答案:l = [4, 5, 6]。上述过程的实现过程如下:
Step 1、在内存中new 一个list 对象 [1,2,3],并将标签label I 贴到这个list上
Step 2、调用func(l),即赋值操作 m = I, 在作用域func 中对 刚才new出来的list对象贴上 m 标签
Step 3、 m[0] = 20 表示更改刚才list对象中的第0项element值
Step 4、重新在内存中new一个list对象 [4, 5, 6],同时把m标签从第一个list对象上撕下来贴到新new出来的list对象上
Step 5、把第二个new出来的list对象return出来并贴上l标签,然后退出作用域
Step 6、print('l =', l) 表示输出打印第一个 list对象
notes:(1) Python中所有function均有返回值,不写Return时,默认返回None;(2)函数有非None返回值,并在调用时对将返回值进行了赋值,就相当于给函数内部返回的对象贴标签。
总结
通过一个function调用来修改外部变量的值,实现方法有:
通过 Return 返回值 赋值 =
使用 全局变量 global
修改 list 或 dict 这类mutable对象的内部元素
修改class实例化的成员变量
有很多的教程把 Python 的函数参数传递分为mutable对象和immutable对象来说明。这种理解困扰了我很久,其实是错误的,Python中不存在值传递/引用传递的概念,官方文档说明如下:
Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.赋值本身就是创建一份 对象的引用(对象唯一标识符identifier或者内存地址id),形参和实参间不存在别名的关系、不存在引用传递。
网上很容易搜到“参数是可变对象就相当于引用传递”这种错误的理解,一直困扰了我好久
References
[1] 知乎 Crossin的编程教室 https://zhuanlan.zhihu.com/p/38375434?utm_source=wechat_session&utm_medium=social&utm_oi=986911835743977472&s_r=0
Motto
日拱一卒,功不唐捐
![e6356dd828422daba2e762f2fa4e1359.png](https://i-blog.csdnimg.cn/blog_migrate/27979705cb192995601c678eac2da633.jpeg)