當前位置:
首頁 > 知識 > Python的指針:有什麼意義?

Python的指針:有什麼意義?

目錄

  • 為什麼Python沒有指針?
  • Python中的對象
  • 不可變對象和可變對象
  • 了解變數
  • C的變數
  • Python的名稱
  • 關於Python的預實現對象的注釋
  • 在Python中模擬指針
  • 使用可變類型作為指針
  • 使用Python對象
  • 使用ctypes模塊實現的真正指針
  • 總結

如果您曾經使用過像C或者C++這樣的低級語言,那麼您可能聽說過指針。指針允許您在部分代碼上取得更高的效率。但它們也會給初學者帶來困惑,而且還可能導致各種內存管理錯誤,即使對於專家來說也會如此。那麼它們在Python的哪裡?您該如何在Python中模擬指針?

指針在C和C++中廣泛應用。從本質上來說,它們是保存另一個變數內存地址的變數。有關指針的複習,您可以考慮看一下這篇關於C語言指針的概述。

通過本文,您會更好地理解Python的對象模型,同時明白為什麼Python中不存在真正的指針。對於需要模仿指針行為的情況,您將學習到如何在沒有內存管理噩夢的情況下在Python中模擬指針。

在本文中,您將

· 了解為什麼Python中的指針不存在

· 探索C變數和Python名稱之間的區別

· 在Python中模擬指針

· 使用ctypes實現真正的指針

注意:在本文中,「Python」會涉及C中Python的參考實現,也稱作CPython。當文章在討論該語言的一些內部結構時,這些注釋適用於CPython 3.7,但在將來或過去的語言迭代中可能不適用。


為什麼Python沒有指針?

事實上,我並不知道答案。Python中的指針本身可以存在嗎?可能,但指針似乎違背了Python的禪宗。指針鼓勵隱含的變化而不是明確的變化。通常,它們很複雜而不是簡單,特別是對於初學者。更糟糕的是,他們會導致你自作自受,或者做一些非常危險的事情,比如從您不被允許的一段內存中讀取數據。

Python傾向於嘗試從用戶那裡抽象出內存地址等實現細節。Python通常關注可用性而不是速度。因此,Python中的指針並沒有多大意義。但不要害怕,默認情況下,Python會為您提供使用指針的一些好處。

理解Python中的指針需要簡要介紹Python的實現細節。具體來說,您需要了解:

1.不可變對象和可變對象

2.Python變數/名稱

保留你的內存地址,讓我們開始吧。


Python中的對象

在Python中,一切都是對象。為了證明,你可以打開一個REPL並嘗試使用isinstance():

Python的指針:有什麼意義?

打開今日頭條,查看更多圖片

此代碼向您顯示Python中的所有內容確實是一個對象。每個對象至少包含三個數據:

?引用計數

?類型

?值

引用計數用於內存管理。要深入了解Python內存管理的內核,您可以閱讀Python中的內存管理。

類型在CPython層使用,用於確保運行時的類型安全性。最後,值,即與對象關聯的實際值。

但並非所有對象都是相同的。您還需要了解另一個重要的區別:不可變對象和可變對象。理解對象類型之間的差異確實有助於闡明Python中的指針的第一層。

不可變對象和可變對象

在Python中,有兩種類型的對象;

1. 不可變對象無法更改

2. 可變對象可以更改

理解這種差異是認識Python指針的第一個關鍵。以下是常見類型的細分以及它們是否可變或不可變:

Python的指針:有什麼意義?

如您所見,許多常用的基元類型是不可變的。您可以通過編寫一些Python來證明這一點。您需要Python標準庫中的一些工具:

1.id() 返回對象的內存地址。

2.is 當且僅當兩個對象具有相同的內存地址時才返回True。

再次,您可以在REPL環境中使用運行以下代碼:

Python的指針:有什麼意義?

在上面的代碼中,您已經將5賦給x。如果您嘗試使用add修改此值,那麼您將獲得一個新對象:

Python的指針:有什麼意義?

上面的代碼似乎修改了x的值,但您得到了一個新對象作為響應。

str類型也是不變的:

Python的指針:有什麼意義?

同樣,經過「+=」操作後s最終會有不同的內存地址。

