第八章《对象引用、可变性和垃圾回收》(上)
對《流暢的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() *10Gizmo id: 2238839532512 Traceback (most recent call last):File "<stdin>", line 1, in <module> 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查看時其內容也發生了變化。
接著上面的代碼:
現在,創建一個新的字典對象,用alex標識,對象的內容與charles的相同,但charles和alex所指定的對象在內存中是兩個截然不同的對象,他們的內容相同,但是在內存中的地址是不一樣的。
使用==運算符可以看到true,而使用is運算符則是false。可以看到, ==運算符比較的是兩個對象的值,而is運算符比較的則是兩個對象的標識。
關于這個標識,可以使用id()方法來查看:
id()方法可以查看對象在內存中的標識,可以理解為對象在內存中的地址,在Cpython中,id()方法就是返回對象地址的整數表示,而is比較的,正是這個id()方法返回的標識。
8.2.1 在==和is之間選擇
==運算符比較兩個對象的值(對象中的數據),而is比較對象的標識。這有點像C++對象,兩個C++對象做比較,重載運算符= =方法一般直接比較對象中的值,而使用兩個分別指向這兩個對象的指針,指針之間比較的則是地址值。
在變量和單例值之間的比較時,應該使用is。最常見的is檢查莫過于判斷變量綁定的值是不是None。
否定的寫法:
x is not Noneis 運算符不能重載,它的速度要比== 運算符快,而 ==運算符是由類中的__eq__()方法實現,也就是說,a == b這種代碼,實際上是:
a.__eq__(b)所有類的eq()方法都繼承于object類,這個方法可以被覆蓋。
需要注意的是,基本類型(int, float, str…)的變量直接與底層的數據結構掛鉤,只要兩個變量標識的數據值相同,那么他們的id值也是相同的,即使用is會返回True:
8.2.2 元組的相對不可變性
都知道元組不可修改,但這并不完全正確。
元組中的元素保留的是對象的引用,也就是說(1,2,[1,2,3])這個元組中,元素1,元素2,元素[1,2,3]其實都只是引用,并不是真正的數據,這一點造成了元組的相對不可變性,元組的不可變性其實指的是元組數據結構中保留的引用不會變,但是引用真正指向的對象變化,跟元組本身沒有關系:
可以看到t1跟t2做比較,結果是True,因為檢查了元組所有元素指向對象的內容,得到True。
>>> t1[-1] = 3 Traceback (most recent call last):File "<stdin>", line 1, in <module> 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 Falselist(l1)這一句是對列表做的淺復制,在內存中重新創建一個對象,新的對象綁定了變量l2,兩個對象的內容相同,地址不同,因此使用 == 會返回True,使用is返回False。
但是,這種淺復制存在一些問題,對于列表元素,簡單類型(如int,float,str)在做淺復制時會重新在內存中創建出另一個簡單類型的對象,但是對于list,set,tuple等內置復雜類型或者其他自定義類來講,在執行list(l2)這種淺復制時,并不會也像簡單類型那樣創建出新的對象再進行綁定。換句話說,在上面的代碼里,l1和l2中的list和tuple元素使用的是同一個元素,而int元素則是不同的元素。
在l1中,列表元素[55,44]屬于可變型元素,這樣做淺復制,可能會導致一些意想不到的問題,下面對l1和l2做操作:
初始的時候,l1和l2內容完全相同,這沒什么問題,但是l1.append(100)開始對l1添加一個尾部元素,而在l1[1].remove(55)中又將l1中的列表元素移除了55,前面說過,復雜類型的元素在做這種淺復制時并不會重新創建對象,而是采取共享的措施,這就出現了問題,在對基本元素操作時,由于不是共享,因此對一個變量的操作不會影響到另一個變量所代表的對象,但是對于復雜類型,它們采取了共享措施,對一個對象復雜類型元素的操作會影響到另一個對象中的內容,l1[1].remove(55)就是這樣,內部的列表[55,44]由兩個對象共享,這樣一來,無論哪個對象對這個列表做操作,都會影響到另一個對象的內容。
那么,其中的元組元素又是什么情況?
l1[2]是一個元組,它是不可變的對象,同時又是復雜類型,來看看對它的操作結果如何:
首先要清楚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中有更多詳細信息。
先到這里,第八章內容有些多
總結
以上是生活随笔為你收集整理的第八章《对象引用、可变性和垃圾回收》(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 普通用户的sudo权限,禁止root用户
- 下一篇: Eclipse安装STS插件