天天看點

《Python遊戲程式設計入門》——1.3 Python中的對象

本節書摘來自異步社群《python遊戲程式設計入門》一書中的第1章,第1.3節,作者[美]jonathan s. harbour ,李強 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

python是面向對象程式設計語言,這意味着,它至少支援一些面向對象程式設計概念。現在,我們将花一些時間來介紹這些概念,因為這是一種編寫代碼的高效方式。面向對象程式設計(oop)是一種方法學,也就是做事情的方式。在計算機科學中,有幾種較大的、“傘狀的”方法學,也就是說,定義了程式設計語言的功能的方法學。要讓我們的技能成為可以傳播的,方法學對于這個産業來說很重要。如果每個公司使用他們自己的方法學,那麼,為該公司工作的過程中所擷取的技能,對于另一個不同的組織來說将會是無用的。軟體工程也是一個充滿挑戰的領域,并且教育訓練的成本很高,是以,對于這個領域相關的每個人(經驗豐富的開發者、老闆以及教授概念的講師)來說,方法學都是有益的。

天生的好奇心,這是有天分的程式員的共同特點,如果你也有的話,那麼,你肯定會問,在面向對象泛型之前,人們使用的是哪一種程式設計類型呢。讓我們來了解一下這個主題,在我們還沒有真正開始使用python之前,先說明一下為什麼這個問題如此重要。在程式設計方法學方面,我們先搞清楚起源在哪裡,才能夠了解今天位于何處。

結構化程式設計

在oop之前,人們所采用的方法學叫作過程化程式設計(procedural programming)或結構化程式設計(structured programming),這意味着,在這種情況下使用的是過程和結構。過程通常叫作函數,并且,我們如今仍然在使用函數。是的,甚至在oop程式中,仍然有獨立的函數,如main()。包含在一個對象中的函數,叫作方法,并且當作為對象的一部分讨論的時候,使用方法這個術語而不是函數。但是,在對象之外,函數仍然存在,并且這是從之前的“時代”(方法學)沿用而來的。

結構是複雜的使用者定義類型(user-defined types,udt),它可以将很多的變量包含在一起。最流行的結構化語言是c。然而,結構化程式設計是一種曆史悠久而且頗為成功的方法學,一直延續至今。結構化運動的時間是從20世紀80年代到20世紀90年代,當然,這個時間和其他的方法學的發展有一些重疊。在電子産業中,很多軟體開發工具包(sdk)仍然按照結構化的方式來開發,提供了函數庫來控制一個電子裝置(例如,顯示卡或嵌入式系統)。可以說,c語言的開發(大概是在20世紀70年代)是以結構化程式設計為主要方式而進行的。c語言用來建立unix作業系統。

如下是python的結構化程式的一個快速示例。

這段程式産生如下的輸出。

函數的定義以def開頭,後面跟着函數名、參數和一個冒号。python中沒有代碼塊符号,如c++中的開始花括号({)和結束花括号(})。在python中,函數的結尾是未定義的,假設函數在下一個未縮進的行之前結束。讓我們做一點試驗,來測試python的行為。如下還是我們的示例,不帶任何的注釋行。你認為它會輸出什麼?

輸出是:

大多數的python初學者會對此感到驚奇。這裡所發生的事情是,print ("end")行向左縮進,是以,它變成了程式的第一行,後面跟着第二行,即printname ("jane doe")。函數定義不被看作是主程式的部分,并且,隻有當調用該函數的時候才會運作。如果我們像下面這樣,把函數定義放在主程式的下方,會發生什麼情況?

這段代碼實際上會産生文法錯誤,因為無法找到printname函數。這就告訴我們,在調用函數之前python必須先解析它。換句話說,函數定義必須位于函數調用的“上方”。

順序式程式設計