福利:「+=」操作會轉換成不同的方法調用。

對於某些對象,如list對象,+=將轉換為__iadd__()(就地添加)。這將修改self並返回相同的ID。但是,str和int對象沒有這些方法,這就導致它們調用的是__add__()而不是__iadd__()。

有關更多詳細信息,請查看Python 數據模型文檔。

試圖直接改變字元串s會導致錯誤:

Python的指針:有什麼意義?

上面的代碼失敗了,這表明str不支持這種突變,這與str類型是不可變的定義一致。

與可變對象作對比,例如list類型:

Python的指針:有什麼意義?

此代碼顯示了兩種類型對象的主要區別。」my_list「最初有一個id。即使在4被附加到列表後,」my_list「也具有相同的 ID。這是因為list類型是可變的。

證明列表可變的另一種方法是賦值:

Python的指針:有什麼意義?

在此代碼中,您可以改變「my_list」,將其第一個元素設置為0。但是,即使在賦值之後,它仍保持原有的ID。隨著可變和不可變對象的出現,Python啟蒙之旅的下一步是理解Python的變數生態系統。


了解變數

Python變數在根本上與C或C ++中的變數不同。事實上,Python甚至沒有變數。Python有名稱,而不是變數。

這可能看起來很迂腐,而且在大多數情況下就是迂腐。大多數時候,將Python名稱視為變數是完全可以接受的,但理解差異很重要。當您在Python中探尋棘手的指針主題時尤為重要。

為了幫助理解差異,您可以了解變數如何在C中工作,它們代表什麼,然後將其與名稱在Python中的工作方式進行對比。


C中的變數

假設您用以下代碼來定義變數x:

Python的指針:有什麼意義?

這一行代碼在執行時有幾個不同的步驟:

1. 為整數分配足夠的內存

2. 將值分配2337給該內存位置

3. 指示x指向該值

以簡化的內存視圖顯示,它可能如下所示:

Python的指針:有什麼意義?

在這裡,您可以看到該變數x具有偽內存位置0x7f1和值2337。如果在程序中稍後要更改其x的值,則可以執行以下操作:

Python的指針:有什麼意義?

上面的代碼給變數x分配了一個新的值2338,從而覆蓋了以前的值。這意味著變數x是可變的。更新的內存布局顯示新值:

Python的指針:有什麼意義?

請注意,x的位置沒有改變,只是改變了值。這是一個重要的觀點。這意味著x 是內存位置,而不僅僅是名稱。

另一種思考這個概念的方法是在所有權方面。從某種意義上說,x擁有內存位置。首先,x恰好是一個可以存儲整數的空盒子,可以用來存儲整數值。

當您給x賦值時,您將向x擁有的盒子中放入一個值。如果你想引入一個新的變數(y),你可以添加這行代碼:

Python的指針:有什麼意義?

此代碼創建一個名為y的盒子,並將x的值複製到y盒子中。現在內存布局將如下所示:

Python的指針:有什麼意義?

注意新位置0x7f5的y。即使將x的值複製到y,但是變數y在內存中擁有新地址。因此,您可以覆蓋y的值而不影響x的值:

Python的指針:有什麼意義?

現在內存布局將如下所示:

Python的指針:有什麼意義?

同樣,你修改的是y的值,而不是它的位置。此外,您始終沒有影響原始的x變數。這與Python名稱的工作方式形成鮮明對比。


Python中的名稱

Python沒有變數。它有名字。是的,這是一個迂腐點,你當然可以隨意使用術語變數。重要的是要知道變數和名稱之間存在差異。

讓我們根據上面的C示例獲取等效代碼並將其寫在Python中:

Python的指針:有什麼意義?

與C類似,上面的代碼在執行過程中分解為幾個不同的步驟:

1.創建一個 PyObject

2.將PyObject的typecode設置為整數 PyObject

3.將PyObject的值設置為2337

4.創建一個名稱 x

5.將x指向新的PyObject

6.將PyObject引用計數增加1

注意:這裡的PyObject與Python的對象不一樣。它於CPython特有的並表示所有Python對象的基本結構。

PyObject被定義為C結構,所以,如果你想知道為什麼你不能調用typecode或refcount,這是因為你沒有許可權直接進入結構。方法調用如sys.getrefcount()可以幫助您獲得一些內部情況。

