Python 語法隨手記錄 – 3

書名、原作者、原譯者資訊:

Python 教學文件

作者:
Guido van Rossum
Fred L. Drake, Jr., editor
譯者:
周譯樂


/////程式錯誤與例外(Exceptions)情形

至此為止,我們都只有稍稍的提到錯誤訊息。但是如果你有試著執行上面的範例的話,你可能注意到,基本上錯誤的情況可以分成兩類:語法錯誤 ( syntax errors ) 以及例外情況 ( exceptions )。

/////語法錯誤

語法錯誤也叫做分析時的錯誤(parsing errors),大概是一般在學Python時最常見到的直譯器所發出來的抱怨:

>>> while 1 print ‘Hello world’
File ““, line 1
while 1 print ‘Hello world’
^
SyntaxError: invalid syntax

Python分析器(parser)會在印出錯誤的行,並且用一個向上的箭號指出最早發現錯誤的地方,而這個錯誤是發生(至少是被發現)在這個箭號所指的單元(token) 之前。在我們的例子裡面:錯誤發生在 print 這個關鍵字,因為前面應該有一個 ( ” :” ) 。錯誤信息裡面也包含檔案名稱以及行數,所以你可以很快知道要到哪裡去找錯。

/////例外(Exceptions)情形

有的時候,甚至當你的語法完全正確時,當你執行程式時仍然會出錯。這種在程式執行階段發生的錯誤叫做例外情形 ( exceptions ) ,並且會造成程式致命的終止(無法執行下去)。你待會就會知道在Python裡面要怎樣處理這樣的狀況,但是我們先來看這樣的狀況下會造成什麼錯誤信息:

>>> 10 * (1/0)
Traceback (innermost last):
File ““, line 1
ZeroDivisionError: integer division or modulo
>>> 4 + spam*3
Traceback (innermost last):
File ““, line 1
NameError: spam
>>> ’2′ + 2
Traceback (innermost last):
File ““, line 1
TypeError: illegal argument type for built-in operation

在這些錯誤信息的最後一行都是解釋到底發生了什麼事。例外情況(Exception)有很多種類型,類型的名稱也在錯誤信息之中,在上面的例子裡面,exception的類型分別是: ZeroDivisionError, NameError 以及 TypeError. 。對於內建的exception來說,這些印出來的字串都是這些內建的exception類型的真正類型名稱,但是對於使用者自己自定的exception類型就不一定了(雖然這是一個有用的約定俗成的習慣)。這些標準的exception名稱也正是他們內建的指稱(identifiers) (不是正式的關鍵字)。

這一行其他部分則是有關這個exception類型的詳細解釋,其意義則是依照exception的類型而有不同。

在錯誤信息最後一行之前的部分則是顯示了這個exception發生時的狀況,也就是記憶體堆積(stack)的內容追朔(backtrace)。一般來說這個這個部分包含了stack backtrace的來源行數,但是這並不代表是在從標準輸入讀入時候的行數。

在Python程式庫參考手冊中( Python Library Reference )詳細列出了所有的內建exception及其說明。

/////例外(Exceptions)情形的處理

我們可以寫一個程式來處理某些的exception。請看下面程式範例,我們要求使用者輸入一個有效的數字,直到所輸入的數字真正有效為止。但是使用者也可以中止這個程式(用 Control-C 或者是任何作業系統支援的方式)。值得注意的是,使用者主動中止程式事實上是使用者引發一個 KeyboardInterrupt 的exception。

>>> while 1:
… try:
… x = int(raw_input(“Please enter a number: “))
… break
… except ValueError:
… print “Oops! That was no valid number. Try again…”

這個 try 敘述的作用如下:

首先,try之後的敘述( try clause ,在 try 及 except 這兩個字之中所有的敘述)都會被執行。

如果沒有發生任何exception, except之後的敘述( except clause )會自動被忽略,整個 try 敘述就算是執行完畢。

如果當執行try之後的敘述時發生了exception,錯誤地方之後的敘述就不會被執行。然後如果這個exception的類型有某一個適合的 except 關鍵字之後的類型的話,就會執行這一個except之後的敘述,然後程式從整個 try 敘述之後的地方開始執行。

如果所發生的exception在except關鍵字之後找不到相對應的類型時,系統會將這個類型傳給外面一層的 try 敘述。如果外層的exception處理機制不存在的話,這就是一個沒有被處理的exception( unhandled exception ),然後整個程式會中斷,並出現上面出現的錯誤程式。

