Python 3 是 Guido van Rossum 功能強大的通用編程語言的最新版本。它雖然打破了與 2.x 版本的向後兼容性,但卻清理了某些語法方面的問題。本文是這個由兩部分組成的系列文章中的第二篇,本文構建在此系列 前一期文章 的基礎之上,內容涵蓋了 Python 更多的新特性和更高深的一些主題,比如在抽象基類、元類和修飾符等方面的變化。
有關 Python 版本 3—,也即 Python 3000 或 Py3K— 的前一篇文章討論了 Python 內打破向後兼容性的一些基本變化,比如新的 print() 函數、 bytes 數據類型以及 string 類型的變化。本文是該系列文章的第 2 部分,探究了更為高深的一些主題,比如抽象基類(ABC)、元類、函數註釋和修飾符(decorator)、整型數(integer literal)支持、數值類型層次結構以及拋出和捕獲異常,其中的大多數特性仍然會打破與版本 2x 產品線的向後兼容性。
類修飾符
在 Python 之前的版本中,對方法的轉換必須在方法定義之後進行。對於較長的方法,此要求將定義的重要組成部分與 Python Enhancement Proposal (PEP) 318(有關鏈接,請參見 參考資料)給出的外部介面定義分離。下面的代碼片段演示了這一轉換要求:
def myMethod(self): # do something myMethod = transformMethod(myMethod) |
為了讓此類情景更易於讀懂,也為了避免必須多次重用相同的方法名,在 Python 版本 2.4 中引入了方法修飾符。
修飾符 是一些方法,這些方法可以修改其他方法並返回一個方法或另外一個可調用對象。對它們的註釋是在修飾符的名稱前冠以 “at” 符號(@)— 類似 Java™ 註釋的語法。清單 2 顯示了實際應用中的修飾符。
@transformMethod def myMethod(self): # do something |
修飾符是一些純粹的語法糖(syntactic sugar)— 或者(如 Wikipedia 所言)“對計算機語言語法的補充,這些補充並不影響語言的功能,而是會讓語言變得更易於被人使用。”修飾符的一種常見用法是註釋靜態方法。比如,清單 1 和清單 2 相當,但清單 2 更容易被人讀懂。
定義修飾符與定義其他方法無異:
def mod(method): method.__name__ = "John" return method @mod def modMe(): pass print(modMe.__name__) |
更棒的是 Python 3 現在不僅支持針對方法的修飾符,並且支持針對類的修飾符,所以,取代如下的用法:
class myClass: pass myClass = doSomethingOrNotWithClass(myClass) |
我們可以這樣使用:
@doSomethingOrNotWithClass class myClass: pass |
元類
元類 是這樣一些類,這些類的實例也是類。Python 3 保留了內置的、用來創建其他元類或在運行時動態創建類的 metaclass 類型。如下的語法仍舊有效:
>>>aClass = type('className', (object,), {'magicMethod': lambda cls : print("blah blah")}) |
上述語法接受的參數包括:作為類名的字元串、被繼承對象的元組(可以是一個空的元組)和一個包含可以添加的方法的字典(也可以是空的)。當然,也可以從類型繼承並創建您自己的元類:
class meta(type): def __new__(cls, className, baseClasses, dictOfMethods): return type.__new__(cls, className, baseClasses, dictOfMethods) |
|
注意:如果上面兩個例子起不到任何作用,我強烈建議您閱讀 David Mertz 和 Michele Simionato 合寫的有關元類的系列文章。相關鏈接,請參見 參考資料。
請注意,現在,在類定義中,關鍵字參數被允許出現在基類列表之後 — 通常來講,即 class Foo(*bases, **kwds): pass。使用關鍵字參數 metaclass 將元類傳遞給類定義。比如:
>>>class aClass(baseClass1, baseClass2, metaclass = aMetaClass): pass |
舊的元類語法是將此元類分配給內置屬性 __metaclass__:
class Test(object): __metaclass__ = type |
而且,既然有了新的屬性 — __prepare__ — 我們就可以使用此屬性為新的類名稱空間創建字典。在類主體被處理之前,先會調用它,如清單 3 所示。
def meth(): print("Calling method") class MyMeta(type): @classmethod def __prepare__(cls, name, baseClasses): return {'meth':meth} def __new__(cls, name, baseClasses, classdict): return type.__new__(cls, name, baseClasses, classdict) class Test(metaclass = MyMeta): def __init__(self): pass attr = 'an attribute' t = Test() print(t.attr) |
我們從 PEP 3115 節選了一個更為有趣的例子,如清單 4 所示,這個例子創建了一個具有其方法名稱列表的元類,而同時又保持了類方法聲明的順序。
# The custom dictionary class member_table(dict): def __init__(self): self.member_names = [] def __setitem__(self, key, value): # if the key is not already defined, add to the # list of keys. if key not in self: self.member_names.append(key) # Call superclass dict.__setitem__(self, key, value) # The metaclass class OrderedClass(type): # The prepare function @classmethod def __prepare__(metacls, name, bases): # No keywords in this case return member_table() # The metaclass invocation def __new__(cls, name, bases, classdict): # Note that we replace the classdict with a regular # dict before passing it to the superclass, so that we # don't continue to record member names after the class # has been created. result = type.__new__(cls, name, bases, dict(classdict)) result.member_names = classdict.member_names return result |
在元類內所做的這些改變有幾個原因。對象的方法一般存儲於一個字典,而這個字典是沒有順序的。不過,在某些情況下,若能保持所聲明的類成員的順序將非常有用。這可以通過讓此元類在信息仍舊可用時,較早地涉入類的創建得以實現 — 這很有用,比如在 C 結構的創建中。藉助這種新的機制還能在將來實現其他一些有趣的功能,比如在類構建期間將符號插入到所創建的類名稱空間的主體以及對符號的前向引用。PEP 3115 提到更改語法還有美學方面的原因,但是對此尚存在無法用客觀標準解決的爭論(到 PEP 3115 的鏈接,請參見 參考資料)。
抽象基類
正如我在 Python 3 初探,第 1 部分:Python 3 的新特性 中提到的,ABC 是一些不能被實例化的類。Java 或 C++ 語言的程序員應該對此概念十分熟悉。Python 3 添加了一個新的框架 —abc— 它提供了對 ABC 的支持。
這個 abc 模塊具有一個元類(ABCMeta)和 修飾符(@abstractmethod 和 @abstractproperty)。如果一個 ABC 具有一個 @abstractmethod 或 @abstractproperty,它就不能被實例化,但必須在一個子類內被覆蓋。比如,如下代碼:
>>>from abc import * >>>class C(metaclass = ABCMeta): pass >>>c = C() |
這些代碼是可以的,但是不能像下面這樣編碼:
>>>from abc import * >>>class C(metaclass = ABCMeta): ... @abstractmethod ... def absMethod(self): ... pass >>>c = C() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class C with abstract methods absMethod |
更好的做法是使用如下代碼:
>>>class B(C): ... def absMethod(self): ... print("Now a concrete method") >>>b = B() >>>b.absMethod() Now a concrete method |
ABCMeta 類覆蓋屬性 __instancecheck__ 和 __subclasscheck__,藉此可以重載內置函數 isinstance() 和 issubclass()。要向 ABC 添加一個虛擬子類,可以使用 ABCMeta 提供的 register() 方法。如下所示的簡單示例:
>>>class TestABC(metaclass=ABCMeta): pass >>>TestABC.register(list) >>>TestABC.__instancecheck__([]) True |
它等同於使用 isinstance(list, TestABC)。您可能已經注意到 Python 3 使用 __instancecheck__,而非 __issubclass__,使用 __subclasscheck__,而非 __issubclass__,這看起來更為自然。若將參數 isinstance(subclass, superclass) 反轉成,比如 superclass.__isinstance__(subclass),可能會引起混淆。可見,語法 superclass.__instancecheck__(subclass) 顯然更好一點。
在 collections 模塊內,可以使用幾個 ABC 來測試一個類是否提供了特定的一個介面:
>>>from collections import Iterable >>>issubclass(list, Iterable) True |
表 1 給出了這個集合框架的 ABC。
ABC | Inherits |
---|---|
Container | |
Hashable | |
Iterable | |
Iterator | Iterable |
Sized | |
Callable | |
Sequence | Sized, Iterable, Container |
MutableSequence | Sequence |
Set | Sized, Iterable, Container |
MutableSet | Set |
Mapping | Sized, Iterable, Container |
MutableMapping | Mapping |
MappingView | Sized |
KeysView | MappingView, Set |
ItemsView | MappingView, Set |
ValuesView | MappingView |
|
ABC 類型層次結構
Python 3 現支持能代表數值類的 ABC 的類型層次結構。這些 ABC 存在於 numbers 模塊內並包括 Number、 Complex、Real、 Rational 和 Integral。圖 1 顯示了這個數值層次結構。可以使用它們來實現您自己的數值類型或其他數值 ABC。
|
新模塊 fractions 可實現這個數值 ABC Rational。此模塊提供對有理數演算法的支持。若使用 dir(fractions.Fraction),就會注意到它具有一些屬性,比如 imag、 real 和 __complex__。根據數值塔的原理分析,其原因在於 Rationals 繼承自 Reals,而 Reals 繼承自 Complex。
拋出和捕獲異常
在 Python 3 內,except 語句已經被修改為處理語法不清的問題。之前,在 Python version 2.5 內,try . . . except 結構,比如:
>>>try: ... x = float('not a number') ... except (ValueError, NameError): ... print "can't convert type" |
可能會被不正確地寫為:
>>> try: ... x = float('not a number') ... except ValueError, NameError: ... print "can't convert type" |
后一種格式的問題在於 ValueError 異常將永遠捕獲不到,因為解釋器將會捕獲 ValueError 並將此異常對象綁定到名稱 NameError。這一點可以從下面的示例中看出:
>>> try: ... x = float('blah') ... except ValueError, NameError: ... print "NameError is ", NameError ... NameError is invalid literal for float(): not a number |
所以,為了處理語法不清的問題,在想要將此異常對象與另一個名稱綁定時,逗號(,)會被替換成關鍵字 as。如果想要捕獲多個異常,必須使用括弧(())。清單 5 中的代碼展示了 Python 3 內的兩個合乎語法的示例。
# bind ValueError object to local name ex try: x = float('blah') except ValueError as ex: print("value exception occurred ", ex) # catch two different exceptions simultaneously try: x = float('blah') except (ValueError, NameError): print("caught both types of exceptions") |
異常處理的另一個改變是異常鏈 — 隱式或顯式。清單 6 給出了隱式異常鏈的一個示例。
def divide(a, b): try: print(a/b) except Exception as exc: def log(exc): fid = open('logfile.txt') # missing 'w' print(exc, file=fid) fid.close() log(exc) divide(1,0) |
divide() 方法試圖執行除數為零的除法,因而引發了一個異常:ZeroDivisionError。但是,在異常語句的嵌套 log() 方法內,print(exc, file=fid) 試圖向一個尚未打開的文件進行寫操作。Python 3 拋出異常,如清單 7 所示。
Traceback (most recent call last): File "chainExceptionExample1.py", line 3, in divide print(a/b) ZeroDivisionError: int division or modulo by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File "chainExceptionExample1.py", line 12, in <module> divide(1,0) File "chainExceptionExample1.py", line 10, in divide log(exc) File "chainExceptionExample1.py", line 7, in log print(exc, file=fid) File "/opt/python3.0/lib/python3.0/io.py", line 1492, in write self.buffer.write(b) File "/opt/python3.0/lib/python3.0/io.py", line 696, in write self._unsupported("write") File "/opt/python3.0/lib/python3.0/io.py", line 322, in _unsupported (self.__class__.__name__, name)) io.UnsupportedOperation: BufferedReader.write() not supported |
請注意,這兩個異常都被處理。在 Python 的早期版本中,ZeroDivisionError 將會丟失,得不到處理。那麼這是如何實現的呢?__context__ 屬性,比如 ZeroDivisionError,現在是所有異常對象的一部分。在本例中,被拋出的 IOError 的 __context__ 屬性 “仍為” __context__ 屬性內的 ZeroDivisionError。
除 __context__ 屬性外,異常對象還有一個 __cause__ 屬性,它通常被初始化為 None。這個屬性的作用是以一種顯式方法記錄異常的原因。__cause__ 屬性通過如下語法設置:
>>> raise EXCEPTION from CAUSE |
它與下列代碼相同:
>>>exception = EXCEPTION >>>exception.__cause__ = CAUSE >>>raise exception |
但更為優雅。清單 8 中所示的示例展示了顯式異常鏈。
class CustomError(Exception): pass try: fid = open("aFile.txt") # missing 'w' again print("blah blah blah", file=fid) except IOError as exc: raise CustomError('something went wrong') from exc |
正如之前的例子所示,print() 函數拋出了一個異常,原因是文件尚未打開以供寫入。清單 9 給出了相應的跟蹤。
Traceback (most recent call last): File "chainExceptionExample2.py", line 5, in <module> fid = open("aFile.txt") File "/opt/python3.0/lib/python3.0/io.py", line 278, in __new__ return open(*args, **kwargs) File "/opt/python3.0/lib/python3.0/io.py", line 222, in open closefd) File "/opt/python3.0/lib/python3.0/io.py", line 615, in __init__ _fileio._FileIO.__init__(self, name, mode, closefd) IOError: [Errno 2] No such file or directory: 'aFile.txt' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "chainExceptionExample2.py", line 8, in <modulei> raise CustomError('something went wrong') from exc __main__.CustomError: something went wrong |
請注意,在異常跟蹤中的一行 “The above exception was the direct cause of the following exception,” 之後的是另一個對導致 CustomError “something went wrong” 異常的跟蹤。
添加給異常對象的另一個屬性是 __traceback__。如果被捕獲的異常不具備其 __traceback__ 屬性,那麼新的跟蹤就會被設置。如下是一個簡單的例子:
from traceback import format_tb try: 1/0 except Exception as exc: print(format_tb(exc.__traceback__)[0]) |
請注意,format_tb 返回的是一個列表,而且此列表中只有一個異常。
|
整數支持和語法
Python 支持不同進位的整型字元串文本 — 八進位、十進位(最明顯的!)和十六進位 — 而現在還加入了二進位。八進位數的表示已經改變:八進位數現在均以一個前綴 0o 或 0O(即,數字零後跟一個大寫或小寫的字母 o)開頭。比如,八進位的 13 或十進位的 11 分別如下表示:
>>>0o13 11 |
新的二進位數則以前綴 0b 或 0B(即,數字零後跟一個大寫或小寫的字母 b)開頭。十進位數 21 用二進位表示應為:
>>>0b010101 21 |
oct() 和 hex() 方法已被刪除。
函數註釋
函數註釋 會在編譯時將表述與函數的某些部分(比如參數)相關聯。就其本身而言,函數註釋是無意義的 — 即,除非第三方庫對之進行處理,否則它們不會被處理。函數註釋的作用是為了標準化函數參數或返回值被註釋的方式。函數註釋語法為:
def methodName(param1: expression1, ..., paramN: expressionN)->ExpressionForReturnType: ... |
例如,如下所示的是針對某函數的參數的註釋:
def execute(program:"name of program to be executed", error:"if something goes wrong"): ... |
如下的示例則註釋了某函數的返回值。這對於檢查某函數的返回類型非常有用:
def getName() -> "isString": ... |
函數註釋的完整語法可以在 PEP 3107(相關鏈接,請參見 參考資料)內找到。
結束語
|
Python 3 最終的發布版在 2008 年 12 月份初就已經發布。自那之後,我曾經查閱過一些博客,試圖了解人們對向後不兼容性問題的反應。雖然,我不能斷言社區在某些程度上已經達成了官方的共識,但我閱讀過的博客所呈現的觀點呈兩極分化。Linux® 開發社區似乎不太喜歡轉變到版本 3,因為需要移植大量代碼。相比之下,因為新版本對 unicode 支持方面的改進,很多 Web 開發人員則歡迎轉變。
我想要說明的一點是,在您做決定是否要移植到新版本之前,應該仔細閱讀相關的 PEP 和開發郵件列表。這些 PEP 解釋了各項更改的初衷、帶來的益處及其實現。不難看出,這些更改的做出經過了深思熟慮和激烈討論。本系列所展示的這些主題旨在讓普通的 Python 程序員無需遍閱所有的 PEP 就能立即對這些更改有大致的概念。(責任編輯:A6)
[火星人 ] Python 3 初探,第 2 部分: 高級主題已經有922次圍觀