在內存中,它可能看起來像這樣:

Python的指針:有什麼意義?

您可以看到內存布局與之前的C布局截然不同。在這裡,新創建的Python對象擁有值2337所在的內存,而不是x擁有值2337所在的內存。Python名稱x不直接擁有任何內存地址,不像C變數在內存中擁有靜態插槽。

如果您嘗試為x賦新的值,可以嘗試以下操作:

Python的指針:有什麼意義?

這裡發生的事情與C的同樣操作不同,但與Python中的原始綁定沒有太大區別。

這行代碼:

· 創建一個新的 PyObject

· 將PyObject的typecode設置為整數

· 將PyObject的值設置為2338

· 將x指向新的PyObject

· 將新的PyObject引用計數增加1

· 將舊的PyObject引用計數減少1

現在在內存中,它看起來像這樣:

Python的指針:有什麼意義?

此圖有助於說明x指向對象的引用,並不像以前那樣擁有內存空間。它還表明命令「x = 2338」不是賦值,而是將名稱x綁定到一個引用。

此外,前一個對象(擁有值2337)現在位於內存中,引用計數為0,並將被垃圾收集器清理。

您可以引入一個新名稱y,就如C的示例一樣:

Python的指針:有什麼意義?

在內存中,您將擁有一個新名稱,但不一定是新對象:

Python的指針:有什麼意義?

現在,你可以看到並沒有創建一個新的Python對象,只是創建指向同一個對象的新名稱。此外,對象的引用參數增加了1。您可以檢查對象標識來確認它們是否相同:

Python的指針:有什麼意義?

上面的代碼表明x和y是相同的對象。沒錯:y仍然是不可改變的。

例如,您可以對有y執行以下操作:

Python的指針:有什麼意義?

添加調用後,將返回一個新的Python對象。現在,內存看起來像這樣:

Python的指針:有什麼意義?

一個新對象被創建,y現在指向新對象。有趣的是,如果你已經將2339綁定到y,結束狀態也是如此:

Python的指針:有什麼意義?

上述語句導致與添加相同的結束內存狀態。回顧一下,在Python中,您不需要分配變數。而是將名稱綁定到引用。


關於Python中的預實現對象的注釋

現在您已經了解了如何創建Python對象並將名稱綁定到這些對象,現在是時候在機器中拋出一把扳手了。該扳手叫做預實現對象。

假設您有以下Python代碼:

Python的指針:有什麼意義?

如上所述,x和y這兩個名字都指向同一個Python對象。但是保存1000的Python對象並不能保證總是具有相同的內存地址。例如,如果將兩個數字相加以獲得1000,則最終會得到一個不同的內存地址:

Python的指針:有什麼意義?

這一次,"x is y"返回False。如果這令人困惑,別擔心。以下是執行此代碼時發生的步驟:

1.創建Python對象(1000)

2.將名稱分配x給該對象

3.創建Python對象(499)

4.創建Python對象(501)

5.將這兩個對象一起添加

6.創建一個新的Python對象(1000)

7.將名稱分配y給該對象

技術說明:只有在REPL中執行此代碼時,才會執行上述步驟。如果您採用上面的示例,將其粘貼到一個文件中,然後運行該文件,那麼您會發現"x is y"將返回True。

這是因為編譯器很聰明。CPython編譯器嘗試進行稱為窺孔優化的優化,這有助於儘可能地保存執行步驟。有關詳細信息,您可以查看CPython的窺孔優化器源代碼。

這不是浪費嗎?嗯,是的,這是你為Python所有巨大好處付出的代價。您永遠不必擔心如何清理這些中間對象,甚至都不需要知道它們存在!令人高興的是,這些操作相對較快,並且直到現在你都不需要去理解這些細節。

Python核心開發人員也睿智地注意到了這種浪費,並決定進行一些優化。這些優化產生了令新手感到驚訝的行為:

Python的指針:有什麼意義?

在此示例中,您看到的代碼幾乎與以前相同,除了這次結果是True。這是預實現對象的結果。Python在內存中預先創建了某個對象子集,並將它們保存在全局命名空間中以供日常使用。

