# 多行输出结果
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
1.函数参数传递
1.1 问题引入
def func(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
func(l)
print('l =', l)
[4, 5, 6]
l = [20, 2, 3]
1.2 分析:
在 Python 的官方文档 FAQ 里有这样一句话:
Remember that arguments are passed by assignment in Python.
要记住,Python 里的参数是通过赋值传递的。
所以要弄清楚参数传递,先得弄清 Python 的赋值。
1.2.1 python中的赋值
Python 中的变量更像是是个标签;给变量赋值,就是把标签贴在一个物体上;再次赋值就是把标签贴在另一个物体上.
而下面这个理解是错误的:或许在很多人的直观印象中,变量是一个容器;给变量赋值,就像是往一个存储的容器中填入一个数据;再次赋值就是把容器中的数据换掉。
体会下这两种设计的差异:
– 前者,变量不存在实体,它仅仅是一个标签,一旦赋值就被设置到另一个物体上,不变的是那些物体。
– 后者,变量是一个固定的存在,赋值只会改变其中的数值,而变量本身没有改动。
这些“物体”就是对象。Python 中所有东西都是对象,包括函数、类、模块,甚至是字符串’hello’,数字1、2、3,都是对象。
举个例子:
a = 1
print('a', a, id(a))
b = 2
print('b', b, id(b))
c = 1
print('c', c, id(c))
a = b
print('a', a, id(a))
a 1 1356099040
b 2 1356099072
c 1 1356099040
a 2 1356099072
在这个代码里,a 和 c 其实指向的是同一个对象—整数 1。给 a 赋值为 b 之后,a 就变成了指向 2 的标签,但 1 和 c 都不会受影响。
注意:上面例子中使用的是小整数,因为python的内存机制,所以小整数在同一脚本里是同一地址,故id(a) == id(c)
, 但是如果上面整数换成大整数则id(a)
不等于id(c)
1.2.2 函数参数的传递
def fn(x):
x = 3
a = 1
fn(a)
print(a)
1
输出结果是1,a没有变化
调用fn(a)时,就相当于调用了一次x = a
, 把 a 赋值给了 x,也就是把 x 这个标签贴在了 a 的对象上。只不过 x 的作用域仅限于函数 fn 内部。
当 x 在函数内部又被赋值为 3 时,就是把 x 又贴在了 3 这个对象上,与之前的 a 不在有关系。所以外部的 a 不会有任何变化。
把其中的数值换成其他对象,效果也是一样的, a没有变化:
def fn(x):
x = [4, 5, 6]
a = [1, 2, 3]
fn(a)
print(a)
[1, 2, 3]
那上次的题目又是怎么回事?
我们再来看一个赋值:
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] 2173243878920
b [1, 2, 3] 2173243878920
a [1, 5, 3] 2173243878920
b [1, 5, 3] 2173243878920
b 赋值为 a 后,和 a 指向同一个列表对象。[1] 这个基于 index 的赋值是 list 对象本身的一种操作,并没有给 b 重新贴标签,改变的是对象本身。所以 b 指向的还是原来的对象,此对象的改动自然也会体现在 a 身上。
再回看最初的问题
def func(m):
m[0] = 20
m = [4, 5, 6]
return m
l = [1, 2, 3]
m_returned = func(l)
print('m_returned =', m_returned)
print('l =', l)
m_returned = [4, 5, 6]
l = [20, 2, 3]
去掉那句 m=[4,5,6]
的干扰,函数的调用就相当于:
l = [1, 2, 3]
m = l
m[0] = 20
而对 m 重新赋值之后,m 与 l 无关,m贴到了另个物体上面,不影响已经做出的修改。
另外说下,函数的返回值 return,也相当于是一次赋值。
只不过,这时候是把函数内部返回值所指向的对象,赋值给外面函数的调用者:
def fn(x):
print('x更改之前', x, id(x))
x = 3
print('x更改之后', x, id(x))
return x
a = 1
print('a传递之前', a, id(a))
a = fn(a)
print('a传递之后', a, id(a))
a传递之前 1 1355705824
x更改之前 1 1355705824
x更改之后 3 1355705888
a传递之后 3 1355705888
函数结束后,x 这个标签虽然不存在了,但 x 所指向的对象依然存在,就是 a 指向的新对象。
所以,如果你想要通过一个函数来修改外部变量的值,有几种方法:
- 通过返回值赋值
- 使用全局变量
- 修改 list 或 dict 对象的内部元素
- 修改类的成员变量
2.关于可变对象与不可变对象
2.1 包含的数据类型:
- 可变对象包括
list
、dict
、set
、自定义类型
等; - 不可变对象包括
int
、float
、bool
、str
、tuple
等。
不可变对象不允许对自身内容进行修改。如果我们对一个不可变对象进行赋值,实际上是生成一个新对象,再让变量指向这个对象。哪怕这个对象简单到只是数字 0 和 1:
a = 0
print('a', id(a))
a = 1
print('a', id(a))
a 1355705792
a 1355705824
因为对象不可变,所以为了提高效率,Python 会使用一些公用的对象: 小整数对象池,
小整数对象池作用: 避免为整数频繁申请和销毁内存空间。
Python 对小整数的定义是 [-5, 256]
这些整数对象是提前建立好的,不会被垃圾回收。在一个 Python 的程序中,无论这个整数处于LEGB
中的哪个位置,所有位于这个范围内的整数使用的都是同一个对象。同理,单个字母也是这样的。
2.2 "池"
概念:比如 内存池,连接池,对象池,线程池.. 这里说的对象池其实也就是一定数量已经创建好的对象的集合。
2.3 不可变对象
a = 1
print('a', id(a))
b = 1
print('b', id(b))
print(a == b)
print(a is b) # is 则判断是否为同一个对象,也就是地址一致
c = 'helloworld'
print('c', id(c))
d = 'helloworld'
print('d', id(d))
print(c == d)
print(c is d) # is 则判断是否为同一个对象,也就是地址一致
a 1355705824
b 1355705824
True
True
c 1369674480432
d 1369674480432
True
True
2.4 可变对象
m = [1, 2, 3]
print('m', m, id(m))
m[1] = 4
print('m', m, id(m))
m.append(5)
print('m', m, id(m))
m [1, 2, 3] 1369674479368
m [1, 4, 3] 1369674479368
m [1, 4, 3, 5] 1369674479368
可变对象于不可变对象本身的不同仅在于一个可以修改变量的值,而另一个不允许。
基于这一设定,两者在功能上的最大区别就是:不可变对象
可以作为字典 dict
的键 key
,而可变对象不行。比如 list
不能作为字典的键,但 tuple
可以。
另外,明白了可变与不可变的区别,一些方法的效果也就自然理解了:
s = 'abc'
s2 = s.replace('b', 'd')
print('s', s)
print('s2', s2)
m = [1, 2, 3]
m2 = m.reverse()
print('m', m)
print('m2', m2)
s abc
s2 adc
m [3, 2, 1]
m2 None
因为 str
是不可变对象,所以它的方法如 replace
、strip
、upper
都不可能修改原对象,只会返回一个新对象,比如重新赋值才可以。而 list
是可变对象,它的方法如 reverse
、sort
、append
,都是在原有对象上直接修改,无返回值。
不过,有个特殊情况需要注意:
m = [1, 2, 3]
print('m', m, id(m))
m += [4]
print('m', m, id(m))
m = m + [5]
print('m', m, id(m))
m [1, 2, 3] 1369674353416
m [1, 2, 3, 4] 1369674353416
m [1, 2, 3, 4, 5] 1369674312456
m = m +
和 m +=
虽然是一样的结果,但 m
指向的对象却发生了变化。原因在于,前者是做了赋值操作,而后者其实是调用的 __iadd__
方法。
如果我们就是需要产生一个 list 对象的副本,可以通过 [:]:
m = [1, 2, 3]
print('m', m, id(m))
n = m[:]
print('n', n, id(n))
n[1] = 4
print('m', m)
print('n', n)
m [1, 2, 3] 1369674480392
n [1, 2, 3] 1369674312456
m [1, 2, 3]
n [1, 4, 3]
这样对 n 的修改便不再会影响到 m,因为它们已不是同一个对象。
那么如果是这样呢:
m = [1, 2, [3]]
n = m[:]
n[1] = 4
n[2][0] = 5
print(m)
[1, 2, [5]]