一個 try 敘述可以包含許多的except 部分來處理各種不同的exception,但是最多只有一個handler(譯:exception之後的敘述)會真正被執行。Handlers 只處理在所對應的 try 部分發生的exception,其他的 try 部分發生的exception則不在處理範圍。一個except子句可以處理一個以上的exception,只要用list括弧把它們括起來。例如:

… except (RuntimeError, TypeError, NameError):
… pass

最後的一個 except 可以不寫出exception 類型的名稱,這就當作是一個外卡(wildcard,譯:處理所有的exception)來使用。當使用時要特別的小心,因為如果你很有可能就把一個應該被注意的程式錯誤給隱藏起來了。你也可以在這個except子句裡面印出一個錯誤信息,然後把這個exception再丟(raise)出去(讓呼叫你程式的人來處理這個exception)。

import string, sys

try:
f = open(‘myfile.txt’)
s = f.readline()
i = int(string.strip(s))
except IOError, (errno, strerror):
print “I/O error(%s): %s” % (errno, strerror)
except ValueError:
print “Could not convert data to an integer.”
except:
print “Unexpected error:”, sys.exc_info()[0]
raise

這個 try … except 的敘述有一個可有可無的else子句( else clause )可以使用,當這個子句存在時,必須是放在所有的except clauses的後面。這個子句裡的敘述是當try子句沒有發生任何exception時,一定要執行的敘述。請看例子:

for arg in sys.argv[1:]:
try:
f = open(arg, ‘r’)
except IOError:
print ‘cannot open’, arg
else:
print arg, ‘has’, len(f.readlines()), ‘lines’
f.close()

使用 else 要比在 try 子句裡面加入多餘的程式碼來的好,因為這樣減少意外的處理到那些不是由 try … except 敘述中保護的程式碼所引發的exception。

當一個exception 發生時,exception本身有一個 連帶的值,也叫做這個exception的參數( argument )。至於這個參數是否存在以及其型態,則是由exception的類型所決定。對於有這個參數存在的exception類型來說,你可以在except clause的後面加入一個名稱(或是list)來接收這個參數的值。請看下例:

>>> try:
… spam()
… except NameError, x:
… print ‘name’, x, ‘undefined’

name spam undefined

