<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ?解密 Python 中的對(duì)象模型

          共 10470字,需瀏覽 21分鐘

           ·

          2020-12-24 03:01

          ?△點(diǎn)擊上方Python貓”關(guān)注 ,回復(fù)“1”領(lǐng)取電子書(shū)

          作者:古明地盆

          來(lái)源:https://www.cnblogs.com/traditional/p/13391098.html

          Python中一切皆對(duì)象

          關(guān)于 Python,你肯定聽(tīng)過(guò)這么一句話(huà):"Python中一切皆對(duì)象"。沒(méi)錯(cuò),在 Python 的世界里,一切都是對(duì)象。

          整型是一個(gè)對(duì)象、字符串是一個(gè)對(duì)象、字典是一個(gè)對(duì)象,甚至 int、str、list 等等,再加上我們使用 class 自定義的類(lèi),它們也是對(duì)象。

          像 int、str、list 等基本類(lèi)型,以及我們自定義的類(lèi),由于它們可以表示類(lèi)型,因此我們稱(chēng)之為類(lèi)型對(duì)象;類(lèi)型對(duì)象實(shí)例化得到的對(duì)象,我們稱(chēng)之為實(shí)例對(duì)象。不管是哪種對(duì)象,它們都屬于對(duì)象。

          因此 Python 中面向?qū)ο蟮睦砟钬瀼氐姆浅氐祝嫦驅(qū)ο笾械?類(lèi)"和"對(duì)象"在 Python 中都是通過(guò)"對(duì)象"實(shí)現(xiàn)的。

          在面向?qū)ο罄碚撝校嬖谥?類(lèi)"和"對(duì)象"兩個(gè)概念,像 int、dict、tuple、以及使用 class 關(guān)鍵字自定義的類(lèi)型對(duì)象實(shí)現(xiàn)了面向?qū)ο罄碚撝?類(lèi)"的概念,而 123、(1, 2, 3),"xxx" 等等這些實(shí)例對(duì)象則實(shí)現(xiàn)了面向?qū)ο罄碚撝?對(duì)象"的概念。但是在 Python 中,面向?qū)ο蟮?類(lèi)"和"對(duì)象"都是通過(guò)對(duì)象實(shí)現(xiàn)的。

          我們舉個(gè)栗子:

          >>>?#?int它是一個(gè)類(lèi),因此它屬于類(lèi)型對(duì)象,?類(lèi)型對(duì)象實(shí)例化得到的對(duì)象屬于實(shí)例對(duì)象
          >>>?int??
          <class?'int'>
          >>>?int('0123')?
          123
          >>>

          因此可以用一張圖來(lái)描述面向?qū)ο笤?Python 中的體現(xiàn):

          類(lèi)型、對(duì)象體系

          a 是一個(gè)整數(shù)(實(shí)例對(duì)象),其類(lèi)型是 int (類(lèi)型對(duì)象)。

          >>>?a?=?123
          >>>?a
          123
          >>>?type(a)
          <class?'int'>
          >>>?isinstance(a,?int)
          True
          >>>

          但是問(wèn)題來(lái)了,按照面向?qū)ο蟮睦碚搧?lái)說(shuō),對(duì)象是由類(lèi)實(shí)例化得到的,這在 Python 中也是適用的。既然是對(duì)象,那么就必定有一個(gè)類(lèi)來(lái)實(shí)例化它,換句話(huà)說(shuō)對(duì)象一定要有類(lèi)型。至于一個(gè)對(duì)象的類(lèi)型是什么,就看這個(gè)對(duì)象是被誰(shuí)實(shí)例化的,被誰(shuí)實(shí)例化那么類(lèi)型就是誰(shuí)。而我們說(shuō) Python 中一切皆對(duì)象,所以像 int、str、tuple 這些內(nèi)置的類(lèi)型也是具有相應(yīng)的類(lèi)型的,那么它們的類(lèi)型又是誰(shuí)呢?

          我們使用 type 函數(shù)查看一下就好了。

          >>>?type(int)
          <class?'type'>
          >>>?type(str)
          <class?'type'>
          >>>?type(dict)
          <class?'type'>
          >>>?type(type)
          <class?'type'>
          >>>

          我們看到類(lèi)型對(duì)象的類(lèi)型,無(wú)一例外都是 type。type 應(yīng)該是初學(xué) Python 的時(shí)候就接觸了,當(dāng)時(shí)使用 type 都是為了查看一個(gè)對(duì)象的類(lèi)型,然而 type 的作用遠(yuǎn)沒(méi)有這么簡(jiǎn)單,我們后面會(huì)說(shuō),總之我們目前看到類(lèi)型對(duì)象的類(lèi)型是 type。

          所以 int、str 等類(lèi)型對(duì)象是 type 的對(duì)象,而 type 我們也稱(chēng)其為元類(lèi),表示類(lèi)型對(duì)象的類(lèi)型。至于 type 本身,它的類(lèi)型還是 type,所以它連自己都沒(méi)放過(guò),把自己都變成自己的對(duì)象了。

          因此在 Python 中,你能看到的任何對(duì)象都是有類(lèi)型的,我們可以使用 type 函數(shù)查看,也可以獲取該對(duì)象的__class__屬性查看。

          所以:實(shí)例對(duì)象、類(lèi)型對(duì)象、元類(lèi),Python 中任何一個(gè)對(duì)象都逃不過(guò)這三種身份。

          Python 中還有一個(gè)特殊的類(lèi)型(對(duì)象),叫做 object,它是所有類(lèi)型對(duì)象的基類(lèi)。不管是什么類(lèi),內(nèi)置的類(lèi)也好,我們自定義的類(lèi)也罷,它們都繼承自 object。因此, object 是所有類(lèi)型對(duì)象的"基類(lèi)"、或者說(shuō)"父類(lèi)"。

          >>>?issubclass(int,?object)
          True
          >>>

          因此,綜合以上關(guān)系,我們可以得到下面這張關(guān)系圖:

          我們自定義的類(lèi)型也是如此,舉個(gè)栗子:

          class?Female:
          ????pass

          print(type(Female))??#?
          print(issubclass(Female,?object))??#?True

          在 Python3 中,自定義的類(lèi)即使不顯式的繼承 object,也會(huì)默認(rèn)繼承自 object。

          那么我們自定義再自定義一個(gè)子類(lèi),繼承自 Female 呢?

          class?Female:
          ????pass


          class?Girl(Female):
          ????pass


          #?自定義類(lèi)的類(lèi)型都是type
          print(type(Girl))??#?

          #?但Girl繼承自Female,?所以它是Female的子類(lèi)
          print(issubclass(Girl,?Female))??#?True
          #?而Female繼承自object,?所以Girl也是object的子類(lèi)
          print(issubclass(Girl,?object))??#?True


          #?這里需要額外多提一句實(shí)例對(duì)象,?我們之前使用type得到的都是該類(lèi)的類(lèi)型對(duì)象
          #?換句話(huà)說(shuō)誰(shuí)實(shí)例化得到的它,?那么對(duì)它使用type得到的就是誰(shuí)
          print(type(Girl()))??#?
          print(type(Female()))??#?

          #?但是我們說(shuō)Girl的父類(lèi)是Female,?Female的父類(lèi)是object
          #?所以Girl的實(shí)例對(duì)象也是Female和object的實(shí)例對(duì)象,?Female的實(shí)例對(duì)象也是object的實(shí)例對(duì)象
          print(isinstance(Girl(),?Female))??#?True
          print(isinstance(Girl(),?object))??#?True

          因此上面那張關(guān)系圖就可以變成下面這樣:

          我們說(shuō)可以使用 type 和__class__查看一個(gè)對(duì)象的類(lèi)型,并且還可以通過(guò) isinstance 來(lái)判斷該對(duì)象是不是某個(gè)已知類(lèi)型的實(shí)例對(duì)象;那如果想查看一個(gè)類(lèi)型對(duì)象都繼承了哪些類(lèi)該怎么做呢?我們目前都是使用 issubclass 來(lái)判斷某個(gè)類(lèi)型對(duì)象是不是另一個(gè)已知類(lèi)型對(duì)象的子類(lèi),那么可不可以直接獲取某個(gè)類(lèi)型對(duì)象都繼承了哪些類(lèi)呢?

          答案是可以的,方法有三種,我們分別來(lái)看一下:

          class?A:?pass

          class?B:?pass

          class?C(A):?pass

          class?D(B,?C):?pass

          #?首先D繼承自B和C,?C又繼承A,?我們現(xiàn)在要來(lái)查看D繼承的父類(lèi)
          #?方法一:?使用__base__
          print(D.__base__)??#?

          #?方法二:?使用__bases__
          print(D.__bases__)??#?(,?)

          #?方法三:?使用__mro__
          print(D.__mro__)
          #?(,?,?,?,?)
          • __base__: 如果繼承了多個(gè)類(lèi), 那么只顯示繼承的第一個(gè)類(lèi), 沒(méi)有顯示繼承則返回一個(gè);

          • __bases__: 返回一個(gè)元組, 會(huì)顯示所有直接繼承的父類(lèi), 如果沒(méi)有顯示的繼承, 則返回(,);

          • __mro__: mro 表示 Method Resolution Order, 表示方法查找順序, 會(huì)從自身除法, 找到最頂層的父類(lèi), 因此返回自身、繼承的基類(lèi)、以及基類(lèi)繼承的基類(lèi), 一直找到 object;

          最后我們來(lái)看一下 type 和 object,估計(jì)這兩個(gè)老鐵之間的關(guān)系會(huì)讓很多人感到困惑。

          我們說(shuō) type 是所有類(lèi)的元類(lèi),而 object 是所有的基類(lèi),這就說(shuō)明 type 是要繼承自 object 的,而 object 的類(lèi)型是 type。

          >>>?type.__base__
          <class?'object'>
          >>>?object.__class__
          <class?'type'>
          >>>

          這就怪了,這難道不是一個(gè)先有雞還是先有蛋的問(wèn)題嗎?其實(shí)不是的,這兩個(gè)對(duì)象是共存的,它們之間的定義其實(shí)是互相依賴(lài)的。至于到底是怎么肥事,我們后面在看解釋器源碼的時(shí)候就會(huì)很清晰了。

          總之目前記住兩點(diǎn):

          1. type 站在類(lèi)型金字塔的最頂端, 任何的對(duì)象按照類(lèi)型追根溯源, 最終得到的都是 type;

          2. object 站在繼承金字塔的最頂端, 任何的類(lèi)型對(duì)象按照繼承追根溯源, 最終得到的都是 object;

          我們說(shuō) type 的類(lèi)型還是 type,但是 object 的基類(lèi)則不再是 object,而是一個(gè) None。為什么呢?其實(shí)答案很簡(jiǎn)單,我們說(shuō) Python 在查找屬性或方法的時(shí)候,會(huì)回溯繼承鏈,自身如果沒(méi)有的話(huà),就會(huì)按照__mro__指定的順序去基類(lèi)中查找。所以繼承鏈一定會(huì)有一個(gè)終點(diǎn),否則就會(huì)像沒(méi)有出口的遞歸一樣出現(xiàn)死循環(huán)了。

          最后將上面那張關(guān)系圖再完善一下的話(huà):

          因此上面這種圖才算是完整,其實(shí)只看這張圖我們就能解讀出很多信息。比如:實(shí)例對(duì)象的類(lèi)型是類(lèi)型對(duì)象,類(lèi)型對(duì)象的類(lèi)型是元類(lèi);所有的類(lèi)型對(duì)象的基類(lèi)都收斂于 object,所有對(duì)象的類(lèi)型都收斂于 type。因此 Python 算是將一切皆對(duì)象的理念貫徹到了極致,也正因?yàn)槿绱耍琍ython 才具有如此優(yōu)秀的動(dòng)態(tài)特性。

          事實(shí)上,目前介紹的有些基礎(chǔ)了,但 Python 中的對(duì)象的概念確實(shí)非常重要。為了后面再分析源碼的時(shí)候能夠更輕松,因此我們有必要系統(tǒng)地回顧一下,并且上面的關(guān)系圖會(huì)使我們?cè)诤竺娴膶W(xué)習(xí)變得輕松。因?yàn)榈鹊娇唇忉屍鞯臅r(shí)候,我們可就沒(méi)完了,就不那么輕松了(なん~~~てね)。

          Python中的變量只是個(gè)名字

          Python 中的變量只是個(gè)名字,站在 C 語(yǔ)言的角度來(lái)說(shuō)的話(huà),Python 中的變量存儲(chǔ)的只是對(duì)象的內(nèi)存地址,或者說(shuō)指針,這個(gè)指針指向的內(nèi)存存儲(chǔ)的才是對(duì)象。

          所以在 Python 中,我們都說(shuō)變量指向了某個(gè)對(duì)象。在其它靜態(tài)語(yǔ)言中,變量相當(dāng)于是為某塊內(nèi)存起的別名,獲取變量等于獲取這塊內(nèi)存所存儲(chǔ)的值。而 Python 中變量代表的內(nèi)存存儲(chǔ)的不是對(duì)象,只是對(duì)象的指針。

          我們用兩段代碼,一段 C 語(yǔ)言的代碼,一段 Python 的代碼,來(lái)看一下差別。

          #include?

          void?main()
          {
          ????int?a?=?123;
          ????printf("address?of?a?=?%p\n",?&a);

          ????a?=?456
          ????printf("address?of?a?=?%p\n",?&a);
          }
          //?輸出結(jié)果
          /*
          address?of?a?=?0x7fffa94de03c
          address?of?a?=?0x7fffa94de03c
          */

          我們看到前后輸出的地址是一樣的,再來(lái)看看 Python 的。

          a?=?666
          print(hex(id(a)))??#?0x1b1333394f0

          a?=?667
          print(hex(id(a)))??#?0x1b133339510

          然而我們看到 Python 中變量 a 的地址前后發(fā)生了變化,我們分析一下原因。

          首先在 C 中,創(chuàng)建一個(gè)變量的時(shí)候必須規(guī)定好類(lèi)型,比如 int a = 666,那么變量 a 就是 int 類(lèi)型,以后在所處的作用域中就不可以變了。如果這時(shí)候,再設(shè)置 a = 777,那么等于是把內(nèi)存中存儲(chǔ)的 666 換成 777,a 的地址和類(lèi)型是不會(huì)變化的。

          而在 Python 中,a = 666 等于是先開(kāi)辟一塊內(nèi)存,存儲(chǔ)的值為 666,然后讓變量 a 指向這片內(nèi)存,或者說(shuō)讓變量 a 存儲(chǔ)這塊內(nèi)存的指針。然后 a = 777 的時(shí)候,再開(kāi)辟一塊內(nèi)存,然后讓 a 指向存儲(chǔ) 777 的內(nèi)存,由于是兩塊不同的內(nèi)存,所以它們的地址是不一樣的。

          所以 Python 中的變量只是一個(gè)和對(duì)象關(guān)聯(lián)的名字罷了,它代表的是對(duì)象的指針。換句話(huà)說(shuō) Python 中的變量就是個(gè)便利貼,可以貼在任何對(duì)象上,一旦貼上去了,就代表這個(gè)對(duì)象被引用了。

          我們?cè)賮?lái)看看變量之間的傳遞,在 Python 中是如何體現(xiàn)的。

          a?=?666
          print(hex(id(a)))??#?0x1e6c51e3cf0

          b?=?a
          print(hex(id(b)))??#?0x1e6c51e3cf0

          我們看到打印的地址是一樣的,我們?cè)儆靡粡垐D解釋一下。

          我們說(shuō) a = 666 的時(shí)候,先開(kāi)辟一份內(nèi)存,再讓 a 存儲(chǔ)對(duì)應(yīng)內(nèi)存的指針;然后 b = a 的時(shí)候,會(huì)把 a 的地址拷貝一份給 b,所以 b 存儲(chǔ)了和 a 相同的地址,它們都指向了同一個(gè)對(duì)象。

          因此說(shuō) Python 是值傳遞、或者引用傳遞都是不準(zhǔn)確的,準(zhǔn)確的說(shuō) Python 是變量之間的賦值傳遞,對(duì)象之間的引用傳遞。

          因?yàn)?Python 中的變量本質(zhì)上就是一個(gè)指針,所以在 b = a 的時(shí)候,等于把a(bǔ)的地址拷貝一份給b,所以對(duì)于變量來(lái)說(shuō)是賦值傳遞;然后 a 和 b 又都是指向?qū)ο蟮闹羔槪虼藢?duì)于對(duì)象來(lái)說(shuō)是引用傳遞。

          另外還有最關(guān)鍵的一點(diǎn),我們說(shuō) Python 中的變量是一個(gè)指針,當(dāng)傳遞一個(gè)變量的時(shí)候,傳遞的是指針;但是在操作一個(gè)變量的時(shí)候,會(huì)操作變量指向的內(nèi)存。

          所以 id(a) 獲取的不是 a 的地址,而是 a 指向的內(nèi)存的地址(在底層其實(shí)就是a),同理 b = a,是將 a 本身,或者說(shuō)將 a 存儲(chǔ)的、指向某個(gè)具體的對(duì)象的地址傳遞給了 b。

          另外在 C 的層面上,a 和 b 屬于指針變量,那么 a 和 b 有沒(méi)有地址呢?顯然是有的,只不過(guò)在 Python 中你是看不到的,Python 解釋器只允許你看到對(duì)象的地址。

          最后提一下變量的類(lèi)型

          我們說(shuō)變量的類(lèi)型其實(shí)不是很準(zhǔn)確,應(yīng)該是變量指向(引用)的對(duì)象的類(lèi)型,因?yàn)槲覀冋f(shuō) Python 中變量是個(gè)指針,操作指針會(huì)操作指針指向的內(nèi)存,所以我們使用 type(a) 查看的是變量 a 指向的內(nèi)存的類(lèi)型,當(dāng)然為了方便也會(huì)直接說(shuō)變量的類(lèi)型,理解就行。那么問(wèn)題來(lái)了,我們?cè)趧?chuàng)建一個(gè)變量的時(shí)候,并沒(méi)有顯示的指定類(lèi)型啊,但 Python 顯然是有類(lèi)型的,那么 Python 是如何判斷一個(gè)變量指向的是什么類(lèi)型的數(shù)據(jù)呢?

          答案是:解釋器是通過(guò)靠猜的方式,通過(guò)你賦的值(或者說(shuō)變量引用的值)來(lái)推斷類(lèi)型。所以在 Python 中,如果你想創(chuàng)建一個(gè)變量,那么必須在創(chuàng)建變量的時(shí)候同時(shí)賦值,否則解釋器就不知道這個(gè)變量指向的數(shù)據(jù)是什么類(lèi)型。所以 Python 是先創(chuàng)建相應(yīng)的值,這個(gè)值在 C 中對(duì)應(yīng)一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體里面有一個(gè)成員專(zhuān)門(mén)用來(lái)存儲(chǔ)該值對(duì)應(yīng)的類(lèi)型。當(dāng)創(chuàng)建完值之后,再讓這個(gè)變量指向它,所以 Python 中是先有值后有變量。但顯然 C 中不是這樣的,因?yàn)?C 中變量代表的內(nèi)存所存儲(chǔ)的就是具體的值,所以 C 中可以直接聲明一個(gè)變量的同時(shí)不賦值。因?yàn)?C 要求聲明變量的同時(shí)必須指定類(lèi)型,所以聲明變量的同時(shí),其類(lèi)型和內(nèi)存大小就已經(jīng)固定了。而 Python 中變量代表的內(nèi)存是個(gè)指針,它只是指向了某個(gè)對(duì)象,所以由于其便利貼的特性,可以貼在任意對(duì)象上面,但是不管貼在哪個(gè)對(duì)象,你都必須先有對(duì)象才可以,不然變量貼誰(shuí)去?

          另外,盡管 Python 在創(chuàng)建變量的時(shí)候不需要指定類(lèi)型,但 Python 是強(qiáng)類(lèi)型語(yǔ)言,強(qiáng)類(lèi)型語(yǔ)言,強(qiáng)類(lèi)型語(yǔ)言,重要的事情說(shuō)三遍。 而且是動(dòng)態(tài)強(qiáng)類(lèi)型,因?yàn)轭?lèi)型的強(qiáng)弱和是否需要顯示聲明類(lèi)型之間沒(méi)有關(guān)系。

          可變對(duì)象與不可變對(duì)象

          我們說(shuō)一個(gè)對(duì)象其實(shí)就是一片被分配的內(nèi)存空間,內(nèi)存中存儲(chǔ)了相應(yīng)的值,不過(guò)這些空間可以是連續(xù)的,也可以是不連續(xù)的。

          不可變對(duì)象一旦創(chuàng)建,其內(nèi)存中存儲(chǔ)的值就不可以再修改了。如果想修改,只能創(chuàng)建一個(gè)新的對(duì)象,然后讓變量指向新的對(duì)象,所以前后的地址會(huì)發(fā)生改變。而可變對(duì)象在創(chuàng)建之后,其存儲(chǔ)的值可以動(dòng)態(tài)修改。

          像整型就是一個(gè)不可變對(duì)象。

          >>>?a?=?666
          >>>?id(a)
          1365442984464
          >>>?a?+=?1
          >>>?id(a)
          1365444032848
          >>>

          我們看到在對(duì) a 執(zhí)行+1操作時(shí),前后地址發(fā)生了變化,所以整型不支持本地修改,因此是一個(gè)不可變對(duì)象;

          原來(lái)a = 666,而我們說(shuō)操作一個(gè)變量等于操作這個(gè)變量指向的內(nèi)存,所以a+=1,會(huì)將a指向的整型對(duì)象666和1進(jìn)行加法運(yùn)算,得到667。所以會(huì)開(kāi)辟新的空間來(lái)存儲(chǔ)這個(gè)667,然后讓a指向這片新的空間,至于原來(lái)的666所占的空間怎么辦,Python 解釋器會(huì)看它的引用計(jì)數(shù),如果不為0代表還有變量引用(指向)它,如果為0證明沒(méi)有變量引用了,所以會(huì)被回收。

          關(guān)于引用計(jì)數(shù),我們后面會(huì)詳細(xì)說(shuō),目前只需要知道當(dāng)一個(gè)對(duì)象被一個(gè)變量引用的時(shí)候,那么該對(duì)象的引用計(jì)數(shù)就會(huì)加1。有幾個(gè)變量引用,那么它的引用計(jì)數(shù)就是幾。

          可能有人覺(jué)得,每次都要?jiǎng)?chuàng)建新對(duì)象,銷(xiāo)毀舊對(duì)象,效率肯定會(huì)很低吧。事實(shí)上確實(shí)如此,但是后面我們會(huì)從源碼的角度上來(lái)看 Python 如何通過(guò)小整數(shù)對(duì)象池等手段進(jìn)行優(yōu)化。

          而列表是一個(gè)可變對(duì)象,它是可以修改的。

          這里先多提一句,Python中的對(duì)象本質(zhì)上就是C中malloc函數(shù)為結(jié)構(gòu)體實(shí)例在堆區(qū)申請(qǐng)的一塊內(nèi)存。Python中的任何對(duì)象在C中都會(huì)對(duì)應(yīng)一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體除了存放具體的值之外,還存放了一些額外的信息,這個(gè)我們?cè)谄饰鯬ython中的內(nèi)置類(lèi)型的實(shí)例對(duì)象的時(shí)候會(huì)細(xì)說(shuō)。

          首先Python中列表,當(dāng)然不光是列表,還有元組、集合,這些容器它們的內(nèi)部存儲(chǔ)的也不是具體的對(duì)象,而是對(duì)象的指針。比如:lst = [1, 2, 3],你以為lst存儲(chǔ)的是三個(gè)整型對(duì)象嗎?其實(shí)不是的,lst存儲(chǔ)的是三個(gè)整型對(duì)象的指針,當(dāng)我們使用lst[0]的時(shí)候,拿到的是第一個(gè)元素的指針,但是操作(比如print)的時(shí)候會(huì)自動(dòng)操作(print)指針指向的內(nèi)存。

          不知道你是否思考過(guò),Python底層是C來(lái)實(shí)現(xiàn)的,所以Python中的列表的實(shí)現(xiàn)必然要借助C中的數(shù)組。可我們知道C中的數(shù)組里面的所有元素的類(lèi)型必須一致,但列表卻可以存放任意的元素,因此從這個(gè)角度來(lái)講,列表里面的元素它就就不可能是對(duì)象,因?yàn)椴煌膶?duì)象在底層對(duì)應(yīng)的結(jié)構(gòu)體是不同的,所以這個(gè)元素只能是指針。

          可能有人又好奇了,不同對(duì)象的指針也是不同的啊,是的,但C中的指針是可以轉(zhuǎn)化的。Python底層將所有對(duì)象的指針,都轉(zhuǎn)成了 PyObject 的指針,這樣不就是同一種類(lèi)型的指針了嗎?關(guān)于這個(gè)PyObject,它是我們后面要剖析的重中之重,這個(gè)PyObject貫穿了我們的整個(gè)系列。目前只需要知道Python中的列表存儲(chǔ)的值,在底層是通過(guò)一個(gè) PyObject * 類(lèi)型的數(shù)據(jù)來(lái)維護(hù)的。

          >>>?lst?=?[1,?2,?3]
          >>>?id(lst)
          1365442893952
          >>>?lst.append(4)
          >>>?lst
          [1,?2,?3,?4]
          >>>?id(lst)
          1365442893952
          >>>

          我們看到列表在添加元素的時(shí)候,前后地址并沒(méi)有改變。列表在C中是通過(guò)PyListObject實(shí)現(xiàn)的,我們?cè)诮榻B列表的時(shí)候會(huì)細(xì)說(shuō)。這個(gè)PyListObject內(nèi)部除了一些基本信息之外,還有一個(gè)成員叫ob_item,它是一個(gè)PyObject的二級(jí)指針,指向了我們剛才說(shuō)的 PyObject * 類(lèi)型的數(shù)組的首個(gè)元素的地址。

          結(jié)構(gòu)圖如下:

          顯然圖中的指針數(shù)組是用來(lái)存儲(chǔ)具體的對(duì)象的指針的,每一個(gè)指針都指向了相應(yīng)的對(duì)象(這里是整型對(duì)象)。可能有人注意到,整型對(duì)象的順序有點(diǎn)怪,其實(shí)我是故意這么畫(huà)的。因?yàn)?PyObject * 數(shù)組內(nèi)部的元素是連續(xù)且有順序的,但是指向的整型對(duì)象則是存儲(chǔ)在堆區(qū)的,它們的位置是任意性的。但是不管這些整型對(duì)象存儲(chǔ)在堆區(qū)的什么位置,它們和數(shù)組中的指針都是一一對(duì)應(yīng)的,我們通過(guò)索引是可以正確獲取到指向的對(duì)象的。

          另外我們還可以看到一個(gè)現(xiàn)象,那就是Python中的列表在底層是分開(kāi)存儲(chǔ)的,因?yàn)镻yListObject結(jié)構(gòu)體實(shí)例并沒(méi)有存儲(chǔ)相應(yīng)的指針數(shù)組,而是存儲(chǔ)了指向這個(gè)指針數(shù)組的二級(jí)指針。顯然我們添加、刪除、修改元素等操作,都是通過(guò)這個(gè)二級(jí)指針來(lái)間接操作這個(gè)指針數(shù)組。

          為什么要這么做?

          因?yàn)樵?Python 中一個(gè)對(duì)象一旦被創(chuàng)建,那么它在內(nèi)存中的大小就不可以變了。所以這就意味著那些可以容納可變長(zhǎng)度數(shù)據(jù)的可變對(duì)象,要在內(nèi)部維護(hù)一個(gè)指向可變大小的內(nèi)存區(qū)域的指針。而我們看到 PyListObject 正是這么做的,指針數(shù)組的長(zhǎng)度、內(nèi)存大小是可變的,所以 PyListObject 內(nèi)部并沒(méi)有直接存儲(chǔ)它,而是存儲(chǔ)了指向它的二級(jí)指針。但是 Python 在計(jì)算內(nèi)存大小的時(shí)候是會(huì)將這個(gè)指針數(shù)組也算進(jìn)去的,所以 Python 中列表的大小是可變的,但是底層對(duì)應(yīng)的 PyListObject 實(shí)例的大小是不變的,因?yàn)榭勺冮L(zhǎng)度的指針數(shù)組沒(méi)有存在 PyListObject 里面。但為什么要這么設(shè)計(jì)呢?

          這么做的原因就在于,遵循這樣的規(guī)則可以使通過(guò)指針維護(hù)對(duì)象的工作變得非常簡(jiǎn)單。一旦允許對(duì)象的大小可在運(yùn)行期改變,那么我們就可以考慮如下場(chǎng)景。在內(nèi)存中有對(duì)象A,并且其后面緊跟著對(duì)象B。如果運(yùn)行的某個(gè)時(shí)候,A的大小增大了,這就意味著必須將A整個(gè)移動(dòng)到內(nèi)存中的其他位置,否則A增大的部分會(huì)覆蓋掉原本屬于B的數(shù)據(jù)。只要將A移動(dòng)到內(nèi)存的其他位置,那么所有指向A的指針就必須立即得到更新。可想而知這樣的工作是多么的繁瑣,而通過(guò)一個(gè)指針去操作就變得簡(jiǎn)單多了。

          定長(zhǎng)對(duì)象與變長(zhǎng)對(duì)象

          Python 中一個(gè)對(duì)象占用的內(nèi)存有多大呢?相同類(lèi)型的實(shí)例對(duì)象的大小是否相同呢?試一下就知道了,我們可以通過(guò) sys 模塊中 getsizeof 函數(shù)查看一個(gè)對(duì)象所占的內(nèi)存。

          import?sys

          print(sys.getsizeof(0))??#?24
          print(sys.getsizeof(1))??#?28
          print(sys.getsizeof(2?<33))??#?32


          print(sys.getsizeof(0.))??#?24
          print(sys.getsizeof(3.14))??#?24
          print(sys.getsizeof((2?<33)?+?3.14))??#?24

          我們看到整型對(duì)象的大小不同,所占的內(nèi)存也不同,像這種內(nèi)存大小不固定的對(duì)象,我們稱(chēng)之為變長(zhǎng)對(duì)象;而浮點(diǎn)數(shù)所占的內(nèi)存都是一樣的,像這種內(nèi)存大小固定的對(duì)象,我們稱(chēng)之為定長(zhǎng)對(duì)象。

          至于 Python 是如何計(jì)算對(duì)象所占的內(nèi)存,我們?cè)谄饰鼍唧w對(duì)象的時(shí)候會(huì)說(shuō),因?yàn)檫@要涉及到底層對(duì)應(yīng)的結(jié)構(gòu)體。

          而且我們知道 Python 中的整數(shù)是不會(huì)溢出的,而C中的整型顯然是有最大范圍的,那么Python是如何做到的呢?答案是Python在底層是通過(guò)C的32位整型數(shù)組來(lái)存儲(chǔ)自身的整型對(duì)象的,通過(guò)多個(gè)32位整型組合起來(lái),以支持存儲(chǔ)更大的數(shù)值,所以整型越大,就需要越多的32位整數(shù)。而32位整數(shù)是4字節(jié),所以我們上面代碼中的那些整型,都是4字節(jié)、4字節(jié)的增長(zhǎng)。

          當(dāng)然Python中的對(duì)象在底層都是一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中除了維護(hù)具體的值之外,還有其它的成員信息,在計(jì)算內(nèi)存大小的時(shí)候,它們也是要考慮在內(nèi)的,當(dāng)然這些我們后面會(huì)說(shuō)。

          而浮點(diǎn)數(shù)的大小是不變的,因?yàn)镻ython的浮點(diǎn)數(shù)的值在C中是通過(guò)一個(gè)double來(lái)維護(hù)的。而C中的值的類(lèi)型一旦確定,大小就不變了,所以Python的float也是不變的。

          但是既然是固定的類(lèi)型,肯定范圍是有限的,所以當(dāng)浮點(diǎn)數(shù)不斷增大,會(huì)犧牲精度來(lái)進(jìn)行存儲(chǔ)。如果實(shí)在過(guò)大,那么會(huì)拋出OverFlowError。

          >>>?int(1000000000000000000000000000000000.)??#?犧牲了精度
          999999999999999945575230987042816
          >>>?10?**?1000??#?不會(huì)溢出
          1000000000000000......
          >>>
          >>>?10.?**?1000??#?報(bào)錯(cuò)了
          Traceback?(most?recent?call?last):
          ??File?"",?line?1,?in?
          OverflowError:?(34,?'Result?too?large')
          >>>

          還有字符串,字符串毫無(wú)疑問(wèn)肯定是可變對(duì)象,因?yàn)殚L(zhǎng)度不同大小不同。

          import?sys

          print(sys.getsizeof("a"))??#?50
          print(sys.getsizeof("abc"))??#?52

          我們看到多了兩個(gè)字符,多了兩個(gè)字節(jié),這很好理解。但是這些說(shuō)明了一個(gè)空字符串要占49個(gè)字節(jié),我們來(lái)看一下。

          import?sys

          print(sys.getsizeof(""))??#?49

          顯然是的,顯然這 49 個(gè)字節(jié)是用來(lái)維護(hù)其它成員信息的,因?yàn)榈讓拥慕Y(jié)構(gòu)體除了維護(hù)具體的值之外,還要維護(hù)其它的信息,比如:引用計(jì)數(shù)等等,這些在分析源碼的時(shí)候會(huì)詳細(xì)說(shuō)。

          小結(jié)

          我們這一節(jié)介紹了 Python 中的對(duì)象體系,我們說(shuō) Python 中一切皆對(duì)象,類(lèi)型對(duì)象和實(shí)例對(duì)象都屬于對(duì)象;還說(shuō)了對(duì)象的種類(lèi),根據(jù)是否支持本地修改可以分為可變對(duì)象和不可變對(duì)象,根據(jù)占用的內(nèi)存是否不變可以分為定長(zhǎng)對(duì)象和變長(zhǎng)對(duì)象;還說(shuō)了 Python 中變量的本質(zhì),Python 中的變量本質(zhì)上是一個(gè)指針,而變量的名字則存儲(chǔ)在對(duì)應(yīng)的名字空間(或者說(shuō)命名空間)中,當(dāng)然名字空間我們沒(méi)有說(shuō),是因?yàn)檫@些在后續(xù)系列會(huì)詳細(xì)說(shuō)(又是后續(xù), 不管咋樣, 坑先挖出來(lái)),不過(guò)這里可以先補(bǔ)充一下。

          名字空間分為:全局名字空間(存儲(chǔ)全局變量)、局部名字空間(存儲(chǔ)局部變量)、閉包名字空間(存儲(chǔ)閉包變量)、內(nèi)建名字空間(存儲(chǔ)內(nèi)置變量, 比如 int、str, 它們都在這里),而名字空間又分為靜態(tài)名字空間和動(dòng)態(tài)名字空間:比如局部名字空間,因?yàn)楹瘮?shù)中的局部變量在編譯的時(shí)候就可以確定,所以函數(shù)對(duì)應(yīng)的局部名字空間使用一個(gè)數(shù)組存儲(chǔ);而全局變量在運(yùn)行時(shí)可以進(jìn)行動(dòng)態(tài)添加、刪除,因此全局名字空間使用的是一個(gè)字典來(lái)保存,字典的 key 就是變量的名字(依舊是個(gè)指針,底層是指向字符串(PyUnicodeObject)的指針),字典的 value 就是變量指向的對(duì)象的指針(或者說(shuō)變量本身)。

          a?=?123
          b?=?"xxx"

          #?通過(guò)globals()即可獲取全局名字空間
          print(globals())??#{...,?'a':?123,?'b':?'xxx'}

          #?我們看到雖然顯示的是變量名和變量指向的值
          #?但是在底層,字典存儲(chǔ)的鍵值對(duì)也是指向具體對(duì)象的指針
          #?只不過(guò)我們說(shuō)操作指針會(huì)操作指向的內(nèi)存,所以這里print打印之后,顯示的也是具體的值,但是存儲(chǔ)的是指針
          #?至于對(duì)象本身,則存儲(chǔ)在堆區(qū),并且被指針指向

          #??此外,我們往全局名字空間中設(shè)置一個(gè)鍵值對(duì),也等價(jià)于創(chuàng)建了一個(gè)全局變量
          globals()["c"]?=?"hello"
          print(c)??#?hello

          #?此外這個(gè)全局名字空間是唯一的,即使你把它放在函數(shù)中也是一樣
          def?foo():
          ????globals()["d"]?=?"古明地覺(jué)"

          #?foo一旦執(zhí)行,{"d":?"古明地覺(jué)"}就設(shè)置進(jìn)了全局名字空間中
          foo()??
          print(d)??#?古明地覺(jué)
          怎么樣,是不是有點(diǎn)神奇呢?所以名字空間是 Python 作用域的靈魂,它嚴(yán)格限制了變量的活動(dòng)范圍,當(dāng)然這些后面都會(huì)慢慢的說(shuō),因?yàn)轱堃豢谝豢诔浴R虼诉@一節(jié)算是回顧基礎(chǔ)吧,雖說(shuō)是基礎(chǔ)但是其實(shí)也涉及到了一些解釋器的知識(shí),不過(guò)這一關(guān)我們遲早是要過(guò)的,所以就提前接觸一下吧。
          Python貓技術(shù)交流群開(kāi)放啦!群里既有國(guó)內(nèi)一二線大廠在職員工,也有國(guó)內(nèi)外高校在讀學(xué)生,既有十多年碼齡的編程老鳥(niǎo),也有中小學(xué)剛剛?cè)腴T(mén)的新人,學(xué)習(xí)氛圍良好!想入群的同學(xué),請(qǐng)?jiān)诠?hào)內(nèi)回復(fù)『交流群』,獲取貓哥的微信(謝絕廣告黨,非誠(chéng)勿擾!)~

          近期熱門(mén)文章推薦:

          腦洞:如何用一個(gè)整數(shù)來(lái)表示一個(gè)列表?
          Python 源碼混淆與加密
          為什么說(shuō) Python 內(nèi)置函數(shù)并不是萬(wàn)能的?
          Python——gRPC詳解及實(shí)戰(zhàn)避坑方案(上)

          感謝創(chuàng)作者的好文
          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本女优中文字幕在线观看 | 夜夜爽7777精品国产三级 | sm调教视频网站 | 黄色一级久久久精品 | 韩日色网 |