[python學習手冊-筆記]004.動態類型

004.動態類型

本系列文章是我我的學習《python學習手冊(第五版)》的學習筆記,其中大部份內容爲該書的總結和我的理解,小部份內容爲相關知識點的擴展。html

非商業用途轉載請註明做者和出處;商業用途請聯繫本人(gaoyang1019@hotmail.com)獲取許可。java

基礎概念的解釋

首先咱們來解釋一些基礎概念,看不懂的能夠跳過,這對於初學者不是很重要。python

強類型語言和弱類型語言

首先,強弱類型語言的區分不是看變量聲明的時候是否顯式的定義數據類型。程序員

強類型語言,定義是任何變量在使用的時候必需要指定這個變量的類型,並且在程序的運行過程當中這個變量只能存儲這個類型的數據。所以,對於強類型語言,一個變量不通過強制轉換,它永遠是這個數據類型,不容許隱式的類型轉換。好比java,python都屬於強類型語言。web

強類型語言在編譯的時候,就能夠檢查出類型錯誤,避免一些不可預知的錯誤,使得程序更加安全。緩存

與之對應的是弱類型語言,在變量使用的時候,不嚴格的檢查數據類型,好比vbScript,數字12和字符串3進行鏈接,能夠直接獲得123。再好比C語言中int i = 0.0是能夠經過編譯的。安全

另外知乎上關於相關問題 rainoftime 大神也有相關解答,沒有查到權威解釋,對大神的解答存疑,可是能夠參考,幫助咱們理解。數據結構

動態類型語言和靜態類型語言

動態類型和靜態類型的區別主要在數據類型檢查的階段。app

動態類型語言:運行期間纔去作數據類型的檢查。在動態類型語言中,不須要給變量顯式的指明其數據類型,該語言會在第一次賦值的時候,將內部的數據類型記錄下來。編輯器

靜態類型語言,在編譯階段就進行數據類型檢查。也就是說靜態類型語言,在定義變量的時候,必須聲明數據類型。

這裏有個比較經典的圖:

各種語言的定義
各種語言的定義

堆和棧

首先,堆兒(很差意思,這裏不該該帶兒化音...),堆(heap)和棧(stack)的概念在不一樣範疇是有不一樣含義的。

在數據結構中,堆指的是知足父子節點知足大小關係的一種徹底二叉樹。棧指的是知足後進先出(LIFO),支持pop和push兩種操做的一個「桶」(原本想說序列,可是不知道準不許確,因此說了個桶...)

在操做系統中,堆兒和棧指的是內存空間。

棧,和數據結構中的棧差很少,是一個LIFO隊列,由編譯器自動分配和釋放,主要用來存放函數的參數值,局部變量的值等內容。

堆,通常由程序員分配和釋放,固然,像java和python這類語言也有自動垃圾回收的機制。這個咱們在後面會講到。

關於堆兒和棧的詳細解釋能夠參考 Memory : Stack vs Heap

變量、對象和引用

python中的變量聲明是不須要顯式的指定類型的,但這並不代表python是一個弱類型語言。

好比,咱們的一條簡單的賦值語句a=3,那麼接下來python編譯器會作哪些事情呢?

  • 建立變量和字面量:
    • 建立一個字面量3(若是這個字面量尚未被建立過的狀況下)
    • 建立一個名稱叫a的變量。通常咱們理解在這個變量a第一次被賦值的時候就建立了它。(實際python解釋器在運行代碼以前就會檢測變量名)
  • 檢查變量類型:
    • python中類型是針對對象而言的,並非針對變量名而言的。 對象會包含兩個重要的頭部信息,一個是類型標誌符,一個是引用計數器。
    • 變量名並不會限制變量的類型。也就是說這個 a 它只是一個名字,具體「關聯」什麼類型的變量,這個是沒有限制的。
  • 變量的使用
    • 當變量出如今表達式中的時候,它就會被當前引用的對象所代替。
    • 仍是說這個例子,若是在以後的代碼中使用了a,好比 a+1那麼這裏的a就會被指向3這個字面量

簡單總結,當咱們執行a=3的時候,實際作了三件事:

  • 建立一個對象實例,3
  • 建立一個變量,a
  • 將變量名a引用到對象實例3上
