zoukankan      html  css  js  c++  java
  • Python坑系列:可变对象与不可变对象

    在之前的文章 http://www.cnblogs.com/bitpeng/p/4748148.html 中,大家看到了ret.append(path) 和ret.append(path[:])的巨大差别。这和Python的对象机制有关。现在谈谈这个问题!

    我们知道,Python有可变对象和不可变对象,他们的表现行为也迥然不同。先来几个简单的问题:

    1 def foo1(arg):
    2     arg = 5
    3     print(arg)
    4 
    5 x = 1
    6 foo(x)    # 输出5
    7 print(x)  # 输出1
    1 def foo2(arg):
    2     arg.append(3)
    3 
    4 x = [1, 2]
    5 print(x)   # 输出[1, 2]
    6 foo(x)
    7 print(x)   # 输出[1, 2, 3]
    1 def foo3(arg):
    2     arg = [3]
    3 
    4 x = [1, 2]
    5 print(x)   # 输出[1, 2]
    6 foo(x)
    7 print(x)   # 输出[1, 2]

    一、Python参数问题

    1.关于Python默认参数问题,我之前一篇博文有过描述。请参考这里:http://www.cnblogs.com/bitpeng/p/4747765.html

    2.关于Python的参数传递问题。原来接触过C/C++ 的朋友,肯定想过,Python函数调用时,到底是值传递,还是引用传递。看到网上说的最多的是:对于不可变对象,是值传递;对于可变对象,是引用传递。可是我个人感觉,这个描述不是很准确,因为Python函数调用时,不管是可变对象,还是不可变对象,参数引用的都是实参。但是,既然为什么都是引用,结果却表现不同,这就和Python的对象有关。

    在Python中,任何东西都是对象。

    Python使用对象模型来储存数据,任何类型的值都是一个对象。所有的python对象都有3个特征:身份、类型和值。

    身份:每一个对象都有自己的唯一的标识,可以使用内建函数id()来得到它。这个值可以被认为是该对象的内存地址。

    类型:对象的类型决定了该对象可以保存的什么类型的值,可以进行什么操作,以及遵循什么样的规则。type()函数来查看python 对象的类型。

    :对象表示的数据项。

    运算符isis not就是通过id()的返回值(即身份)来判定的,也就是看它们是不是同一个对象的“标签”。

    这里有个很形象的例子:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables

    根据这里,我们可以知道,Python是通过名字来访问对象。这和其他语言有很大的不同,比如在C中,你定义了变量(注意是定义不是声明),那么编译后就一定会给变量分配内存,以后对该变量的读写就是通过该内存地址进行的。而Python中,只会给对象分配内存,然后再通过名称来访问该对象而已。所以,这就是为什么Python名称可以赋值给任何类型的原因(并不是真的赋值). 参数传递也一样,只是用形参名称来访问实参所表示的对象。跟所谓的值传递、引用传递没有任何关系。所以:在foo2()中,append()执行列表方法,当然也会影响实参;而赋值操作,只是将x绑定到另一个列表对象。这样,原来的实参列表还是原来的,没有变化,现在foo2()和foo3()应该能懂了吧。

    关于Python参数调用的结论:

    • Python函数不会替换调用参数所引用的对象。
    • 对一个参数名重新赋值不会起任何作用。
    • Python函数可以修改参数,如果这个参数是可变的。
    • 在Python中没有什么是被隐式复制的。
    • 在Python中函数调用时,不存在什么所谓的值传递和引用传递,只是通过名称(形参)来访问对象(实参所代表的对象), 这和Python对象机制是一致的!

     二、可变对象,不可变对象复制行为

     1 >>> a = 1
     2 >>> b = 1
     3 >>> a is b
     4 True
     5 >>> import copy
     6 >>> c = copy.deepcopy(a)
     7 >>> c
     8 1
     9 >>> c is a
    10 True
    11 >>> s = "abc"
    12 >>> c = copy.deepcopy(s)
    13 >>> c
    14 'abc'
    15 >>> c is s
    16 True
    >>> a = "abc"; b = a; c = a[:]; d = copy.deepcopy(a)
    >>> a,b,c,d
    ('abc', 'abc', 'abc', 'abc')
    >>> a is b ;c is a; d is a
    True
    True
    True

    看到了吗?对于字符、字符串、数值型,不管是赋值,切片,还是深度复制,他们都是同一个对象。但是对于元组呢?表现有所不同 

    1 >>> a = (1,2,[3,4]); b = a; c = a[:];d = copy.deepcopy(a)
    2 >>> b is a; c is a; d is a
    3 True
    4 True
    5 False
    6 >>> a[2][0] = 0;a,b,c,d
    7 ((1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [0, 4]), (1, 2, [3, 4]))

    显然,对于元组并且包含可变元素时,切片和深度复制表现时不一样的。

    结论:

    1、赋值:简单地拷贝对象的引用,两个对象的id相同。
    2、浅拷贝:创建一个新的组合对象,这个新对象与原对象共享内存中的子对象。
    3、深拷贝:创建一个新的组合对象,同时递归地拷贝所有子对象,新的组合对象与原对象没有任何关联。虽然实际上会共享不可变的子对象,但不影响它们的相互独立性。

    浅拷贝和深拷贝的不同仅仅是对组合对象来说,所谓的组合对象就是包含了其它对象的对象,如列表,类实例。而对于数字、字符串以及其它“原子”类型,没有拷贝一说,产生的都是原对象的引用。

    可能有些朋友会有疑问,元组时不可变的,为什么还可以给a[2][0]赋值呢。

    >>> a = (1,2,[3,4])
    >>> a[2] = [1,2,3] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> a[2][:] = [1,2,3] >>> a (1, 2, [1, 2, 3])

    这里我们这样认为,元组可以包含可变对象,只要元组的每个元素的id没有变化即可。所以a[2] = [1,2,3]是尝试把列表第3个元素引用其他的列表,id肯定变了;所以不支持。但是a[2][:] = [1,2,3]是原地赋值,虽然列表本身变了,但是列表本身的id号没变,所以支持。

    结论:对于不可变对象如元组:仅仅代表,他的每一个元素的id号是不可变的。如果元组本身包含可变元素,那么还是可以改变他的值的!

    三、Python与二维数组

    之前做算法题,需要用二维数组,所以很当然的想到了二维列表。

    需求:初始化一个8行8列的数组,每个元素初始化为0.

    当时,我想当然的是这样做的:

    >>> a = [[0] * 8 ]* 8; a
    [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]

    当时还想着,看,多完美!!!

    结果程序总是死循环,不能正常退出。刚开始,结果对程序逻辑进行一次次的检查后,问题还是无解!于是开始调试。最后发现了诡异的问题。

    问:a[i][j] = 1 后,程序会发送什么, 其中0 <= i, j < 8.

    >>> a = [[0] * 8 ]* 8; a
    [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
    >>> a[0][0] = 1;a
    [[1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0]]

    呵呵,发现了吗?给a[0][0]赋值后,结果很多其他的元素也都变了。现在看这个问题其实很简单,因为列表执行乘法,相当于是浅复制。所以a中看似有8个列表,实际上引用的都是同一个。ps:这个问题,实际上书上讲过,我忘了!结果写程序时,出了这样的问题. 

    >>> for i in a:
    ...     print id(a)
    ... 
    140383550710200
    140383550710200
    140383550710200
    140383550710200
    140383550710200
    140383550710200
    140383550710200
    140383550710200

    也许有人会问,那为什么a = [[0] * 8 ]* 8; a[0][0] = 1后, 只有每一列第一个元素变为1,而其他的不变呢?这个问题也简单:因为0是不可变对象。实际上,a = [[0] * 8 ]* 8执行完毕后,a的所有元素id都是相同的。但是,执行a[0][0] = 1后,a[0][0] 的id号已经变了。

    >>> for i in a[0]:print id(i)
    ... 
    32276848
    32276848
    32276848
    32276848
    32276848
    32276848
    32276848
    32276848
    >>> a[0][0] = 1
    >>> for i in a[0]:print id(i)
    ... 
    32276824
    32276848
    32276848
    32276848
    32276848
    32276848
    32276848
    32276848

    结论:需要二维数组时,老老实实的用列表推导。

    >>> a = [[0 for i in range(8)] for i in range(8)]
    >>> a
    [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]
    >>> a[0][0] = 1
    >>> a
    [[1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]]

    现在已经一切正常了。

     

  • 相关阅读:
    iOS 内存分配与分区
    iOS 静态库和动态库
    iOS 静态:动态 Pod
    iOS 架构
    基本控件文档-UIScrollView
    UIActivityIndicatorView
    UIControl事件
    UIDatePicker
    UIPageControl
    UIScrollView
  • 原文地址:https://www.cnblogs.com/bitpeng/p/4748577.html
Copyright © 2011-2022 走看看