第八章《对象引用、可变性和垃圾回收》(上)

对《流畅的python》的读书笔记以及个人理解
9-16

8.1 变量不是盒子

python的变量类似于Java中的引用式变量,最好将它们理解为附加在对象上的标注。变量并不是一个盒子,盒子中装着对象内容,而应该是一个便利贴,贴在对象上的标注,变量本身不是容器。
赋值的时候,应该说“将变量分配给对象”,而不是“将对象分配给变量”,变量是虚无缥缈的东西,而对象才是真真正正存在于内存中的,有实际意义的数据。

对象在赋值之前就已经被创建了:

>>> class Gizmo:
...     def __init__(self):
...             print('Gizmo id: %d' % id(self))
>>> x = Gizmo()
Gizmo id: 2238839532232

>>> y = Gizmo() *10

Gizmo id: 2238839532512
Traceback (most recent call last):
  File "", line 1, in 
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'

>>> dir()
['Gizmo', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'x']
>>>

从上面的代码可以清楚看出对象在赋值之前就已经创建好了,对于Gizmo类的构造方法,其中会打印出当前创建对象在内存中的id号,因此在创建对象并赋值给x的时候,会直接打印出对象的id值。而在赋值给y这个变量的时候,可以看到依然打印出了对象的id,但随后又报错了,这是因为,Gizmo()*10这行代码,Gizmo()依然会创建对象,但Gizmo对象不可能与int对象做乘法,因此报错,赋值给y也就无从说起,最后一行代码dir()打印出了当前目录存在的变量,可以看到没有y,因为在y = Gizmo()*10中,操作的熟悉是先创建Gizmo对象,再执行乘法,最后赋值,而在执行乘法的时候引发错误而终止操作,因此y这个变量并没有被创建出来。

为了理解Python的赋值语句,读代码的时候应该从右边读起,对象在右边创建或者获取,在此之后左边的变量才会绑定到对象上,这像是为对象贴上了标签,但毫无疑问的是,由于变量只是对象的标签,因此一个对象可以有多个标签,也就是一个对象可以有多个变量标注,这些变量便是别名。

9-17

8.2 标识、相等性和别名

Lewis Carroll 是Charles Lutwidge Dodgson教授的笔名,Carroll指的就是Dodgson本人


>>> charles = {'name':'Charles L. Dodgson', 'born':1832}
>>> lewis = charles
>>> lewis is charles
True
>>> id(lewis)
1574035868624
>>> id(charles)
1574035868624
>>> lewis['balance'] = 950
>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
>>>

首先创建一个字典对象,将变量charles绑定到这个对象上,接着lewis = charles这行代码表明lewis已经成为字典对象的另一个别名,现在charles和lewis都是指这个对象,在内存中,这两个变量共享一个对象,使用id()查看其对象的id号可以知道。而lewis[‘balance’]=950则在字典中添加了一个新的键值对,这使得用charles查看时其内容也发生了变化。
接着上面的代码:

>>> charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
>>> alex == charles
True
>>> alex is not charles
True
>>> alex is charles
False
>>>

现在,创建一个新的字典对象,用alex标识,对象的内容与charles的相同,但charles和alex所指定的对象在内存中是两个截然不同的对象,他们的内容相同,但是在内存中的地址是不一样的。
使用==运算符可以看到true,而使用is运算符则是false。可以看到, ==运算符比较的是两个对象的值,而is运算符比较的则是两个对象的标识。
关于这个标识,可以使用id()方法来查看:

>>> id(charles)
1574035868624
>>> id(alex)
1574035868696
>>>

id()方法可以查看对象在内存中的标识,可以理解为对象在内存中的地址,在Cpython中,id()方法就是返回对象地址的整数表示,而is比较的,正是这个id()方法返回的标识。

8.2.1 在==和is之间选择