結構化程式設計是從早期的順序式程式設計方法學發展而來的。這不是正式的教科書的說法,但卻是更富有描述性的一種說法。順序式程式要求在每行代碼之前都要有行号。盡管跳轉到程式的其他行也是可能的(使用goto或gosub指令),并且這是結構化程式設計的一個早期的發展方向,但是,順序式程式傾向于陷入某種程度的複雜性,使得代碼變得難以識别或無法修改。這個時候所導緻的問題,稱為“意大利面條式代碼”,這是由于程式似乎要去向每個方向的“流”而導緻的。兩種最常用的順序式語言是basic和fortran,并且這些語言的全盛期是20世紀70年代到20世紀80年代。随着開發者對于維護“意大利面條式代碼”感到厭煩,人們迫切地需要進行範型遷移。随着諸如pascal和c這樣的新的結構化語言的引入,結構化程式設計應運而生。

助記式程式設計

在順序式程式設計之前,開發者編寫的代碼更接近于計算機硬體的層級,而他們使用的是彙編語言。有一個“彙編器”程式,就像是編譯器一樣,但是,它将會把助記式的指令直接轉換為對象或二進制檔案中的機器代碼,準備好讓處理器一次一個位元組地運作它們。一條彙編式的助記式指令,直接關聯到處理器所能夠了解的一條機器指令。這就像是在說機器自身的語言,并且很有挑戰性。在ms-dos的時代,這些彙編性的指令能夠把顯示模式轉換成分辨率為320×200并且具有256(8位)色的圖形模式,這對于20世紀90年代的ibm pc遊戲來說已經很好了,因為這會很快。記住,在那個時代,我們沒有今天這樣的顯示卡,隻有建構到rom bios中的“視訊輸出”以及作業系統所支援的各種模式。這就是那個時代的所有遊戲開發者都喜歡的聲名狼藉的“vga mode 13h”。

“ax”是一個16位的處理器寄存器,處理器上的實際的實體電路可以當作一種通用目的的“變量”對待,這裡使用了你所熟悉的術語而沒有使用電子工程的語言。還有其他3種通用目的寄存器:bx、cx和dx。它們自身都是從8位的intel處理器更新而來的,而後者擁有叫作a、b、c和d的寄存器。當發展到16位的時候,這些寄存器擴充為al/ah、bl/bh、cl/ch和dl/dh,它們分别表示每個16位寄存器的兩個8位的部分。乍一聽起來,這并不複雜。将一個值放到一個或多個這些變量寄存器之中,然後通過調用一個中斷來“加載”一個過程。在vga模式更改的例子中,中斷是10h。

我們已經簡單地回顧了從過去到現在的程式設計方法學,以了解和掌握當今所擁有的工具和語言的方法,下面,我們來介紹一下目前的情況以及有些什麼發展。如今,面向對象程式設計仍然是專業程式員所采用的最主要的方法學。它是microsoft的visual studio和.net framework等流行的工具的基礎。如今的商業和科學領域中,最主要的編譯型oop語言是c++、c#、basic(其現代變體是visual basic)以及java。當然還有其他的語言,但是,這些是最主要的。

python和lua都是腳本程式設計語言。和c++這樣的編譯型語言相比,python和lua的處理方式有很大不同,它們是解釋型的,而不是編譯型的。當你運作一個python程式的時候(擴充名為.py的一個檔案),它不會進行編譯,而會運作。你可能會在一個python函數中帶入文法錯誤,但是,在調用該函數之前,python不會提示錯誤。

python或者這段程式中沒有一個名為printgobblegobble()的函數,是以,這裡應該産生一個錯誤。輸出如下。

但是,如果添加了對errorprone()函數的調用,輸出将會如下。

現在,對于python中這一貌似忽略的部分有一些限制。如果你明顯錯誤地定義了一個變量,那麼,在運作之前,它才會初次産生錯誤。在python中,還會因為做了另一件奇怪的事情而把事情搞砸,那就是,使用保留字作為變量:

第一行沒問題,但是第二行導緻了如下的錯誤。

這條錯誤的意思是,print變成了一個變量,确切地說,是一個整數,其值設定為10。然後,我們試圖調用舊的print()函數,并且python無法得到它。因為舊的print()函數已經被忽略了。現在,這種奇怪的行為不再适用于python語言中的保留字了,如while、for、if等保留字,而隻是适用于函數。當你發現python作為一種腳本語言有着巨大的靈活性的時候,我覺得你會感到驚訝的。

