本系列文章譯自Python之父 Guido van Rossum 的系列博客“The History of Python”捅位。這個博客系列對我們理解Python及其演變很有幫助杯活,經(jīng)Guido同意,在這里翻譯推薦給大家坝锰,希望大家喜歡状原,也請大家多多指教蠢笋!
1. 問題
設(shè)計好用戶自定義類的運行機制后(見上一篇),我需要確定類的句法鬓椭,特別是方法定義句法。主要是我想讓類的方法定義和一般函數(shù)定義保持一致关划,否則小染,就必須大動干戈,重構(gòu) Python 的基本語法和字節(jié)碼生成器了贮折。
不過裤翩,即使我能讓方法定義句法保持一致,依然要解決實例變量的問題调榄。
一開始踊赠,我想模仿 C++ 中的隱式引用呵扛。比如說,在 C++ 中筐带,你可以這樣定義一個類:
''class A {
'' public:
'' int x;
'' void spam(int y) {
'' printf("%d %d\n", x, y);
'' }
'' };
這個類的實例有一個變量 x今穿,在其方法中,可以隱式地調(diào)用這個變量伦籍。比如蓝晒,在 spam() 方法中,x 既不是方法的參數(shù)帖鸦,也不是局部變量拔创,但由于這個類聲明了變量 x,所以可以直接指向?qū)嵗兞俊?/p>
不過我很快意識到富蓄,Python 是做不到這一點的剩燥。因為在一門不需要聲明變量的語言中,找不到優(yōu)雅的辦法立倍,來區(qū)分實例變量與局部變量灭红。
2. 隱式調(diào)用實例變量的困難
理論上說,要獲取實例變量的值是很容易的口注。Python 已經(jīng)有一個變量搜索順序:局部變量变擒、全局變量、內(nèi)置變量——它們各有一個字典寝志,只需要按順序查找過去就可以了娇斑。例如,我們運行一個函數(shù)材部,涉及局部變量 p 和全局變量 q毫缆,那么語句“print p, q”,就會先查找第一個字典乐导,即局部變量苦丁,并在其中找到 p,由于沒找到 q物臂,便開始查找第二個字典旺拉,即全局變量。
把實例字典加到查找序列前面是很容易的棵磷。那么蛾狗,如果我們運行一個涉及實例變量 x 和局部變量 y 的函數(shù),語句“print x, y”就會先在實例變量中找到 x仪媒,然后在局部變量中找到 y沉桌。
不過,在給實例變量賦值時,這個思路就不行了蒲牧。在 Python 中撇贺,給變量賦值并不會按順序查找變量名,而是在查找順序的第一個字典中直接添加變量或替換變量的值冰抢,一般來說松嘶,即局部變量。也就是說挎扰,變量是默認創(chuàng)建在局部作用域的(當(dāng)然翠订,可以通過全局聲明來改變默認行為)。
如果不調(diào)整這種簡單的賦值策略遵倦,只是將實例字典置于變量搜索順序之前尽超,就會導(dǎo)致無法給局部變量賦值。比如說梧躺,如下這個方法:
''def spam(y):
'' x = 1
'' y = 2
給 x 似谁、 y 賦值的語句只會給實例變量 x 重新賦值,并增加一個實例變量 y掠哥,賦值為 2巩踏。
交換實例變量和局部變量的搜索順序也是一樣的,只是讓我們無法賦值的對象改為實例變量而已续搀。
另外塞琼,改變賦值語法:如果實例存在該變量,就賦值給實例禁舷,如果不存在彪杉,就賦值給局部變量——也是沒用的,因為這會產(chǎn)生另一個問題:怎么增加實例變量呢牵咙?
一個可能的方案是派近,和全局變量一樣,通過顯式聲明創(chuàng)建實例變量霜大。不過构哺,考慮到 Python 一直沒有變量聲明,我實在不想因為這個問題增加這個特性战坤。而且,全局變量聲明一般比較少用残拐,而實例變量卻幾乎無處不在途茫。
另一個可能的方案是,在詞法上對實例變量進行區(qū)別溪食,比如說囊卜,讓實例變量前面都增加一個 @ (即Ruby采用的方法)或其它前綴等。
這兩種方案我都毫無興趣(至今依然如此)。
3. 采用顯式調(diào)用
我打算放棄隱式引用的思路栅组。在 C++ 之類的語言中雀瓢,我們可以通過 this -> foo 來顯式引用實例變量 foo(以免局部變量中有一個重名的 foo 的情況)。因此玉掸,我決定刃麸,實例變量必須顯式引用。另外司浪,我認為與其讓當(dāng)前對象(this)成為一個特殊關(guān)鍵詞泊业,不如直接讓“this”(或者其它等義詞)作為實例方法的第一個參數(shù),這樣啊易,實例變量就可以作為這個參數(shù)的屬性被引用吁伺。
采用顯式引用后,就不用為類的方法定義設(shè)計特殊句法租谈,也不用擔(dān)心變量查找的復(fù)雜化篮奄。我們只需要定義一個方法,并把第一個參數(shù)設(shè)為實例自身割去,即“self”就可以了窟却。比如說:
''def spam(self, y):
'' print self.x, y
我在 Modula-3 中見過類似思路——Python 的 import 和異常處理語法也借鑒自 Modula-3。Modula-3 沒有類的概念劫拗,但可以創(chuàng)建包含指向已定義函數(shù)的指針的記錄類型(record types)间校,并提供語法糖,使我們可以在調(diào)用函數(shù)的時候引用記錄的變量页慷。比如 x 是記錄的變量憔足,m 是這個記錄包含的函數(shù)指針,指向函數(shù) f酒繁,那么滓彰,x.m(args) 就等價于 f(x, args)。
這樣州袒,我就完成了類與方法的實現(xiàn)揭绑,并可以通過方法的第一個參數(shù)的屬性來調(diào)用實例變量。
剩下的就是一些細節(jié)設(shè)計了郎哭。
遵循一貫的簡潔原則他匪,我把類語句當(dāng)成一系列的方法定義,句法上與函數(shù)一致夸研,但一般會有“self”作為第一個參數(shù)邦蜜。同時,為避免給特殊方法設(shè)計新句法(比如初始化方法或析構(gòu)函數(shù)(destructors))亥至,我決定讓用戶實現(xiàn)一些特殊命名的方法悼沈,比如 init贱迟、del 等。這種命名慣例來自 C語言絮供,在 C語言中衣吠,帶兩個下劃線前綴的變量名由編譯器保留,通常有一些特殊意義(比如 FILE)壤靶。
這樣缚俏,Python 中的類看起來就是如下代碼:
''class A:
'' def __init__(self, x):
'' self.x = x
'' def spam(self, y):
'' print self.x, y
4. 類作為一個命名空間
在這里,我依然希望盡可能重用我之前的代碼萍肆。
一般來說袍榆,定義一個函數(shù)就是創(chuàng)建一個可執(zhí)行語句,并在當(dāng)前命名空間創(chuàng)建一個指向函數(shù)對象的變量(變量名稱即函數(shù)名稱)塘揣。因此我想包雀,與其設(shè)計一個全新的方法來處理類,不如直接把類看做一系列在新命名空間執(zhí)行的語句亲铡。這個新命名空間的字典才写,就被用于初始化類字典并創(chuàng)建一個類對象。
從底層看奖蔓,即把類變成一個匿名函數(shù)赞草,所有語句都在其中執(zhí)行,并將其局部變量字典作為結(jié)果返回吆鹤。之后厨疙,這個字典被傳遞給一個創(chuàng)建類對象的輔助函數(shù),輔助函數(shù)會把類對象存儲于一個作用域中疑务,類的名稱就是這個作用域的名稱沾凄。
因為類可以支持任意序列的有效語句,大家往往會覺得很神奇知允。其實 Python 的這個特性只是簡化句法撒蟀,不進行人為限制的直接結(jié)果而已。
最后一個細節(jié)就是實例化的句法温鸽。很多語言保屯,比如 C++ 和 Java,都通過特殊操作符“new”來創(chuàng)建實例涤垫。在 C++ 中姑尺,因為類名在解析器中有一個特殊狀態(tài),這是可行的蝠猬。但 Python 解析器不關(guān)心用戶調(diào)用的什么類型的對象股缸,因此,最好吱雏、最直接敦姻、不產(chǎn)生任何新句法的方案,就是讓類對象本身可調(diào)用歧杏。
關(guān)于這一點镰惦,當(dāng)時的我可能超越時代了——通過工廠函數(shù)創(chuàng)建實例現(xiàn)在已經(jīng)很流行,而我當(dāng)時即把類當(dāng)做它自己的工廠犬绒。
5. 關(guān)于特殊方法
最后旺入,簡單提一下,我的一個主要目標(biāo)凯力,就是盡可能以簡單的方式來實現(xiàn)類。在大多數(shù)面向?qū)ο笳Z言中,都有一些只針對類的特殊操作符或特殊方法库物。比如在 C++ 中侨核,有定義構(gòu)造函數(shù)和析構(gòu)函數(shù)的特殊句法,與定義常規(guī)函數(shù)或方法的句法不同祈惶。
而我真的不想為對象的特殊操作引入新句法雕旨,因此,我采用了一系列預(yù)定義的“特殊方法”捧请,比如 init 和 del凡涩。用戶可以通過實現(xiàn)這些方法來定義構(gòu)造和析構(gòu)過程。
同時疹蛉,我也用這種方法來支持用戶對 Python 操作符的行為進行自定義活箕。如之前所說,Python 是用 C 語言實現(xiàn)的可款,并通過函數(shù)指針來實現(xiàn)內(nèi)置對象行為(如“get attribute”育韩、“add”、“call”等)筑舅。為使用戶自定義類也可以有這些行為座慰,我為這些函數(shù)指針也指定了一些特殊方法名稱,比如 getattr翠拣、add版仔、call 等。
公眾號:ReadingPython