10. Python 類的句法設(shè)計

本系列文章譯自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ù)定義的“特殊方法”捧请,比如 initdel凡涩。用戶可以通過實現(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末误墓,一起剝皮案震驚了整個濱河市蛮粮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谜慌,老刑警劉巖然想,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欣范,居然都是意外死亡变泄,警方通過查閱死者的電腦和手機令哟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妨蛹,“玉大人屏富,你說我怎么就攤上這事⊥苈保” “怎么了狠半?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颤难。 經(jīng)常有香客問我神年,道長,這世上最難降的妖魔是什么行嗤? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任已日,我火速辦了婚禮,結(jié)果婚禮上昂验,老公的妹妹穿的比我還像新娘捂敌。我一直安慰自己,他們只是感情好既琴,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布占婉。 她就那樣靜靜地躺著,像睡著了一般甫恩。 火紅的嫁衣襯著肌膚如雪逆济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天磺箕,我揣著相機與錄音奖慌,去河邊找鬼。 笑死松靡,一個胖子當(dāng)著我的面吹牛简僧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雕欺,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岛马,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屠列?” 一聲冷哼從身側(cè)響起啦逆,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎笛洛,沒想到半個月后夏志,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡苛让,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年沟蔑,在試婚紗的時候發(fā)現(xiàn)自己被綠了湿诊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡溉贿,死狀恐怖枫吧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宇色,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布颁湖,位于F島的核電站宣蠕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏甥捺。R本人自食惡果不足惜抢蚀,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镰禾。 院中可真熱鬧皿曲,春花似錦、人聲如沸吴侦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽备韧。三九已至劫樟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間织堂,已是汗流浹背叠艳。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留易阳,地道東北人附较。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像潦俺,于是被迫代替她去往敵國和親拒课。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容