Python 的"self"參數是什麼?
讓我們從我們已經知道的開始:self - 方法中的第一個參數 - 指的是類實例:
class MyClass: ┌─────────────────┐ ▼ │ def do_stuff(self, some_arg): │ print(some_arg) ▲ │ │ │ │ │ │ │ │ │ instance = MyClass() │ │ instance.do_stuff("whatever") │ │ │ └───────────────────────────────┘
此外,這個論點實際上不必稱為 self - 它只是一個約定。例如,你可以像其他語言中常見的那樣使用它。
上面的代碼可能是自然而明顯的,因為你一直在使用,但是我們只給了 .do_stuff() 一個參數 (some_arg),但該方法聲明瞭兩個 (self 和 , some_arg),好像也説不通。片段中的箭頭顯示 self 被翻譯成實例,但它是如何真正傳遞的呢?
instance = MyClass() MyClass.do_stuff(instance, "whatever")
Python 在內部所做的是將 instance.do_stuff("whatever") 轉換為 MyClass.do_stuff(instance, "whatever")。我們可以在這裏稱之為“Python 魔法”,但如果我們想真正瞭解幕後發生的事情,我們需要了解 Python 方法是什麼以及它們與函數的關係。
類屬性/方法
在 Python 中,沒有“方法”對象之類的東西——實際上方法只是常規函數。函數和方法之間的區別在於,方法是在類的命名空間中定義的,使它們成為該類的屬性。
這些屬性存儲在類字典 __dict__ 中,我們可以直接訪問或使用 vars 內置函數訪問:
MyClass.__dict__["do_stuff"] # <function MyClass.do_stuff at 0x7f132b73d550> vars(MyClass)["do_stuff"] # <function MyClass.do_stuff at 0x7f132b73d550>
訪問它們的最常見方法是“類方法”方式:
print(MyClass.do_stuff) # <function MyClass.do_stuff at 0x7f132b73d550>
在這裏,我們使用類屬性訪問該函數,正如預期的那樣打印 do_stuff 是 MyClass 的函數。然而,我們也可以使用實例屬性訪問它:
print(instance.do_stuff) # <bound method MyClass.do_stuff of <__main__.MyClass object at 0x7ff80c78de50>
但在這種情況下,我們得到的是一個“綁定方法”而不是原始函數。Python 在這裏為我們所做的是,它將類屬性綁定到實例,創建了所謂的“綁定方法”。這個“綁定方法”是底層函數的包裝,該函數已經將實例作為第一個參數(self)插入。
因此,方法是普通函數,它們的其他參數前附加了類實例(self)。
要了解這是如何發生的,我們需要看一下描述符協議。
描述符協議
描述符是方法背後的機制,它們是定義 __get__()、__set__() 或 __delete__() 方法的對象(類)。為了理解 self 是如何工作的,我們只考慮 __get__(),它有一個簽名:
descr.__get__(self, instance, type=None) -> value
但是 __get__() 方法實際上做了什麼?它允許我們自定義類中的屬性查找 - 或者換句話説 - 自定義使用點符號訪問類屬性時發生的情況。考慮到方法實際上只是類的屬性,這非常有用。這意味着我們可以使用 __get__ 方法來創建一個類的“綁定方法”。
為了讓它更容易理解,讓我們通過使用描述符實現一個“方法”來演示這一點。首先,我們創建一個函數對象的純 Python 實現:
import types class Function: def __get__(self, instance, objtype=None): if instance is None: return self return types.MethodType(self, instance) def __call__(self): return
上面的 Function 類實現了 __get__ ,這使它成為一個描述符。這個特殊方法在實例參數中接收類實例 - 如果這個參數是 None,我們知道 __get__ 方法是直接從一個類(例如 MyClass.do_stuff)調用的,所以我們只返回 self。但是,如果它是從類實例中調用的,例如 instance.do_stuff,那麼我們返回 types.MethodType,這是一種手動創建“綁定方法”的方式。
此外,我們還提供了 __call__ 特殊方法。__init__ 是在調用類來初始化實例時調用的(例如 instance = MyClass()),而 __call__ 是在調用實例時調用的(例如 instance())。我們需要用這個,是因為 types.MethodType(self, instance) 中的 self 必須是可調用的。
現在我們有了自己的函數實現,我們可以使用它將方法綁定到類:
class MyClass: do_stuff = Function() print(MyClass.__dict__["do_stuff"]) # __get__ not invoked # <__main__.Function object at 0x7f229b046e50> print(MyClass.do_stuff) # __get__ invoked, but "instance" is None, "self" is returned print(MyClass.do_stuff.__get__(None, MyClass)) # <__main__.Function object at 0x7f229b046e50> instance = MyClass() print(instance.do_stuff) # __get__ invoked and "instance" is not None, "MethodType" is returned print(instance.do_stuff.__get__(instance, MyClass)) # <bound method ? of <__main__.MyClass object at 0x7fd526a33d30>
通過給 MyClass 一個 Function 類型的屬性 do_stuff,我們大致模擬了 Python 在類的命名空間中定義方法時所做的事情。
綜上所述,在instance.do_stuff等屬性訪問時,do_stuff在instance的屬性字典(__dict__)中查找。如果 do_stuff 定義了 __get__ 方法,則調用 do_stuff.__get__ ,最終調用:
# For class invocation: print(MyClass.__dict__['do_stuff'].__get__(None, MyClass)) # <__main__.Function object at 0x7f229b046e50> # For instance invocation: print(MyClass.__dict__['do_stuff'].__get__(instance, MyClass)) # Alternatively: print(type(instance).__dict__['do_stuff'].__get__(instance, type(instance))) # <bound method ? of <__main__.MyClass object at 0x7fd526a33d30>
正如我們現在所知 - 將返回一個綁定方法 - 一個圍繞原始函數的可調用包裝器,它的參數前面有 self !
如果想進一步探索這一點,可以類似地實現靜態和類方法(https://docs.python.org/3.7/howto/descriptor.html#static-methods-and-class-methods)
為什麼self在方法定義中?
我們現在知道它是如何工作的,但還有一個更哲學的問題——“為什麼它必須出現在方法定義中?”
顯式 self 方法參數是有爭議的設計選擇,但它是一種有利於簡單性的選擇。
Python 的自我體現了“越差越好”的設計理念——在此處進行了描述。這種設計理念的優先級是“簡單”,定義為:
設計必須簡單,包括實現和接口。實現比接口簡單更重要...
這正是 self 的情況——一個簡單的實現,以接口為代價,其中方法簽名與其調用不匹配。
當然還有更多的原因為什麼我們要明確的寫self,或者説為什麼它必須保留, Guido van Rossum 在博客文章中描述了其中一些(http://neopythonic.blogspot.com/2008/10/why-explicit-self-has-to-stay.html),文章回復了要求將其刪除的提議。
Python 抽象了很多複雜性,但在我看來,深入研究低級細節和複雜性對於更好地理解該語言的工作原理非常有價值,當事情發生故障和高級故障排除/調試時,它可以派上用場不夠。
此外,理解描述符實際上可能非常實用,因為它們有一些用例。雖然大多數時候你真的只需要@property 描述符,但在某些情況下自定義的描述符是有意義的,例如 SLQAlchemy 中的或者 e.g.自定義驗證器。
- Spring中實現異步調用的方式有哪些?
- 帶參數的全類型 Python 裝飾器
- 整理了幾個Python正則表達式,拿走就能用!
- SOLID:開閉原則Go代碼實戰
- React中如何引入CSS呢
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫編程語言-遞歸函數是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多線程永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- LeCun再炮轟Marcus: 他是心理學家,不是搞AI的
- Java保證線程安全的方式有哪些?
- 19個殺手級 JavaScript 單行代碼,讓你看起來像專業人士
- Python 的"self"參數是什麼?
- 別整一坨 CSS 代碼了,試試這幾個實用函數
- 再有人問你什麼是MVCC,就把這篇文章發給他!