成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

解密 Python 的變量和對象,它們之間有什么區別和聯系呢?

開發 前端
盡管 Python 一切皆對象,但你拿到的都是對象的指針,變量是一個指針,函數是一個指針,元組、列表、字典里面存儲的還是指針。

Python 中一切皆對象

在學習 Python 的時候,你肯定聽過這么一句話:Python 中一切皆對象。沒錯,在 Python 世界里,一切都是對象。整數是一個對象、字符串是一個對象、字典是一個對象,甚至 int, str, list 以及我們使用 class 關鍵字自定義的類,它們也是對象。

像 int, str, list 等基本類型,以及自定義的類,由于它們可以表示類型,因此我們稱之為類型對象;類型對象實例化得到的對象,我們稱之為實例對象。但不管是哪種對象,它們都屬于對象。

因此 Python 將面向對象理念貫徹的非常徹底,面向對象中的類和對象在 Python 中都是通過對象實現的。

在面向對象理論中,存在著類和對象兩個概念,像 int、dict、tuple、以及使用 class 關鍵字自定義的類型對象實現了面向對象理論中類的概念,而 123、3.14,"string" 等等這些實例對象則實現了面向對象理論中對象的概念。但在 Python 里面,面向對象的類和對象都是通過對象實現的。

我們舉個例子:

# dict 是一個類,因此它屬于類型對象
# 類型對象實例化得到的對象屬于實例對象
print(dict)
"""
<class 'dict'>
"""
print(dict(a=1, b=2))
"""
{'a': 1, 'b': 2}
"""

因此可以用一張圖來描述面向對象在 Python 中的體現。

圖片圖片

而如果想查看一個對象的類型,可以使用 type,或者通過對象的 __class__ 屬性。

numbers = [1, 2, 3]
# 查看類型
print(type(numbers))
"""
<class 'list'>
"""
print(numbers.__class__)
"""
<class 'list'>
"""

如果想判斷一個對象是不是指定類型的實例對象,可以使用 isinstance。

numbers = [1, 2, 3]
# 判斷是不是指定類型的實例對象
print(isinstance(numbers, list))
"""
True
"""

但是問題來了,按照面向對象的理論來說,對象是由類實例化得到的,這在 Python 中也是適用的。既然是對象,那么就必定有一個類來實例化它,換句話說對象一定要有類型。

至于一個對象的類型是什么,就看這個對象是被誰實例化的,被誰實例化那么類型就是誰,比如列表的類型是 list,字典的類型是 dict 等等。

而 Python 中一切皆對象,所以像 int, str, tuple 這些內置的類對象也是具有相應的類型的,那么它們的類型又是誰呢?使用 type 查看一下就知道了。

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

我們看到類型對象的類型,無一例外都是 type。而 type 我們也稱其為元類,表示類型對象的類型。至于 type 本身,它的類型還是 type,所以它連自己都沒放過,把自己都變成自己的對象了。

因此在 Python 中,你能看到的任何對象都是有類型的,可以使用 type 查看,也可以獲取該對象的 __class__ 屬性查看。所以:實例對象、類型對象、元類,Python 中任何一個對象都逃不過這三種身份。

到這里可能有人會發現一個有意思的點,我們說 int 是一個類對象,這顯然是沒有問題的。因為站在整數(比如 123)的角度上,int 是一個不折不扣的類對象;但如果站在 type 的角度上呢?顯然我們又可以將 int 理解為實例對象,因此 class 具有二象性。

至于 type 也是同理,雖然它是元類,但本質上也是一個類對象。

注:不僅 type 是元類,那些繼承了 type 的類也可以叫做元類。

然后 Python 中還有一個關鍵的類型(對象),叫做 object,它是所有類型對象的基類。不管是什么類,內置的類也好,我們自定義的類也罷,它們都繼承自 object。因此 object 是所有類型對象的基類、或者說父類。

那如果我們想獲取一個類都繼承了哪些基類,該怎么做呢?方式有三種:

class A: pass

class B: pass