image-20201214204037907
image-20201214204037907

這裏提到了一個概念,引用。 引用其實就是一種關係,是經過內存中的指針所實現的。

好嘞,這裏又出現了一個新的概念,指針。 指針這個東西,簡單來講能夠理解爲內存地址的一個指向。就是對初學者很差解釋(主要是我懶得解釋,就是屬於那種懂的不須要講,不懂的一時半會講了也是不懂,可是隨着學習的深刻,慢慢就理解了的東西。。。)

變量的類型

首先,python是一個強類型語言,這是毫無疑問的。 可是python不須要顯式的聲明變量類型。 這是由於python的類型是記錄在對象實例中的。

在前面咱們講到過,python中的對象會包含兩個重要的頭部信息:

  • 類型標誌符(type designator):用來標識這個對象的類型
  • 引用計數器(reference counter): 代表有多少個變量引用到了這個對象上,用於跟蹤改對象應該什麼時候被回收

由於對象的這個機制,python中的變量聲明的時候,就不須要再指定類型了。 也就是說變量名與變量類型是無關的。

a=1
a='spam'
a=1.123

並且如上所示,同一個變量名能夠賦值給不一樣類型的對象實例。

共享引用

這裏提出一個問題,以下代碼:

In [6]: a=3
In [7]: b=a
In [8]: a='spam'

那麼在通過這一系列操做以後,a和b的值分別是啥?

In [9]: a
Out[9]: 'spam'

In [10]: b
Out[10]: 3

首先咱們來看,在執行a=3b=a以後,發生了什麼

image-20201214210333750
image-20201214210333750

a=3根據以前的介紹,比較好理解了。b=a實際上變量名b只是複製了a的引用,而後b也引用到了對象實例3上。那在以後這一句a='spam'又發生了什麼?

image-20201214210854202
image-20201214210854202

這個圖就說的很清楚了,在咱們執行了a='spam'以後,a被指向了另一個對象。

搞清楚了這個以後,咱們再來看下一個例子:

a=3
b=a
a=a+3

這個前兩句就不須要解釋了,第三句a=a+3 其實一眼就能夠看出來,此時a是6。這個就涉及到前面說的,當a出如今表達式中的時候,它就會「變成」它所引用的對象實例。a=a+3也就是會變成3+3 計算後得出新的對象實例6,而後變量a引用到6這個對象上。

在原位置修改

關於共享引用,這裏看一個特殊的例子:

In [16]: L1=[1,2,3]

In [17]: L2=L1

In [18]: L1[0]=1111

In [19]: L1
Out[19]: [111123]

In [20]: L2
Out[20]: [111123]

按照以前的劇本,L2和L1都是指向列表[1,2,3]這個對象的,那爲何在咱們修改L1[0] 這個元素以後,爲何L2也跟着發生變化了呢?

我本身畫了圖,從這個圖能夠看出來,實際上對於L1和L2的共享引用來看,並無違反咱們上面說的共享引用的原則。只是對於序列中元素的修改,L1[0]會在原位置覆蓋列表對象中的某部分值。

image-20201214212940939
image-20201214212940939

那麼問題來了若是在修改L1[0]以後,並不想L2的值受到影響,那該怎麼辦?

簡單

把列表原本來本的複製一份就行了。 複製的辦法有三種:

第一種針對列表而言,能夠直接建立一個完整的切片,本質上是一種淺拷貝。

In [32]: L1=[[1,2,3],4,5,6]

In [33]: L2=L1[:]

In [34]: L2
Out[34]: [[123], 456]

In [37]: L1[2]='aaa'

In [38]: L2
Out[38]: [[111123], 456]

In [39]: L1
Out[39]: [[111123], 4'aaa'6]

第二種,淺拷貝,以下面這個例子中的D1.copy()

In [26]: D1={a:[1,2,3],b:3}

In [27]: import copy

In [28]: D2=D1.copy()

In [29]: D2
Out[29]: {6: [123], 33}

In [30]: D1[a][0]=1111

In [31]: D2
Out[31]: {6: [111123], 33}

第三種,深拷貝,以下D2=copy.deepcopy(D1)

In [41]: import copy
    
In [45]: D1={'A':[1,2,3],'B':'spam'}

