参数传递

  • 2018-09-14
  • 351
  • 0
# 多行输出结果
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 包含的数据类型:

  • 可变对象包括 listdictset自定义类型等;
  • 不可变对象包括 intfloatboolstrtuple 等。

不可变对象不允许对自身内容进行修改。如果我们对一个不可变对象进行赋值,实际上是生成一个新对象,再让变量指向这个对象。哪怕这个对象简单到只是数字 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 是不可变对象,所以它的方法如 replacestripupper 都不可能修改原对象,只会返回一个新对象,比如重新赋值才可以。而 list 是可变对象,它的方法如 reversesortappend,都是在原有对象上直接修改,无返回值。

不过,有个特殊情况需要注意:

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]]

版权声明: 本网站所有资源采用BY-NC-SA 4.0协议进行授权,转载应当以相同方式注明文章来自:参数传递 - 一方的天地

评论

还没有任何评论,你来说两句吧

发表评论

陕ICP备18010914号
知识共享许可协议
本作品由一方天地采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载或引用本站文章应遵循相同协议。如果有侵犯版权的资源请尽快联系站长,本站会在24h内删除有争议的资源。 -