class C(A): pass

class D(B, C): pass

# 首先 D 繼承自 B 和 C, C 又繼承 A
# 我們現在要來查看 D 繼承的父類

# 方法一: 使用 __base__
print(D.__base__)  
"""
<class '__main__.B'>
"""

# 方法二: 使用 __bases__
print(D.__bases__)  
"""
(<class '__main__.B'>, <class '__main__.C'>)
"""

# 方法三: 使用 __mro__
print(D.__mro__)
"""
(<class '__main__.D'>, <class '__main__.B'>, 
 <class '__main__.C'>, <class '__main__.A'>, 
 <class 'object'>)
"""
  • __base__:如果繼承了多個類,那么只顯示繼承的第一個類,沒有顯式繼承則返回 <class 'object'>
  • __bases__:返回一個元組,會顯示所有直接繼承的父類,沒有顯式繼承則返回 (<class 'object'>,)
  • __mro__: mro(Method Resolution Order)表示方法查找順序,會從自身出發,找到最頂層的父類。因此返回自身、繼承的基類、以及基類繼承的基類, 一直找到 object

而如果想查看某個類型是不是另一個類型的子類,可以通過 issubclass。

print(issubclass(str, object))
"""
True
"""

因此,我們可以得出以下兩個結論:

  • type 站在類型金字塔的最頂端,任何一個對象按照類型追根溯源,最終得到的都是 type;
  • object 站在繼承金字塔的最頂端,任何一個類型對象按照繼承關系追根溯源,最終得到的都是 object;

但要注意的是,我們說 type 的類型還是 type,但 object 的基類則不再是 object,而是 None。

print(
    type.__class__
)  # <class 'type'>

# 注:以下打印結果容易讓人產生誤解
# 它表達的含義是 object 的基類為空
# 而不是說 object 繼承 None
print(
    object.__base__
)  # None

但為什么 object 的基類是 None,而不是它自身呢?其實答案很簡單,Python 在查找屬性或方法的時候,自身如果沒有的話,會按照 __mro__ 指定的順序去基類中查找。所以繼承鏈一定會有一個終點,否則就會像沒有出口的遞歸一樣出現死循環了。

我們用一張圖將對象之間的關系總結一下:

圖片圖片

  • 實例對象的類型是類型對象,類型對象的類型是元類;
  • 所有類型對象的基類都收斂于 object;
  • 所有對象的類型都收斂于 type;

因此 Python 算是將一切皆對象的理念貫徹到了極致,也正因為如此,Python 才具有如此優秀的動態特性。

但是還沒結束,我們再重新審視一下上面那張圖,會發現里面有兩個箭頭看起來非常的奇怪。object 的類型是 type,type 又繼承了 object。

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

因為 type 是所有類的元類,而 object 是所有類的基類,這就說明 type 要繼承自 object,而 object 的類型是 type。很多人都會對這一點感到奇怪,這難道不是一個先有雞還是先有蛋的問題嗎?其實不是的,這兩個對象是共存的,它們之間的定義其實是互相依賴的。而具體是怎么一回事,我們后續分析。

Python 的變量其實是指針

Python 的變量只是一個名字,如果站在 C 語言的角度來看,那么就是一個指針。所以 Python 的變量保存的其實是對象的內存地址,或者說指針,而指針指向的內存存儲的才是對象。

所以在 Python 中,我們都說變量指向了某個對象。在其它靜態語言中,變量相當于是為某塊內存起的別名,獲取變量等于獲取這塊內存所存儲的值。而 Python 中變量代表的內存所存儲的不是對象,而是對象的指針(或者說引用)。

我們舉例說明,看一段 C 代碼。

#include <stdio.h>

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

    a = 667;
    printf("address of a = %p\n", &a);
}

編譯執行一下:

圖片圖片

賦值前后地址都是 0x7fff9eda521c,沒有變化,再來看一段 Python 代碼。

a = 666
print(hex(id(a)))  # 0x7febf803a3d0

a = 667
print(hex(id(a)))  # 0x7fec180677b0