==运算符比较两个对象的值(对象中的数据),而is比较对象的标识。这有点像C++对象,两个C++对象做比较,重载运算符= =方法一般直接比较对象中的值,而使用两个分别指向这两个对象的指针,指针之间比较的则是地址值。
在变量和单例值之间的比较时,应该使用is。最常见的is检查莫过于判断变量绑定的值是不是None。

x is None

否定的写法:

x is not None

is 运算符不能重载,它的速度要比== 运算符快,而 ==运算符是由类中的__eq__()方法实现,也就是说,a == b这种代码,实际上是:

a.__eq__(b)

所有类的eq()方法都继承于object类,这个方法可以被覆盖。
需要注意的是,基本类型(int, float, str…)的变量直接与底层的数据结构挂钩,只要两个变量标识的数据值相同,那么他们的id值也是相同的,即使用is会返回True:

>>> x = 'h'
>>> y = 'h'
>>> x is y
True
>>> x == y
True

8.2.2 元组的相对不可变性

都知道元组不可修改,但这并不完全正确。
元组中的元素保留的是对象的引用,也就是说(1,2,[1,2,3])这个元组中,元素1,元素2,元素[1,2,3]其实都只是引用,并不是真正的数据,这一点造成了元组的相对不可变性,元组的不可变性其实指的是元组数据结构中保留的引用不会变,但是引用真正指向的对象变化,跟元组本身没有关系:

>>> t1 = (1,2,[1,2,3])
>>> t2 = (1,2,[1,2,3])
>>> t1 == t2
True

可以看到t1跟t2做比较,结果是True,因为检查了元组所有元素指向对象的内容,得到True。

>>> t1[-1] = 3
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'tuple' object does not support item assignment
>>>

现在尝试对t[-1]做修改,结果自然是引发了异常,因为元组需要维护它保留的元素的不变性,也就是说,t[-1]实际上是[1,2,3]这个list对象的引用,并不是list对象本身,我姑且用t_three这个引用来指代[1,2,3]这个对象,那么元组t1就是(1,2,t_three),元组维护的元素不变性,只是维持t_three这个引用\变量不被其他的变量改变,比如变成了t_four,这个是元组不允许的,但是元组t1对于[1,2,3]这个List对象,则没有权限控制它不做改变,比如说:

>>> t1[-1].append(4)
>>> t1
(1, 2, [1, 2, 3, 4])

现在可以看到t1的内容其实是被修改的了,但是这仅仅是对列表元素的修改,对于元组本身,它维护的内容依然是各个对象的引用(1,2,t_three)。

默认做浅复制