像gcc或visual c++這樣的傳統的編譯器,甚至在考慮運作這樣的代碼的時候,你就會抓狂。畢竟,它們是編譯器。在将程式轉換成目标代碼之前,它們完整地解析了程式的流程。這麼做的缺點就是:編譯器無法處理未知的東西,它們隻能處理已知的東西,而腳本語言可以很好地處理未知的情況。

順序式程式設計演變為結構化程式設計,結構化程式設計演變為oop,程式設計範型從oop開始的下一次演進也将繼續保持同樣的方式,在範型發生變化之前,目前的程式設計方法學中将會出現一些明顯的改變的迹象。今天,發生在oop上的這些變化,可能會稱為自适應程式設計(adaptive programming)。在當今快節奏的世界中,沒有人會像我們以前程式設計的時候那樣,坐在計算機前閱讀wordperfect或lotus 1-2-3的200頁的手冊。還是有人會認為“閱讀手冊”是解決技術問題的有效方法,但是如今,即便是帶有類似手冊的産品也很少見了。如今,系統必須具有互動性和自适應性。超越oop的下一次演進,可能是面向實體程式設計(eop,entity oriented programming)。

想象一下,我們使用實體(使用簡單規則來解決複雜問題的自包含對象)來編寫代碼,而不是使用包含了屬性(變量)和方法(函數)的對象來編寫代碼。這似乎是a.i.的研究方向,而且應該能夠與如今已有的oop很好地适應。實際上,已經有了一些早期的迹象出現了。聽說過web service嗎?web service是寄存在網上的自包含對象,程式可以使用它來執行獨特的服務,而程式自身不知道如何進行這些服務。

這些web service可能會隻是要求一個庫存資料庫的參數,并且傳回與查詢比對的項目的清單。這種形式的程式互動,一定能夠超越編寫sql(structured query language,結構化查詢語句,這是關系資料庫的語言)!那麼,将其帶入到下一個層級如何?使用某種庫或搜尋引擎線上查詢一個服務,而不是接入一個已知的服務,這會怎麼樣?

作為另一個可能的示例,假設有一個線上的、可以用于遊戲中的遊戲實體的庫(很可能是由獨立開發者或開源團隊建立的),其中的實體将會帶有其自己的美工素材(2d精靈、3d網狀物、材質、音頻剪輯等)以及自身的行為(例如一段python腳本)。需要某種格式的素材的一個已有的遊戲引擎,可能會使用這種eop的概念來擴充遊戲設定。假設你要玩一個遊戲,諸如minecraft(www.minecraft.net)這樣的某種世界構造遊戲,并且,假設你是遊戲中的某個新角色。是以,你向遊戲提出查詢:“我需要一把短的木頭椅子”。在查詢發出去後的片刻,一把短的木頭椅子出現在你的遊戲中。假設有一個用于minecraft這樣的引擎的線上遊戲裝備庫,我們當然可以想象會發生這種情況。

我們已經進行了足夠的曆史分析和思考,進而可以觸發一些有想象力的思路。現在,讓我們來介紹一些具體而實際的内容,即目前的oop方法學及其在python中的實作。或者換句話說,我們用python來建立對象。python确實支援oop特性,但是,它不像是高度特定性的語言c++那樣,在各個程度上支援oop。在開始之前,讓我們先來了解一些術語。類是一個對象的藍圖。類不能做任何事情,因為它是一個藍圖。隻有在運作時建立對象的時候,對象才會存在。是以,當我們編寫類代碼的時候,它隻是一個類的定義,而不是一個對象。隻有在運作時,通過類的藍圖來建立對象的時候,它才是真正的對象。類的函數也叫作方法。類的變量通常作為屬性來通路(有一種方法用來擷取或設定一個變量的值)。當建立一個對象的時候,類執行個體化為該對象。

讓我們來了解python的oop特性的一些具體内容。示例如下。

每個定義的行末,都必須有一個冒号。關鍵字self描述目前的類,這和它在c++中的作用是相同的。所有的類變量前面必須有一個“self”,以便可以認出這是類的成員;否則,它們将會被當作局部變量。def __init__(self)這一行開始了類的構造函數,這是在類執行個體化的時候運作的第一個方法。在構造函數之外,可以聲明類變量并且在聲明的時候進行初始化。

多态