哪些對象依賴於Python的預實現。CPython 3.7預實現對象如下:

1.-5到256之間的整數

2.僅包含ASCII字母,數字或下劃線的字元串

這背後的原因是這些變數很可能在許多程序中使用。通過預先實現些對象,Python可以防止對一致使用的對象進行內存分配調用。

預先實現小於20個字元且包含ASCII字母,數字或下劃線的字元串。背後的原因是假設這些字元串是某種身份:

Python的指針:有什麼意義?

在這裡您可以看到s1和s2都指向相同的內存地址。如果您要引入非ASCII字母,數字或下劃線組成的字元串,那麼您將得到不同的結果:

Python的指針:有什麼意義?

因為此示例中包含感嘆號「!」,所以這些字元串不會被預先實現,並且s1和s2是內存中的不同對象。

福利:如果您真的希望這些對象引用相同的內部對象,那麼您可能需要查看sys.intern()。文檔中概述了此功能的一個用例:

預先實現的字元串對於在字典查找中獲得一點性能很有用 - 如果字典中的鍵被預先實現,並且查找鍵被預先實現,則鍵比較(完成在散列之後)就可以通過指針來比較而不是用字元串來比較。(來源)

預實現對象通常是混亂的來源。請記住,如果您有任何疑問,可以隨時使用id()和is確定對象是否相同。


在Python中模擬指針

僅僅因為Python中的指針本身不存在並不意味著你無法獲得使用指針的好處。實際上,可以有多種方法在Python中模擬指針。您將在本節中學習到兩種:

1.使用可變類型作為指針

2.使用自定義Python對象

好的,讓我們進入正題。


使用可變類型作為指針

您已經了解過可變類型。因為這些對象是可變的,所以您可以將它們視作指針,以此來模擬指針行為。假設您複製了以下c代碼:

Python的指針:有什麼意義?

此代碼將一個指針指向一個整數(*x),然後將其值增加1。這有一個運行代碼的主函數:

Python的指針:有什麼意義?

在上面的代碼中,將值2337賦給y,列印出當前值,將值增加1,然後列印出修改後的值。執行此代碼的輸出如下:

Python的指針:有什麼意義?

在Python中模仿此類行為的一種方法是使用可變類型。考慮使用列表並修改第一個元素:

Python的指針:有什麼意義?

在這裡,add_one(x)訪問第一個元素並將其值增加1。通過使用列表,最終似乎已修改了該值。那麼Python中的指針確實存在嗎?好吧,不。唯一的可能是:因為列表是一種可變類型。如果您嘗試使用一個元組,則會收到錯誤消息:

Python的指針:有什麼意義?

上面的代碼演示了元組是不可變的。因此,它不支持項目賦值。列表不是唯一可變的類型。在Python中模仿指針的另一種常見方法是創建字典。

假設您有一個應用程序,您希望每次發生有趣事件時都要跟蹤。實現此目的的另一種方法是創建一個字典 並使用其中的一項作為計數器:

Python的指針:有什麼意義?

在此示例中,counters字典用於跟蹤函數調用的數量。調用foo()函數後,計數器按預期增加到2。這都是因為字典是可變類型。

請記住,這只是模擬指針行為,並不直接映射到C或C ++中的真指針。也就是說,這些操作在Python中會比在C或C ++中付出更多代價。


使用Python對象

使用字典是在Python中模擬指針的一種好方法,但有時您需要記住使用的密鑰名稱,這會很繁瑣。如果您在應用程序的各個部分都使用字典,則尤其如此。這就是自定義Python類可以真正起到作用的地方。

構建最後一個示例,假設您要跟蹤應用程序中的指標。創建一個類是解決那些討厭的抽象細節的好方法:

Python的指針:有什麼意義?

此代碼定義了一個Metrics類。該類仍然使用字典來保存實際數據,該數據位於_metrics成員變數中。這將為您提供所需的可變性。現在您只需要能夠訪問這些值。一個很好的方法是使用屬性:

這段代碼利用了@property。如果您不熟悉裝飾器,可以查看Python裝飾器入門。@property裝飾器允許您訪問func_calls,cat_pictures_served,它們就像屬性一樣:

Python的指針:有什麼意義?