复制列表(或者多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

>>> l1 = [3,[55,44],(7,8,9)]
>>> l2 = list(l1)
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l2 == l1
True
>>> l2 is l1
False

list(l1)这一句是对列表做的浅复制,在内存中重新创建一个对象,新的对象绑定了变量l2,两个对象的内容相同,地址不同,因此使用 == 会返回True,使用is返回False。
但是,这种浅复制存在一些问题,对于列表元素,简单类型(如int,float,str)在做浅复制时会重新在内存中创建出另一个简单类型的对象,但是对于list,set,tuple等内置复杂类型或者其他自定义类来讲,在执行list(l2)这种浅复制时,并不会也像简单类型那样创建出新的对象再进行绑定。换句话说,在上面的代码里,l1和l2中的list和tuple元素使用的是同一个元素,而int元素则是不同的元素。
第八章《对象引用、可变性和垃圾回收》(上)_第1张图片
在l1中,列表元素[55,44]属于可变型元素,这样做浅复制,可能会导致一些意想不到的问题,下面对l1和l2做操作:

>>> l1
[3, [55, 44], (7, 8, 9)]
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l1.append(100)
>>> l1[1].remove(55)
>>> l1
[3, [44], (7, 8, 9), 100]
>>> l2
[3, [44], (7, 8, 9)]
>>>

初始的时候,l1和l2内容完全相同,这没什么问题,但是l1.append(100)开始对l1添加一个尾部元素,而在l1[1].remove(55)中又将l1中的列表元素移除了55,前面说过,复杂类型的元素在做这种浅复制时并不会重新创建对象,而是采取共享的措施,这就出现了问题,在对基本元素操作时,由于不是共享,因此对一个变量的操作不会影响到另一个变量所代表的对象,但是对于复杂类型,它们采取了共享措施,对一个对象复杂类型元素的操作会影响到另一个对象中的内容,l1[1].remove(55)就是这样,内部的列表[55,44]由两个对象共享,这样一来,无论哪个对象对这个列表做操作,都会影响到另一个对象的内容。
那么,其中的元组元素又是什么情况?
l1[2]是一个元组,它是不可变的对象,同时又是复杂类型,来看看对它的操作结果如何:

>>> l1
[3, [44], (7, 8, 9), 100]
>>> l2
[3, [44], (7, 8, 9)]

>>> l1[2] += (10, 11)
>>> l1
[3, [44], (7, 8, 9, 10, 11), 100]
>>> l2
[3, [44], (7, 8, 9)]
>>>

首先要清楚l1[2] += (10, 11)这个操作到底做了什么。
这个操作是合法的,重新创建了一个元组,这个元组内容是(7,8,9,10,11),然后绑定到l1上,原来l1绑定的元组被丢弃掉,那么现在l1和l2的元组元素可就不是共享的了。在这之前,它们也是共享的,但由于元组并不能修改,我们不能写出l1[2][1] = 5这种类型的代码来,因为元组维持的相对不变性不允许这种操作,而在使用+=这种操作之后,可以看到l1和l2的内容并没有相互混淆,这就是不可变元素带来的好处了,虽然他们在内部仍然是共享的,但是当试图对这些不可变元素做修改的时候,python就会强制让它们不共享,重新创建出对象出来,因此不会互相影响,所以,对于不可变对象来说,浅复制是没有问题的,而且还能节省内存资源,但是对于可变对象来说,浅复制就不是那么安全了,要想做到可变对象的安全复制(复制后对于一个对象的操作不会影响到另一个对象的内容),就要使用深层复制。

为任意对象做深层复制和浅复制

浅复制机制没什么问题,但有时需要用到深复制(即副本不共享内部对象的引用)。内置模块copy提供的copy()和deepcopy()可以实现为任意对象的浅复制和深复制。

9-20

定义一个简单类来演示copy和deepcopy的用法:

class Bus:
    def __init__(self, passengers = None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

Bus类对象创建时,会在内部创建一个list,list属于复杂对象,而且是可变对象,在做浅复制时并不安全。

>>> bus1 = Bus(['one', 'two', 'three'])   #创建对象bus1
>>> bus2 = copy.copy(bus1)    #浅复制bus1
>>> bus3 = copy.deepcopy(bus1)  # 深复制bus1
>>> id(bus1), id(bus2), id(bus3)  # 查看id,这是三个不同的对象
(2111764720832, 2111761750392, 2111762678112)

>>> bus1.drop('three')  #在bus1中移除元素three
>>> bus1.passengers  #可以看到bus1的passengers少了three
['one', 'two']
>>> bus2.passengers  # 但是对bus1浅复制的bus2的passengers也受到了影响
['one', 'two']
>>> bus3.passengers  # 而深复制的bus3不会受到影响
['one', 'two', 'three']

>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers) 
>>> # 分别查看三个对象的passengers,可以看到bus1和bus2共享同一个passengers
(2111761482312, 2111761482312, 2111760949832) 

有时候深层复制可能复制的太深,一些需要共享的对象也被复制过来,这可以通过实现特殊方法__copy__()__deepcopy()__来控制copy和deepcopy的行为,链接:https://docs.python.org/3/library/copy.html中有更多详细信息。

先到这里,第八章内容有些多

你可能感兴趣的:(流畅的python)