In [46]: D1
Out[46]: {'A': [123], 'B''spam'}

In [47]: D2=copy.deepcopy(D1)

In [48]: D2
Out[48]: {'A': [123], 'B''spam'}

In [49]: D1['A'][0]=1111

In [50]: D1
Out[50]: {'A': [111123], 'B''spam'}

In [51]: D2
Out[51]: {'A': [123], 'B''spam'}

我相信,看到這裏,對於深拷貝和淺拷貝有些讀者已經明白了,可是有些讀者仍是迷糊的。 這裏簡單說一下,

  • 淺拷貝:只拷貝父對象,不會拷貝對象內部的子對象。

  • 深拷貝:徹底拷貝父對象和子對象。

淺拷貝
淺拷貝
深拷貝
深拷貝

更詳細的內容見: Python 直接賦值、淺拷貝和深度拷貝解析

關於相等

先看一個例子

In [59]: L1=[1,2,3]

In [60]: L2=L1

In [61]: L1==L2
Out[61]: True

In [62]: L1 is L2
Out[62]: True
In [66]: L1=[1,2,3]

In [67]: L2=[1,2,3]

In [68]: L1==L2
Out[68]: True

In [69]: L1 is L2
Out[69]: False

從上面這個例子就能夠看出來,==比較的是值,is 實際比較的是實現引用的指針。

對象的垃圾收集和弱引用

垃圾回收機制也是一件很複雜的事情,可是python編譯器能夠本身去處理這玩意兒。 因此在初級階段,咱們不須要過多關注這玩意兒。 知道有這麼個東西就夠了。

這裏簡單的介紹下,python中的垃圾回收就是咱們所謂的GC,靠的是對象的引用計數器。引用計數器爲0的時候,這個對象實例就會被釋放。對象的引用計數器能夠經過sys.getrefcount(istance)來查看。

In [70]: import sys

In [72]: sys.getrefcount(1)
Out[72]: 2719

引用計數器的引入能夠很好的跟蹤對象的使用狀況,可是在某些狀況下,也可能會帶來問題。 好比循環引用的問題。

以下代碼:

In [73]: L =[1,2,3]

In [74]: L.append(L)

固然,正常人確定不會寫出這種智障代碼,可是在一些複雜的數據結構中,子對象互相引用,就可能會形成死鎖。好比:

In [1]: class Node:
   ...:   def __init__(self):
   ...:     self.parent=None
   ...:     self.child=None
   ...:   def add_child(self,child):
   ...:     self.child=child
   ...:     child.parent=self
   ...:   def __del__(self):
   ...:     print('deleted')
   ...:

這裏咱們定義了一個簡單的類。這時,若是咱們建立一個節點,而後刪除它,能夠看到,對象被回收,而且準確的打印出了deleted。

In [2]: a=Node()

In [3]: del a
deleted

那麼,像下面這個例子,在刪除a節點以後,貌似沒有觸發垃圾回收,只有手動的gc以後,這兩個對象實例才被刪除。

在刪除a以後,沒有觸發垃圾回收,是由於它倆互相引用,實例的引用計數器並無置0 。

那在手動gc以後,因爲python的gc會檢測這種循環引用,並刪除它。

In [4]: a=Node()

In [5]: a.add_child(Node())

In [6]: del a

In [7]: import gc

In [8]: gc.collect()
deleted
deleted
Out[8]: 356
A和B的關係
A和B的關係

那麼若是使用弱引用的話,效果就不同了

In [9]: import weakref
   ...:
   ...: class Node:
   ...:   def __init__(self):
   ...:     self.parent=None
   ...:     self.child=None
   ...:   def add_child(self,child):
   ...:     self.child=child
   ...:     child.parent=weakref.ref(self)
   ...:   def __del__(self):
   ...:     print('deleted')
   ...:

In [10]: a=Node()

In [11]: a.add_child(Node())

In [12]: del a
deleted
deleted

因此這裏就能夠看出來,所謂弱引用,其實並無增長對象的引用計數器,即便弱引用存在,垃圾回收器也會當作沒看見。

弱引用通常能夠拿來作緩存使用,對象存在時可用,對象不存在的時候返回None。這正符合緩存有則使用,無則從新獲取的性質。

相關文章
相關標籤/搜索