您可以把名稱當作屬性訪問這一事實,意味著您已抽象了一個事實:這些值在字典中。您還可以更明確地指出屬性的名稱是什麼。當然,您應該能夠增加這些值:

Python的指針:有什麼意義?

您已了解了兩種新方法:

1.inc_func_calls()

2.inc_cat_pics()

這些方法能夠修改類中字典的值。您現在有一個類可以修改,就像您正在修改指針一樣:

Python的指針:有什麼意義?

這樣,您就可以在應用程序中的各個位置訪問func_calls和調用inc_func_calls(),並在Python中模擬指針。當您需要在應用程序的各個部分中頻繁使用和更新指針時,這非常有用。

注意:特別是在這個類中,使用inc_func_calls()和inc_cat_pics()更為清楚明白,而不是使用@property.setter,這能阻止用戶將這些值設置為任意的整型或無效的值,如字典。

這是Metrics類的完整代碼:

Python的指針:有什麼意義?


使用ctypes模塊實現真實指針

好吧,也許Python中有指針,特別是CPython。使用內置ctypes模塊,您可以在Python中創建真正的C風格指針。如果您不熟悉ctypes,那麼您可以查看使用C庫擴展Python和「ctypes」模塊。

你要使用它的真正原因是你需要對C庫創建一個需要指針的函數調用。讓我們回到之前的C函數add_one():

Python的指針:有什麼意義?

同樣,這段代碼將x的值增加1。要使用它,首先將其編譯為共享對象。假設上述代碼存儲在add.c文件中,您可以通過gcc來完成以下操作:

Python的指針:有什麼意義?

第一個命令將C源文件編譯為一個名為add.o的對象。第二個命令獲取該未鏈接的目標文件並生成一個名為libadd1.so的共享對象。

libadd1.so應該在您當前的目錄中。您可以使用ctypes的命令將其載入到Python:

Python的指針:有什麼意義?

代碼ctypes.CDLL返回一個代表libadd1的共享對象。因為您add_one()在此共享對象中定義,所以您可以像訪問其他任何Python對象一樣訪問它。在調用該函數之前,您應該指定函數簽名。這有助於Python確保將正確的類型傳遞給函數。

在這種情況下,函數簽名是指向整數的指針。ctypes允許您使用以下代碼來指定:

Python的指針:有什麼意義?

在此代碼中,您設置函數簽名以匹配C所期望的內容。現在,如果您嘗試使用錯誤的類型調用此代碼,那麼您將得到一個很好的警告而不是未定義的行為:

Python的指針:有什麼意義?

Python拋出一個錯誤,解釋說add_one()想要一個指針而不是一個整數。幸運的是,ctypes有一種方法可以將指針傳遞給這些函數。首先,聲明一個C風格的整數:

Python的指針:有什麼意義?

上面的代碼創建了一個C風格的整數x,其值為0。ctypes提供方便的byref()方法,它允許通過引用來傳遞變數。

注意:傳遞變數時,術語"通過引用"與"通過值"相反。

通過引用傳遞時,您將引用傳遞給原始變數,因此修改將反映在原始變數中。按值傳遞會生成原始變數的副本,並且修改不會反映在原始變數中。

你可以用下面的代碼來調用add_one():

Python的指針:有什麼意義?

太好了!你的整數加1。恭喜,您已成功使用Python的真實指針。


總結

您現在對Python對象和指針之間的關係有了更好的理解。儘管名稱和變數之間的某些區別似乎很迂腐,但從根本上理解這些關鍵術語可以擴展您對Python如何處理變數的理解。

您還學習了一些在Python中模擬指針的好方法:

· 利用可變對象作為低開銷指針

· 創建自定義Python對象以便於使用

· 使用ctypes模塊的解鎖真實指針

這些方法允許您在Python中模擬指針,而且不會犧牲Python提供的內存安全性。

感謝您的閱讀。如果您仍有疑問,請隨時在評論部分或Twitter上與我聯繫。


英文原文:https://realpython.com/pointers-in-python

譯者:ZH

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python部落 的精彩文章:

如何將Python內存佔用縮小20倍?
雖然Python「內置電池」,但是電池正在漏電--Amber Brown

TAG:Python部落 |