我們看到 Python 里面輸出的地址發生了變化,下面分析一下原因。

首先在 C 中,創建一個變量的時候必須規定好類型,比如 int a = 666,那么變量 a 就是 int 類型,以后在所處的作用域中就不可以變了。如果這時候再設置 a = 777,那么等于是把內存中存儲的 666 換成 777,a 的地址和類型是不會變化的。

而在 Python 中,a = 666 等于是先開辟一塊內存,存儲的值為 666,然后讓變量 a 指向這片內存,或者說讓變量 a 保存這塊內存的地址。然后 a = 777 的時候,再開辟一塊內存,然后讓 a 指向存儲 777 的內存,由于是兩塊不同的內存,所以它們的地址是不一樣的。

圖片圖片

所以 Python 的變量只是一個和對象關聯的名字,它代表的是對象的指針。換句話說 Python 的變量就是個便利貼,可以貼在任何對象上,一旦貼上去了,就代表這個對象被引用了。

值傳遞?引用傳遞?

再來看看變量之間的傳遞,在 Python 中是如何體現的。

a = 666
print(hex(id(a)))  # 0x1f4e8ca7fb0

b = a
print(hex(id(b)))  # 0x1f4e8ca7fb0

我們看到打印的地址是一樣的,再用一張圖解釋一下。

圖片圖片

a = 666 的時候,先開辟一份內存,再讓 a 存儲對應內存的地址;然后 b = a 的時候,會把 a 拷貝一份給 b,所以 b 和 a 存儲了相同的地址,它們都指向了同一個對象。

因此說 Python 是值傳遞、或者引用傳遞都是不準確的,準確的說 Python 是變量的值傳遞,對象的引用傳遞。因為 Python 的變量可以認為是 C 的一個指針,在 b = a 的時候,等于把 a 指向的對象的地址(a 本身)拷貝一份給 b,所以對于變量來說是值傳遞;然后 a 和 b 又都是指向對象的指針,因此對于對象來說是引用傳遞。

在這個過程中,對象沒有重復創建,它只是多了一個引用。

另外還有最關鍵的一點,Python 的變量是一個指針,當傳遞變量的時候,傳遞的是指針;但是在操作變量的時候,會操作變量指向的內存。所以 id(a) 獲取的不是 a 的地址,而是 a 指向的內存的地址(在底層其實就是 a 本身);同理 b = a,是將 a 本身,或者說將 a 存儲的、指向某個具體的對象的地址傳遞給了 b。

另外在 C 的層面,顯然 a 和 b 屬于指針變量,那么 a 和 b 有沒有地址呢?顯然是有的,只不過在 Python 中是獲取不到的,解釋器只允許獲取對象的地址。

我們再舉個函數的例子:

def some_func(num):
    print("address of local num", hex(id(num)))
    num = 667
    print("address of local num", hex(id(num)))

num = 666
print("address of global num", hex(id(num)))
some_func(num)
"""
address of global num 0x2356cd698d0
address of local num 0x2356cd698d0
address of local num 0x2356c457f90
"""

函數的參數也是一個變量,所以 some_func(num) 其實就是把全局變量 num 存儲的對象的地址拷貝一份給局部變量 num,所以兩個 num 指向了同一個對象,打印的地址相同。

然后在函數內部執行 num = 667,相當于讓局部變量指向新的對象,或者說保存新對象的地址,因此打印的結果發生變化。

變量有類型嗎?

當提到類型時,這個類型指的是變量的類型還是對象的類型呢?不用想,肯定是對象的類型。因為 Python 的變量是個指針,操作指針會自動操作它指向的內存,所以使用 type(a) 查看的其實是變量 a 指向的對象的類型。

那么問題來了,我們在創建變量的時候,并沒有顯式地指定類型啊,那么 Python 是如何判斷一個變量指向的是什么類型的數據呢?答案是:解釋器是通過靠猜的方式,通過賦的值(或者說變量引用的值)來推斷類型。