術語多态表示有“多種形式”或“多種形狀”,是以,多态是指具備多種形态的能力。在類的環境中,這意味着我們可以使用具有多種形态的方法,也就是說,參數的多種不同的集合。在python中,我們可以使用可選的參數來讓方法具備多種功能。新的bug類的構造函數,可以使用可選的參數來進行變換,如下所示:

同樣,walk()方法可以更新以支援一個可選的參數:

資料隐藏(封裝)

python不允許變量和方法聲明為私有的或受保護的,因為python中的所有内容都是公有的。但是,如果你想要讓代碼像是資料隐藏一樣地工作,這也是可以辦到的。例如,如下這段代碼可以用來通路或修改distance變量(我們假設它是私有的,即便它不是)。

從資料隐藏的角度來看,你可以将distance重命名為p_distance(使其看上去像是私有變量),然後,使用這兩個方法來通路它。也就是說,如果資料隐藏對于你的程式來說很重要的話,可以這麼做。

繼承

python支援基類的繼承。當定義一個類的時候,基類包含在圓括号中:

此外,python支援多繼承,也就是說,一個子類可以繼承自多個父類或基類。例如:

隻要每個父類中的變量和方法與其他的變量和方法不沖突,新的子類可以通路它們而毫無問題。但是,如果有任何的沖突,來自父類的沖突變量和方法在繼承順序中具有優先性。

當一個python類繼承自一個基類,父類所有的變量和方法都是可用的。變量可以使用,方法可以覆寫。當調用一個基類的構造函數或任何方法的時候,我們可以使用super()來引用基類:

但是,當涉及多繼承的時候,當共享相同的變量名或方法名的時候,必須使用父類的名稱,以避免混淆。

我們先來看看單繼承的示例。如下是一個point類,以及繼承自它的一個circle類。

我們可以直接測試這些類:

這會得到如下輸出。

我們看到point的功能很簡單,但是,circle先調用point的構造函數,然後才調用自己的構造函數,然後複雜地調用point的tostring()并添加自己的新的radius屬性。這真的有助于我們了解,為什麼所有的類都有一個tostring()方法。

現在,當建立circle類的時候,調用構造函數并傳遞給它3個參數(100,100,50)。注意,調用了父類(point)的構造函數來處理x和y參數,而radius參數在circle中處理:

super()調用了point類的構造函數,point類是circle類的父類或基類。當使用單繼承的時候,這種做法的效果令人驚奇。

盡管多繼承是一片沼澤,但至少還是要展示一下它是如何工作的。使用多繼承的時候,我們基本上不會使用super()來調用父類中的任何内容,除非每個父類中的變量和方法都是獨特的。這裡有另一對類,它們建構在前面已經給出的兩個類的基礎之上。還記得吧,我警告過你,python是一種看上去很奇怪的語言。我們現在來看看。别忘了,python是一種腳本語言,而不是編譯型語言。python代碼是在運作時解釋的。

size類是一個新的輔助類,而rectangle是我們這個示例中真正的焦點。這裡,rectangle将繼承自point和size:

point是早就定義了的,而size剛剛定義。現在,我們應該可以開始使用point.x、point.y、size.width和size.height,以及每個類中的tostring()方法了。python應該不會抱怨。但是,思路是通過調用父類的構造函數來自動初始化父類。否則,我們會喪失oop的所有優點,并且隻是在編寫結構化的代碼。是以,rectangle構造函數必須按照名稱來調用每個父類的構造函數:

注意,x和y傳遞給了point.__init__(),而width和height傳遞給了size.__init__()。這些變量在它們各自的類中正确地初始化。當然,我們可以隻是在rectangle中定義x、y、width和height,但是,這隻是一個示範。通常,為了保持代碼簡單,我們不建議那麼做。在真正的程式設計中,絕不要以這種方式使用繼承。這裡隻是為了說明多繼承。測試一下新的size和rectangle類:

産生如下輸出。

現在,這真的有點意思了。size足夠簡單,很容易了解,但是看一下rectangle的輸出。我們調用了point的構造函數和size的構造函數,這完全是按照計劃進行的。此外,tostring()方法有效地組合了point.tostring()和size.tostring()各自的輸出。