如果一個exception 有一個參數的話,當它在沒有被處理,當作錯誤信息印出來的時候,就會成為最後(詳細解釋(`detail’))的一部份。

Exception的處理者(handlers,exception clause)並不只處理在try clause當中所發生的exception,也會處理所有在try clause當中所(直接或間接)呼叫的函式所引發的exception。請看下例:

>>> def this_fails():
… x = 1/0

>>> try:
… this_fails()
… except ZeroDivisionError, detail:
… print ‘Handling run-time error:’, detail

Handling run-time error: integer division or modulo

/////如何引發例外(Exceptions)

使用 raise 敘述可以引發一個特定的 exception,例如:

>>> raise NameError, ‘HiThere’
Traceback (innermost last):
File ““, line 1
NameError: HiThere

raise 的第一個參數是想要引發的exception的類型,第二個參數(可有可無)則是指定這個exception的參數值。

/////使用者自訂的例外(Exceptions)

程式設計師可以自己命名自己想要的excetion,其方法是指定一個字串給一個變數,或者是自己創造一個新的exception類別來。舉例說明:

>>> class MyError:
… def __init__(self, value):
… self.value = value
… def __str__(self):
… return `self.value`

>>> try:
… raise MyError(2*2)
… except MyError, e:
… print ‘My exception occurred, value:’, e.value

My exception occurred, value: 4
>>> raise MyError, 1
Traceback (innermost last):
File ““, line 1
__main__.MyError: 1

許多標準的module都自己自訂其exception來報回(report)在他們自己所定義的函式裡面所發生的錯誤。

有關於classes 更多的討論請看第九章 “類別”。

/////定義善後的動作

在 try 敘述的機制裡面有一個可有可無的子句(optional clause),其功用是在定義不管什麼情況發生下,你都得要做的清除善後的工作。 舉例來說:

>>> try:
… raise KeyboardInterrupt
… finally:
… print ‘Goodbye, world!’

Goodbye, world!
Traceback (innermost last):
File ““, line 2
KeyboardInterrupt

這個 finally clause 不管你的程式在try裡面是否有任何的exception發生都會被執行。當exception發生時,程式會執行finally clause之後再引發這個exception。當程式的 try try部分因為 break 或 return 而離開時,finally clause也一樣會在離開的時候(“on the way out”)被執行。

一個 try 敘述機制應該要有一個或多個except clauses,或者是有一個 finally clause,但是不能兩個都有。

/////Class(類別)

Python的類別機制在加入最少新的語法及語意的情況下加入了類別的支援。Python的類別機制是C++ 以及Modula-3的綜合體。正如同在modules裡面的情況一樣,Python的 class也沒有在其定義及使用者之間加入絕對的障礙,而是仰賴使用者有禮貌的不要去闖入其定義之中(not to “break into the definition”)。對於class來說最重要的一些特性在Python裡面都完全的保留:類別的繼承可以繼承自個基礎類別(base classes),一個子類別(derived class)可以override其所有基礎類別(base class)的任何方法(method),一個method也可以呼叫一個基礎類別的同名方法,物件可以自由決定是否要讓某些資料是 private的。

以C++ 的術語來說,Python所有的類別成員(包含其資料成員)都是 public 的,而且所有的函式成員(member functions)都是 virtual 的。也並沒有所謂的建構元(constructors)或是解構元(destructors)的存在。如同在 Modula-3裡面一樣,從物件的方法(method)裡面要使用物件的成員並沒有捷徑可以使用:成員函式的宣告必須在第一個參數中明白的在表示所存在其中的物件,而此參數在呼叫時是不用傳的。如同在Smalltalk裡面一樣,類別本身也是一個物件,事實上在Python裡面,所有的資料型態(data type)都是物件。這提供了在import以及重新命名時候的語意(sematics)。但是如同在C++ 或是Modula-3裡面,內建的基本型態是不能被使用者拿來當作基礎類別使用的。與C++類似但不同於Modula-3的是,大部分有特別語法的內建運算元(operators),例如數值運算及subscripting,都可以被拿來在類別中重新定義的。

/////術語的使用說明

由於缺乏普遍性的術語可以討論類別,我只好偶而從Smalltalk或是C++的術語中借來用。(我其實更想用Modula-3的術語,因為它的術語在語意上比C++還要接近Python,但是我想大部分的讀者都沒有聽過它)。

我也要警告你的是,物件這個字在Python裡面不必然指的是類別的一個特例(instance),這是一個在物件導向讀者中常見的陷阱。與C++及Modula-3相同但與Smalltalk不同的是,並非所有在Python裡面的資料型態都是類別,像是整數及list這類的基本的內建型態就不是類別,甚至一些特別的資料型態像是file都不是類別。無論如何, 所有的 Python的資料型態都或多或少都有一些基本相同的語意特性,我們可以把這個相同點叫做物件。

物件有其個體性(individuality,獨特性),而且你可以用不同的名字連結到同一個物件去,這在其他的程式語言中也叫做別名(aliasing)。通常你第一次看到Python不會覺得這有什麼特別,而且你在處理不可變動的(immutable)基本型態(例如數目字,字串及tuple)時,你根本可以不去管它。但是對於一些都可變動的(mutable)物件,像是list,dictioanry以及其他用來表現在程式之外的實體(像是檔案及視窗)的資料型別,對它們來說aliasing就和與它們有關之Python程式碼語意的解釋,有(故意的)一些影響。這樣的影響通常是對程式有正面的效益,因為別名(alias)運作的方式就像是一個有禮貌的指標(pointer)。舉例來說,當你傳一個物件當參數時,因為所傳的其實只是一個指標,所以所費的資源就不多。而且,當在函式之內對這個傳入的物件進行修改時,在外面呼叫這個函式的人(caller)會看得見函式所做的修改,這大大的簡化了在Pascal裡面需要兩種不同參數傳遞機制才能作到的事。

/////Python的可用範圍(Scopes)及命名空間(Naming Spaces)

在介紹類別(class)之前,我首先必須介紹Python有關可用範圍(scope)的一些準則。類別的定義也對命名空間(namespace)做了一些小技巧,所以你需要對scope及namespace的運作有一些了解才能夠完全的掌握到底發生了什麼事。對於進階的Python程式設計師來說,有關這個主題的了解是很有幫助的。

現在讓我們先來定義一些東西:

一個 namespace 指的是名稱與物件的對應關係的組合。目前來說,namespace都是以Python的dictionary來實作出來的,但是這應該沒有多大意義(除非對程式的效率),而且在未來可能也有所改變。Namespace的例子有:一組的內建名稱(像是 abs() 的函式,還有內建的exception名稱),在module裡的全域變數(global variables),以及在函式裡的local變數。某種意義來說,一個物件裡的特性(attributes,譯:成員)也組成一個namespace。在這裡要知道的重點是,不同的namespace裡面所定義的名稱是彼此沒有任何關係的。舉例來說,兩個不同的module都可以定義一個叫做“maximize”的函式。這一點都不衝突,因為使用者必須要在這個函式的名稱前加上module的名稱。

喔,我在這裡所用的 attribute 一字指的事所有在點號後面的東西,舉例來說在 z.real 這個expression裡面 real 就是一個屬於 z 這個物件的attribute。嚴格說來,使用module裡面的名稱也是一個attribute的指稱(references),在 modname.funcname 這個expression裡面, modname 就是一個module物件,而 funcname 就是其attribute。在這個例子裡面,剛好module的attributes就對應了在module裡面定義的全域變數,所以我們就說它們就是在一個namespace裡面。 9.1

Attributes可以是唯讀的或是可改寫的。對可改寫的attribute,你可以設定值給它。Module的 attributes是可以改寫的:所以你可以寫 “modname.the_answer = 42″ 來改變其值。可改寫的attributes也可以被刪除掉,你可以用 del 敘述像是 “del modname.the_answer ” 來做。

命名空間(Name spaces)是在不同的時候被創造出來的,而且其存在的時間也都不一定。內建名稱的namespace是在當Python直譯器啟動時就被創造出來,而且不會被刪除掉。Module裡全域(global)的namespace是當module的定義被讀入的時候就被創造出來,通常在直譯器離開之前也不會被刪除。那些在top-level啟動直譯器裡面被執行的指令,不管是在互動模式或是從script裡面而來的,都隸屬於一個叫做 __main__ 的module,所以它們也算有自己的一個global namespace。 (事實上,內建的名稱也都在一個module裡面,這個module叫做 __builtin__ )

函式所有的namespace叫做local namespace,是在函式被呼叫時才創造的,而且當函式傳回一個值或是引發一個本身無法處理的exception時,這個namespace就被刪除(事實上,也許說遺忘是比較貼切的形容詞)。當然,遞迴的函式呼叫會使每個呼叫都有自己的local namespace。

一個可用範圍( scope )是一個在Python程式裡面文字上的範圍,在這個範圍裡面你可以直接使用某個namespace。直接使用(“Directly accessible”)的意思是指對一個名稱而言不合格的參考(unqualified reference)試圖想要在namespace裡面找某一個名稱。

雖然scope是靜態的(statically)被決定的,但是我們使用namescope的時候是很動態(dynamically)的來使用之。在任何一個程式執行的地方,都正好有三層的scope正在被使用(也就是有三個可以直接使用的namespace):首先尋找的是最內圈的scope,包含有local的名稱;其次搜尋的是中間一層,包含有目前所在的module的全域名稱(global names);最後搜尋的是最外面的一層,也就是包含有內建名稱的namespace。

通常,local scope指的是在文字上面目前的函式所擁有的local名稱。在函式之外的話,local scope就指的是global scope所指的namespace。類別的定義在local scope裡面則又放入了另外的一個namespace。

要注意的是scope的決定是依文字的安排來決定的。一個定義在module裡面的函式,其global scope就是module的namespace,不管這個函式是從哪裡或是用哪一個別名被呼叫的。在另一方面來說,真正的名稱搜尋路線是動態的決定的(在程式執行的時候)。但是Python語言本身的定義好像慢慢的往靜態決定變化(也就是在編譯的時候),所以,不要過分依賴動態的名稱解釋。(事實上,local的變數都已經是靜態就已經決定了的)。

Python有一個很特別的變化就是當設定(assignment)的時候都一定是進入到了最內層的scope。設定並不是複製資料,相反的,它只是把物件及名稱連結起來而已。對於刪除也是一樣的, “del x” 事實上只是把 x 的連結從local scope所代表的namespace中除去。事實上,所有會引進新名稱的動作都是使用local scope:特別是, import敘述以及函式的定義就是把module以及函式的名稱都連結到local scope裡面來了。( global 這個敘述可以用來特別指定某個特殊的變數是要放在global scope裡的)

/////Class(類別)初探

類別(Classes)的觀念引進了許多新的語法,三種新的物件以及一些新的語言上的意義:

//////定義Class(類別)的語法

最簡單的類別定義的形式看起來像是這樣的:

class ClassName:

.
.
.

類別的定義與函式的定義(都是用 def 敘述)相同,都必須在要在它們有任何作用之前就定義好。(你也可以在 if 敘述或是一個函式裡面放入類別的定義)。

在實務上,存在於類別定義內的敘述通常都是函式的定義,但是我們也可以放入其他的敘述。這樣的做法有時也很好用,我們之後會再會來看這個用法。類別定義內的函式定義通常都有一個特別的參數形式,這是為了method的特別呼叫習俗的。我們還是留到後面再來討論之。

當一個類別的定義進來時,就會創造出一個新的namespace,而且會當作是一個local scope來用。所以所有對local變數的設定都會進入到這個新的namespace裡面。具體來說,函式的定義也會把新的函式的名稱連結到這裡來。

當一個類別的定義正常的離開時( 藉由定義的尾端),一個類別物件( class object )就被創造出來了。這個類別物件基本上來說是只是一個包裝起來的東西,其內容是由這個類別定義所創造出來的namespace裡面的內容。我們在下一節就會有更多有關類別物件(class objects)的討論。另外在類別的定義離開時,原來的local scope (在進入類別的定義之前的那一個local space)就會被重新使用,並且這個創造出來的類別物件就會被放在這個local scope裡面,並且被連結到你所定義的類別名稱(上面的例子裡是 ClassName )上面。

/////類別物件(Class Objects)

類別物件可以做兩件事情,一是attribute的指涉(references),另一個是創造出一個特例來(instantiation)。

Attribute references 所使用的是在Python裡面標準的attribute reference的語法: obj.name 。有效的attribute的名稱指的是當類別物件被創造時,所有在類別的namespace裡面的名稱。所以,如果你的類別定義如同下面例子的話:

class MyClass:
“A simple example class”
i = 12345
def f(x):
return ‘hello world’

你就可以使用 MyClass.i 以及 MyClass.f 這兩個有效的attribute references語法,它們分別會傳回一個整數以及一個method物件來。你也可以設定值給這些類別的attributes,如此你就可以改變 MyClass.i 的值了。 __doc__ 也是類別物件的一個有效的attribute,其傳回值是這個類別的注釋字串(docstring),也就是: “A simple example class” 。

類別的特例化( Class instantiation )是使用函式的表示方法。看起來好像這個類別物件是一個沒有參數的函式,然後傳回來的就是這個類別的的一個特例(instance)。我們再以前面的類別為例子:

x = MyClass()

就會創造出一個新的類別的 instance ,然後我們再把這個物件設定給 x 這個local的變數。

類別的特例化(Class instantiation )這個動作(也就是“呼叫”一個類別物件)所創造出來的是一個空的物件。有許多的類別希望創造出來的物件有一個特定的初始狀態,所以你可以在類別裡面定義一個特別的method叫做 __init__() ,如同下例:

def __init__(self):
self.data = []

當你的類別有定義一個 __init__() method時,當你在特例化(instantiation)你的類別時,就會自動的引發 __init__() 執行,並且創造出一個類別的特例(instance)。所以,一個新的物件就可以藉由底下的呼叫來創造出來:

x = MyClass()

當然, __init__() 這個method 可以有參數傳入,這樣可以增加使用時的彈性。在這樣做的時候,使用特例化(instantiate)類別的語法時,所傳入的參數就會被傳到 __init__() 裡面去。如範例:

>>> class Complex:
… def __init__(self, realpart, imagpart):
… self.r = realpart
… self.i = imagpart

>>> x = Complex(3.0,-4.5)
>>> x.r, x.i
(3.0, -4.5)

/////特例物件(instance objects)

現在對於這個被創造出來的特例物件(instance objects),我們又該怎麼用呢?對於這樣的特例物件,唯一它們懂得的就是attribute references。有兩種的attribute names我們可以使用:

第一種我叫他是資料特性( data attributes ),這類似於在Smalltalk中所說的特例變數(“instance variables”)以及在C++中的資料成員(“data members”)。如同local變數一樣,Data attributes不需要再宣告,你第一次設定值給它們的時候它們就自動存在了。舉例來說,如果 x 是 MyClass 這個物件的一個instance,底下這個程式碼就會印出 16 這個結果來:

x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print x.counter
del x.counter

第二種instance objecet可以使用的attribute references叫做方法( methods ) 。一個method就是一個隸屬於某個物件的函式。(在Python中,method一詞並不只特定用於類別的instances,其他的物件資料型態也可以有自己的 method,例如list物件就有很多methods像是append,insert,remove,sort等等。但是我們底下用到method這個詞的時候,除非特別說明,要不然我們倒是單獨指著instance objects 的method說的。)

一個instance objects 可以用的有效method名稱是由其類別所決定的。定義上來說,所有類別裡面(使用者定義)為函式物件的attribute,都會成為其instance的相對應method。所以在我們的例子裡, x.f 就是一個有效的method的reference,其原因是因為 MyClass.f 是一個函式;但是 x.i 就不是一個method的reference,因為 MyClass.i 不是一個函式。但是,要注意的是, x.f 和 MyClass.f 是兩回事,它是一個method物件( method object ),而非一個函式物件。

/////Method Objects(方法物件)

通常,一個method可以馬上被呼叫,例如:

x.f()

在我們的例子裡,這一個呼叫會傳回來 'hello world' 這個字串。但是,因為 x.f 是一個method物件,所以我們沒有必要馬上就呼叫它,我們可以把它儲存起來,然後再稍後再呼叫它。舉例如下:

xf = x.f
while 1:
print xf()

這個例子同樣的會一直不斷的印出 "hello world" 來。

到底,你的method被呼叫時,什麼事情發生了呢?你也許注意到了 當我們呼叫 x.f() 的時候並沒有傳入任何參數,但是我們在類別定義的時候確實有定義 f 所傳入的參數。到底是怎麼回事呢?當然,依照Python的定義,當一個函是需要參數而你呼叫時沒有傳入參數的話,是會引發一個例外狀況(exception)的,甚至這個傳入的參數沒有被用到也是一樣…

事實上,你也許已經猜到答案了。對於method來說有一個較特殊的事是,method所處的物件會被當作函式傳入的第一個參數。所以在我們的例子裡面,當我們呼叫 x.f() 的時候,事實上我們是呼叫 MyClass.f(x) 。一般來說,如果你呼叫method的時候傳了 n 個參數,其實你是呼叫背後所代表之類別的函式,而且該method所在的物件會插入在傳入的參數中當作第一個參數。

如果你還不了解到底method如何運作的話,你也許可以看看它的實作來更了解它。當一個instance的attribute被reference,而這個attribute又不是一個data attribute的時候,該instance的類別會被尋找。如果這個class attribute的名字在類別裡面代表的是一個函式物件的話,就會有一個method物件被創造出來。這個method物件是一個由這個instance物件(的指標),以及剛剛找到的這個函式物件所包裝起來的一個抽象的物件。當這個method物件被帶著一串參數呼叫的時候,這個method物件會先打開原來的包裝,然後會用instance物件(的指標)以及那一串傳進來的參數組成新的參數串,然後我們再用這個新的參數串來呼叫在method物件裡面的函式物件。

/////一些隨意的想法

[這些東西其實應該更多花點心思加以處理的…]

如果data attributes和method attributes 有相同名稱的話,data attributes 會蓋過method attributes 。要避免這個命名的衝突(這常常是許多bug的由來),你可能需要一些命名的規則。比如說,讓method的名稱都是大寫的,在data attribute的前面加上一些小字串(或者是底線),或者對於method都用動詞,對data attribute都用名詞。

除了一般object的使用者(client)之外,Data attributes也可以在method裡面被使用到。也就是說,類別(class)是不能用來實作出純粹的抽象資料型態(abstract data types)的。事實上,再Python裡面沒有東西可以保證資料的隱藏(data hiding),我們只能仰賴彼此的約定及尊重了。(另一方面來說,用C寫成的Python是可能完全隱藏其實作的細節並且在需要的時候可以控制對物件的存取權限的;這是用來給C所寫成的Python延伸機制(extension to Python)所使用的。)

使用data attributes的人要特別小心,你有可能把由method所管理的data attributes弄得一蹋糊塗。值得注意的是,類別的使用者可以自行在instance物件裡面加入data attributes,只要小心處理命名的問題,這不會對method的正確性有所影響。再次提醒,你可以用命名的規則來避免此事發生。

從method裡面要使用data attributes (或者是其他的methods)並沒有捷徑。我發現這樣的好處是程式的可讀性會增加很多,因為當你在讀method的程式碼的時候,local變數跟instance變數混淆的機會就會少很多。

習慣上,我們把一個method的第一個參數叫做 self 。這只是一個習慣而已, self 這個名字對Python來說完全沒有什麼特殊的意義。(但是你要注意,如果你不用這一個習慣的話,對於某些讀你程式的Python程式設計師來說,也許你程式的可讀性就低了一點。而且可能有一些類似像 class browser 之類的程式是靠這個約定來分辨class的特性,所以你不遵守的話,你的類別它們可能就讀不懂)

所有的在類別裡面的函式物件,在定義上都是該類別之instance的一個method。在類別裡面的意思不限定於一定要在文字上是在類別的定義裡面,你也可以把一個函式物件設定給一個在類別裡面的local變數,這樣也算數的。比如說:

# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)

class C:
f = f1
def g(self):
return 'hello world'
h = g

現在 f, g 以及 h 都是類別 C attributes,而且都指涉(reference)到某個函式物件去。而且,如此做的話,當 C 有instance的時候, f , g 以及 h 都會變成instance的method(事實上 h 所指的函式是跟 g 同一個的)。值得注意的是,如果你真這樣做的話,你只是讓讀你程式的人頭昏眼花罷了。

你也可以在method裡面呼叫其他的method,你所需要的只是用 self 這個參數的method attribute就可以了。例如:

class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)

method跟一般的函式物件一樣可以使用全域名稱(global name)。Method的global scope所指的是類別的定義所存在的module,(注意:類別本身絕不會是一個global scope!)。 你大概很少有機會在method裡面會需要用到global scope,但是你還是可以使用global scope的,method可以使用在global scope之中所import進來的函式以及module,也可以使用在global scope裡面定義的函式及類別。通常,包含method的這個類別本身就定義在這個global space裡面,而且下一段我們就要講到為什麼你會需要在method裡面用到自己本身的類別。

/////繼承(Inheritance)

當然啦,一個程式語言如果沒有繼承的話就不需要擔心類別(``class'')這個字了。一個子類別(derived class)的定義看起來是這樣的:

class DerivedClassName(BaseClassName):

.
.
.

其中,基礎類別的名字 BaseClassName 這個字必須是在子類別所處的scope裡面有定義的。除了直接使用基礎類別的名字之外,你也可以使用一個expression。這在當你的基礎類別是定義在別的module裡的時候特別有用:

class DerivedClassName(modname.BaseClassName):

子類別定義的執行過程與基礎類別定義的執行過程是一樣的。當一個類別物件被創造出來時,基礎類別也同樣會存在記憶體中。這是為了要確保能夠找到正確的attribute的所在,如果你的子類別沒有定義某個attribute的話,就會自動去找基礎類別的定義。如果這個基礎類別也是某個類別的子類別的話,這個法則是一直延伸上去的。

子類別的特例化(instantiation)也沒有什麼特別之處,使用 DerivedClassName() 就會創造出子類別的一個新的instance。子類別的method 則是由以下的過程來尋找:會先找該類別的attribute,然後如果需要的話會沿著繼承的路線去找基礎類別,如果找到任何的函式物件的話,這個method的參考(reference)就是有效的。

子類別可以override基礎類別裡的method。因為method在呼叫自己物件的其他method的時候沒有特別的權限,當一個基礎類別的method呼叫原屬於該基礎類別的method的時候,有可能真正呼叫到的是一個在子類別裡面定義的override的method。(給C++的程式設計師們:所有在Python裡面的method都是 virtual 的。)

一個在子類別裡面override的method也許會需要延伸而非取代基礎類別裡面同名的method,這時候你就需要呼叫在基礎類別裡面的method:你只需要呼叫 “BaseClassName.methodname(self, arguments)” 就可以了。這對於類別的使用者來說,有時候也是有用的。(注意的是,如果你要這樣做,你需要將基礎類別定義在global scope或是import進來global scope 裡面。)

/////多重繼承

Python也支援部分的多重繼承形式。一個類別如果要繼承多個基礎類別的話,其形式如下:

class DerivedClassName(Base1, Base2, Base3):

.
.
.

唯一需要解釋的規則是,當你尋找一個attribute的定義時你要如何尋找。其規則是先深,而後由左至右(depth-first, left-to-right)。所以當你要找一個在子類別 DerivedClassName 裡面的attribute卻找不到時,會先找 Base1 ,然後沿著 Base1 的所有基礎類別尋找,如果找完還沒有找到的話再找 Base2 及其基礎類別,依此類推。

(也許有些人認為先左至右然後在深才對,應該是先找 Base2 及 Base3 ,然後才找 Base1 的基礎類別。如果你這樣想的話,你可以再想一想,當你找 Base1 的時候,你需要先知道這個attribute到底是定義在 Base1 本身或是其基礎類別裡面,如此才不會與 Base2 裡面的attribute有同名的困擾。如果你使用先深,而後由左至右的規則的話,就不會有這個困擾。)

大家都知道如果不小心使用的話,多重繼承可能變成在維護程式時的一個惡夢。Python仰賴程式設計師們的約定俗成的習慣來避免可能的名稱衝突。例如一個眾所週知多重繼承的問題,如果一個類別繼承了兩個基礎類別,這兩個基礎類別又分別繼承了同一個基礎類別。也許你很容易就了解在這樣的情況下到底會是什麼狀況,(這個instance將會只有一個單一共用基礎類別的“instance variables”或是data attributes),但是很難了解這到底有什麼用處。

/////Private變數

在Python裡面只有有限度的支援類別中的private指稱 (class-private identifiers,譯:指變數及函式)。任何的identifier,在之前是以 __spam 這個形式存在的(最前面至少要有兩個底線,最後面最多只能有一個底線) 現在都要以 _classname__spam 這個形式來取代之。在這裡的 classname 指的是所在的類別名稱,拿掉所有前面的底線。這個名稱的變化不受限於這個identifier其語法上所在的位置,所以可以套用在定義類別的private instance,類別變數,method,global名稱,甚至用來儲存 其他 的類別instance裡,對目前這個類別來說是private的instance變數。當這個變化過的名稱超過255個字元時,有可能超過的部分是會被截掉的。在類別之外,或者是當類別的名稱只包含底線的時候,就沒有任何名稱的變化產生。

這個名稱的變化主要是用來給類別有一個簡單的方法來定義“private”的instance變數及methods,而不需要擔心其他子類別裡面所定義的instance變數,或者與其他的在類別之外的程式碼裡的instance變數有所混淆。注意的是這個變化名稱的規則主要是用來避免意外的,如果你存心要使用或修改一個private的變數的話,這還是可行的。某方面來說這也是有用的,比如說用在除錯器(debugger)上面,這也是為什麼這個漏洞沒有被補起來的一個原因。(如何製造bug:如果一個類別繼承自某個基礎類別時用了相同的名字,這會使得你可以從子類別裡面使用基礎類別裡的private的變數。)

值得注意的是,被傳到 exec, eval() 或 evalfile() 的程式碼並不用考慮引發這個動作的類別是目前的類別,這是相類似於 global 敘述的效果,但是這個效果只限於這個程式碼是一起被編譯器編譯成bytecode的時候的。同樣的限制也存在於 getattr() , setattr() 以及 delattr(),或是當直接使用 __dict__ 的時候。

底下這個例子是一個類別裡面定義了自己的 __getattr__() 以及 __setattr__() 兩個方法,並且把所有的attributes都儲存在private的變數裡面。這個例子適用於所有的Python版本,甚至是包括在這個特性加入之前的版本都可以:

class VirtualAttributes:
__vdict = None
__vdict_name = locals().keys()[0]

def __init__(self):
self.__dict__[self.__vdict_name] = {}

def __getattr__(self, name):
return self.__vdict[name]

def __setattr__(self, name, value):
self.__vdict[name] = value

/////其它

有的時候如果有一個像是Pascal的“record”,或者是C的“struct”這類的資料型態是很方便的,這類的資料型態可以把一些的資料成員都放在一起。這種資料型態可以用空白的類別來實作出來,例如:

class Employee:
pass

john = Employee() # Create an empty employee record

# Fill the fields of the record
john.name = ‘John Doe’
john.dept = ‘computer lab’
john.salary = 1000

如果有一段的Python程式碼需要一個特別的抽象資料型態的時候,通常你可以傳給這段程式碼一個類似功能的類別來代替。例如,如果你有一個函式是用來格式化一些來自於file物件的資料,你可以定義一個類別,類別裡面有類似 read() 以及 readline() 之類method可以從一個字串緩衝區(string buffer)讀出資料,然後再把這個類別傳入函式當作參數。

Instance的method物件也可以有attributes: m.im_self 就是其method為instance的一個物件, m.im_func 就是這個method相對應的函式物件。

/////例外(Exceptions)也可以是類別

使用者自訂的exception不用只是被限定於只是字串物件而已,它們現在也可以用類別來定義了。使用這個機制的話,就可以創造出一個可延伸的屋exception的階層了。

有兩個新的有效的(語意上的)形式現在可以用來當作引發exception的敘述:

raise Class, instance

raise instance

在第一個形式裡面, instance 必須是 Class 這個類別或其子類別的一個instance。第二種形式其實是底下這種形式的一個簡化:

raise instance.__class__, instance

所以現在在except的語句裡面就可以使用字串物件或是類別都可以了。一個在exception子句裡的類別可以接受一個是該類別的exception,或者是該類別之子類別的exception。(相反就不可以了,一個except子句裡如果用的是子類別,就不能接受一個基礎類別的exception。)例如,下面的程式碼就會依序的印出B, C, D來:

class B:
pass
class C(B):
pass
class D(C):
pass

for c in [B, C, D]:
try:
raise c()
except D:
print “D”
except C:
print “C”
except B:
print “B”

值得注意的是,如果上面的例子裡的except子句次序都掉轉的話(也就是 “except B” 是第一個),這樣子印出來的就是B, B, B,也就是只有第一個可以接受的except子句被執行而已。

當一個沒有被處理到的exception是一個類別時,所印出來的錯誤信息會包含其類別的名稱,然後是(:),然後是這個instance用內建的 str() 函式轉換成的字串。

你喜歡這篇文章嗎? 馬上分享它:

相關文章:

  1. Python 語法隨手記錄 – 2
  2. Python 語法隨手記錄 – 1
This entry was posted in Python, 隨記 and tagged , . Bookmark the permalink.

Leave a Reply