因此在 Python 中,如果你想創建一個變量,那么必須在創建變量的時候同時賦值,否則解釋器就不知道這個變量指向的數據是什么類型。所以 Python 是先創建相應的值,這個值在 C 中對應一個結構體,結構體里面有一個成員專門用來存儲該值對應的類型,因此在 Python 中,類型是和對象綁定的,而不是和變量。當創建完值之后,再讓這個變量指向它,所以 Python 中是先有值后有變量。

但顯然在 C 里面不是這樣的,因為 C 的變量代表的內存所存儲的就是具體的值,所以在 C 里面可以直接聲明一個變量的同時不賦值。因為 C 要求聲明變量時必須指定類型,因此變量聲明之后,其類型和內存大小就已經固定了。

而 Python 的變量存的是個地址,它只是指向了某個對象,所以由于其便利貼的特性,可以貼在任意對象上面。但是不管貼在哪個對象,都必須先有對象才可以,不然變量貼誰去。

另外,盡管 Python 在創建變量的時候不需要指定類型,但 Python 是強類型語言,而且是動態強類型。

小結

以上我們就聊了聊 Python 的變量和對象,核心就在于:變量保存的不是對象本身,而是對象的內存地址,站在 C 的角度上看變量就是一個指針。

盡管 Python 一切皆對象,但你拿到的都是對象的指針,變量是一個指針,函數是一個指針,元組、列表、字典里面存儲的還是指針。我們可以想象一下列表,它底層是基于數組實現的,由于 C 數組要求里面的每個元素的類型和大小都相同,因此從這個角度上講,列表內部存儲的只能是指針。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2018-01-16 00:11:18

數據中心云計算大數據

2020-09-08 11:00:00

IaaSPaaSSaaS

2025-04-09 08:15:00

分布式系統微服務架構

2022-07-18 14:05:08

云計算邊緣計算IT

2020-06-02 10:28:17

機器學習技術人工智能

2023-07-19 21:54:02

小區扇區信號

2018-10-23 09:13:24

程序員學歷求職者

2023-10-07 00:26:09

2021-09-08 05:52:57

工業物聯網物聯網IOT

2021-02-25 10:07:42

人工智能AI機器學習

2022-08-31 08:33:54

Bash操作系統Linux

2021-12-17 14:40:02

while(1)for(;;)語言

2022-02-27 15:33:22

安全CASBSASE

2024-05-27 00:40:00

2021-05-16 14:26:08

RPAIPACIO

2024-09-09 13:10:14

2024-03-05 18:59:59

前端開發localhost

2022-08-02 08:23:37

SessionCookies

2021-09-10 17:02:43

Python協程goroutine

2024-01-05 08:31:08

C語言
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品成人久久久久 | av一区二区在线观看 | 精品亚洲一区二区 | 欧美午夜视频 | 亚洲综合无码一区二区 | 久久久久久亚洲精品 | 国产精品视频在线免费观看 | 亚洲欧美网站 | 五月天婷婷综合 | 在线一区视频 | 涩涩操| 日韩三区| 国产精品观看 | 亚洲性人人天天夜夜摸 | 国产一区三区在线 | 一区在线免费视频 | 能看的av| 99视频在线免费观看 | www.色婷婷 | 成人在线一区二区 | 天天草狠狠干 | 日韩欧美国产精品一区二区 | 男女羞羞视频网站 | 欧美精品v国产精品v日韩精品 | 成人精品视频99在线观看免费 | 欧美电影网 | 老熟女毛片| 亚洲激情在线观看 | 懂色中文一区二区在线播放 | 亚洲欧美一区二区三区1000 | 欧美xxxx做受欧美 | 亚洲毛片一区二区 | 国产精品久久久久久久久久久久 | 91在线视频观看免费 | 国产在线视频一区二区 | 看av片网站 | 99视频在线免费观看 | a级黄色毛片免费播放视频 国产精品视频在线观看 | 午夜影院网站 | 成人黄色网址大全 | 琪琪午夜伦伦电影福利片 |