後端程式設計Python3-高階程式設計(面向物件-下)

本節是第五講的第十六小節下,本節主要介紹面向物件程式設計技巧(

類修飾器、抽象基類、多繼承、元類

)。

類修飾器(Class Decorators)

就像可以為函式與方法建立修飾器一樣,我們也可以為整個類建立修飾器。類修飾器以類物件(class語句的結果)作為引數,並應該返回一個類——通常是其修飾的類的修訂版。這一小節中,我們將研究兩個類修飾器,以便了解其實現機制。

在前面,我們建立了自定義組合類SortedList,該類聚集了一個普通列表,並將其作為私有屬性self。__list()。 8種SortedList方法簡單地將其工作傳遞給該私有屬性。 比如,下面展示了 SortedList。clear()方法與SortedList。pop()方法是如何實現的:

def clear(self):

self。__list =[]

def pop(self, index=-1):

return self。__list。pop(index)

對於clear()方法,我們沒有什麼可做的,因為list型別不存在相應的方法,但對於pop()方法以及SortedList授權的其他6種方法,我們可以簡單地呼叫list類的相應方法。這可以使用@delegate類修飾器(取自書中的util模組)實現。下面是新版SortedList類的起始處:

@Util。delegate(“__list”, (“pop”,”__delitem__“, ”__getitem__“,”__iter__“,”__reversed__“, “__str__”))

class SortedList:

def delegate(attribute_name, method_names):

def decorator(cls):

nonlocal attribute_name

if attribute_name。startswith(”__“):

attribute_name = ”_“+ cls。__name__ + attribute_name

for name in method_names:

setattr(cls, name, eval(”lambda self, *a, **kw:“,”self。{0}。{1}(*a, **kw)“。format(attribute_name, name)))

return cls

return decorator

第一個引數是待授權的屬性名,第二個引數是我們需要delegate()修飾器進行處理的方法或方法序列,以便我們自己不再做這個工作。SortedListDelegate。py檔案中的SortedList類使用了這種方法,因此不包含列出的方法的任何程式碼,即便該類完全支援這些方法。下面給出的是替我們實現這些方法的類修飾器:

我們不能使用普通的修飾器,因為我們需要向其傳遞引數,因此,我們建立了一 個函式,該函式接受我們的引數,並返回一個類修飾器,修飾器本身只接受一個引數, 該引數是一個類(就像函式修飾器接受單一的函式或方法作為其引數一樣)。

我們必須使用nonlocal,以便巢狀的函式使用的是來自外部範圍(而不會嘗試使 用來自自身範圍)的attribute_name。若有必要,我們必須可以糾正屬性名,以便考慮對私有屬性進行名稱操縱的情況。修飾器的行為非常簡單:對賦予delegate()函式的所有方法名進行迭代,對每一個方法名都建立一個新方法,並將其設定為給定方法名所在類的屬性。

我們使用eval()來建立每個被授權的方法,因為eval()可用於執行單一的語句,並 且,lambda語句可以生成一個方法或函式,比如,用於生成pop()方法的程式碼如下:

lambda self, *a, **kw: self。_SortedList__list。pop(*a, **kw)

我們使用了*與**這種引數形式,以便可以接受任何引數,即便被授權的方法可能有特定的引數列表形式。比如,list。pop()方法接受一個單一的索引位置引數(或無引數,此時預設處理最後一項),這種引數是可以的,因為如果傳遞的是錯誤的引數個數或引數型別,那麼被呼叫完成該項工作的list方法將產生適當的異常。

我們將檢視的第2個類修飾器巳經展示過,我們只需要提供__lt__()與__eq__()這兩個特殊方法(用於<與==),並自動生成所有其他甩於比較操作的方法。在該章沒有展示的是類定義的完整起點:

@Util。complete_comparisons

class FuzzyBool:

其他4個比較運算子是由complete_comparisons()類修飾器提供的,給定一個只定義了< (或<與==)的類,則修飾器將生成未給出的其他比較運算子,這是透過如下的一些邏輯等價關係實現的:

後端程式設計Python3-高階程式設計(面向物件-下)

如果待修飾的類有<與==運算子,那麼修飾器將使用這兩個運算子;如果只提供了< 運算子,就回退到使用<完成所有任務的情況。(實際上,提供了<,則Python會自動地生成>;提供了==,則Python會自動生成!=,因此只要實現3個運算子<、<=與==,Python 就完全可以推斷出其他運算子。然而,透過使用類修飾器,可以將實現運算子的工作量最小化到只有<,這是方便的,並可以確保所有比較運算子使用相容的邏輯。)

def complete_comparisons(cls):

assert cls。__lt__ is not object。__lt__,(”{0} must define < and ideally ==“。format(cls。__name__)

if cls。__eq__ is object。__eq__:

cls。__eq__ = lambda self, other: (not(cls。__lt__(self, other) or cls。__lt__(other, self)))

cls。__ne__ = lambda self, other: not cls。__eq__(self, other)

cls。__gt__ = lambda self, other: cls。__It__(other, self)

cls。__le__= lambda self, other: not cls。__lt__(other, self)

cls。__ge__= lambda self, other: not cls。__It__(self, other)

return ds

修飾器面臨的一個問題是,object類(每個物件類最終繼承的都是該類)定義了所有這6個比較運算子,如果使用都會產生TypeError異常。因此,我們需要知道< 與 ==是否巳被重新實現(因此是可用的),透過將類中相關的正在進行修飾的特殊方法物件中的方法進行比較,就可以很容易地做到。

如果修飾的類不包含自定義的<,那麼斷言將失敗,因為這是修飾器的最小需求。 如果有一個自定義的==,我們就使用,否則,就建立一個。之後,所有其他方法都被建立,而修飾的類(現在包含所有6個比較方法)將被返回。

使用類修飾器可能是最簡單的也是最直接的改變類的方式,另一種方法是使用元類,本章後面部分將關注這一主題。

抽象基類(Abstract Base Classes)

抽象基類(ABC)也是一個類,但不是用於建立物件,而是用於定義介面,也就是說,列出一些方法與特性——繼承自ABC的類必須對其進行實現。這種機制是有用的,因為我們可以將抽象基類用作一種允諾——任何自ABC衍生而來的類必須實現抽象基類指定的方法與特性。

抽象基類包含至少一種抽象方法與特性,抽象方法在定義時可以沒有實現(其suite 為pass,或者,在子類中強制對其重新實現則產生NotImplementedError()),也可以包含實際的(具體的)實現,並可以從子類中呼叫,比如,存在某個通常情況。抽象基類也可以包含其他具體(非抽象)方法與特性。

只有在實現了繼承而來的所有抽象方法與抽象特性之後,自ABC衍生而來的類才可以建立例項。對那些包含具體實現的抽象方法(即便只是pass),衍生類可以簡單地使用super()來呼叫ABC的實現版本。任何具體方法與特性都可以透過繼承獲取,與通常一樣。所有ABC必須包含元類abc。ABCMeta (來自abc模組),或來自其某個子類。後面我們會講解元類相關的一些內容。

Python提供了兩組抽象基類,一組在collections模組中,另一組在numbers模組中。這兩個模組可用於對物件的相關屬性進行査詢,比如,給定變數x,使用isinstance(x,

collections。MutableSequence),可以判斷其是否是一個序列,也可以使用isinstance(x, numbers。Integral)來判斷其是否是一個整數。由於Python支援動態型別機制(我們不必要知道或關心某個物件的型別,而只需要知道其是否支援將要對其施加的操作),因此, 這種查詢功能是特別有用的。數值型與組合型ABC分別在表1與表2中列出,其他的主要ABC是io。IOBase,該抽象基類是所有檔案與流處理相關類的父類。

表1 數值模組的抽象基類

ABC 繼承自 API 例項

Number object complex、

decimal。Decimals、floats、fractions。Fraction、int

Complex Number ==、!=、+、-、*、/、abs()、bool()、complex()、conjugate(), 以及real與imag特性 complex、 decimal。Decimal、 float、 fractions。Fraction、int

Real Complex <,<=、==、!=、>=、>、+、-、*、/、//、%、abs()、bool()、complex()、conjugate()、divmod()、float()、math。ceil()、 math。floor(),round()、trunc();以及 real 與 imag 特性 decimal。Decimal、float、fractions。Fraction、int

Rational Real <、<=、==、!=、>=、>、+、-、*、/、//、%、abs()、 bool()、complex()。 conjugate()、divmod()、float()、 math。ceil()、 math。floor(), round(), trunc();以及 real、 imag、numerator denominator 特性 fractions。Fractionint

Integral Rational <、<=、==、!=、>=、>、+、-、*、/、//、%、<<、>>、 ~、&、^、|、abs()、bool()、 complex(),conjugate()、 divmod()、 float()、math。ceil()、math。floor()、pow()、 round()、TRunc();以及 real、imag、numerator 與 denominator 特性 int

表2組合模組的主抽象基類

ABC 繼承自 API 例項

Callable object () 所有函式、方法以及 lambdas

Container object in bytearray、bytes、dict、 frozenset、 list、set、str、 tuple

Hashable object hash() bytes、 frozenset、str、 tuple

Iterable object iter()

Iterator Iterable iter()、next()

Sized object len() bytearray、bytes、collections。deque、dict、 frozenset、 list、set、str、 tuple

Mapping Container、Iterable、Sized ==、 !=、[]、len()、 iter()、in、get()、items()、 keys()、 values() dict

Mutable-Mapping Mapping ==、!=、[]、del、len()、iter()、in、 clear()、get()、 items()、keys()、 pop()、 popitem()、setdefauIt()、 update()、 values() dict

Sequence Container、Iterable、 Sized []、len()、iter()、 reversed()、in、count()、 index() bytearray、bytes、list、 str、tuple

Mutable-Sequence Container、Iterable、 Sized []、+=、del、 len()、iter()、 reversed()、 in、 append()、 count()、 extend()、 index()、 insert()、pop()、 remove()、reverse() bytearray、list

Set Container、 Iterable、Sized <、<=、==、!=、=>、 >、&、|、^、len()、iter()、in、isdisjoint() frozenset、set

MutableSet Set <、<=、==、!=、=>、>、 &、|、^、&=、|=、^=、-=、len()、iter()、in、 add()、 clear()、 discard()、isdisjoint()、 pop()、remove() set

為完全整合自己的自定義數值型類與組合類,應該使其與標準的ABC匹配。比 如,SortedList類是一個序列。事實是,如果L是一個SortedList,那麼isinstance(L, collections。Sequence)將返回False。為解決這一問題,一種簡單的方式將該類繼承自相關的ABC:

class SortedList(collections。Sequence):

透過將collections。Sequence作為基類,isinstance()此時將返回True。並且,我們需要實現__init__()(或__new__())、__ getitem__()以及__len__()等方法(我們進行了實現)。collections。Sequence ABC 還為__contains__()、__iter__()、__reversed__()、 count() 以及index()等方法提供了具體(非抽象)的實現。在SortedList類中,我們重新實現了所有這些方法,如果需要,我們也可以使用方法的ABC版——只要不對其進行重新實現即可。我們不能將SortedList作為

collections。MutableSequence的一個子類(即使列表是可變的),這是因為SortedList不包括collections。MutableSequence必須提供的所有方法,比如__setitem__()與append()。(這裡的SortedList的程式碼在SortedListAbc。py 檔案中,在元類的介紹中,我們將看到使SortedList成為collections。Sequence的另一 種替代方案。)

在瞭解瞭如何使得自定義類完全整合於標準的ABC之後,我們開始瞭解ABC的另一種用途:為自己的自定義類提供介面允諾。我們將檢視3個相當不同的例項,以 便了解建立與使用ABC的不同方面。

我們首先從一個非常簡單的例項開始,該例項展示瞭如何處理可讀/可寫的特性。 該類用於表示國產的應用裝置,建立的每臺應用裝置必須包含一個只讀的型號字串 以及可讀/可寫的價格,還要求必須對ABC的__init__()方法進行重新實現。下面給出 該 ABC (取自 Appliance。py 檔案),我們沒有展示 import abc 語句,對 abstractmethod() 與abstractproperty()函式而言,必須先執行該匯入語句,這兩個函式都可以用作修飾器:

class Appliance(metaclass=abc。ABCMeta):

@abc。abstractmethod

def __init__(self, model, price):

self。__model = model

self。price = price

def get_price(self):

return self。__price

def set_price(self, price):

self。__price = price

price = abc。abstractproperty(get_price, set_price)

@property

def model(self):

return self。__model

我們將該類的元類設定為abc。ABCMeta,因為對ABC而言,這是必需的。當然, 也可以將其設定為任意的abc。ABCMeta子類。我們將__init__()作為一個抽象方法,以確保必須對其進行重新實現,我們也提供了一個實現,並希望(但不強制)繼承者呼叫該實現。為實現一個抽象的可讀和寫特性,我們不能使用修飾器語法,並且,我們沒有為獲取者與設定者使用私有名稱,因為這樣做對子類化是不方便的。

price特性是抽象的(因此我們不能使用@property修飾器),並且是可讀/寫的。 這裡,我們遵循一種通常的模式,用於將私有可讀/寫資料(比如__price)作為特性的 情況:我們在__init__()方法中初始化property,而不是直接設定私有資料——這可以確保設定者被呼叫(也可以潛在地進行驗證或其他工作,儘管在本例項中沒有)model特性是非抽象的,因此子類不必對其進行重新實現,我們可以使用 ©property修飾器使其成為一個特性。這裡,我們遵循一種通常的模式,用於將私有隻讀資料(比如__model)作為特性的情況:我們在__init__()方法中對私有__model資料 進行一次設定,並透過只讀的model特性提供讀訪問。

要注意的是,不能建立Appliance物件,因為該類包含了抽象屬性。下面給出一 個子類例項:

class Cooker(Appliance):

def __init__(self, model, price, fuel):

super()。__init__(model, price)

self。fuel = fuel

price = property(lambda self: super()。price,lambda self, price: super()。set_price(price))

Cooker類必須重新實現__init__()方法與price特性,對特性,我們只是將所有工作傳遞給基類。model這一隻讀特性是繼承而來的。我們可以以Appliance為基礎建立更多的類,比如Fridge、Toaster等。

#下面將要査看的ABC更短小,是一個用於文字過濾函子(在檔案TextFilter。py中) 的 ABC;

class TextFiIter(metaclass=abc。ABCMeta):

@abc。abstractproperty

def is_transformer(self):

raise NotlmplementedError()

@abc。abstractmethod

def __call__(self):

raise NotlmplementedError()

TextFilterABC沒有提供任何功能,其存在純粹是為了定義一個介面,這裡就是一 個只讀特性,is_transformer以及一個—call__()方法,所有子類必須提供。由於抽象特性與方法沒有實現,我們不希望子類對其進行呼叫,因此,這裡不再使用無用的pass 語句,而是在嘗試對其呼叫(比如透過super()呼叫)時產生異常。

#s下面是一個簡單的子類:

class CharCounter(TextFilter):

@property

def is_transformer(self):

return False

def __call__(self, text, chars):

count = 0

for c in text:

if c in chars:

count += 1

return count

這一文字過濾器並不是一個轉換器,因為其功能並不是對給定的文字進行轉換, 而是簡單地返回指定字元在文字中出現的計數值,下面是一個使用例項:

vowel_counter = CharCounter()

vowel_counter(”dog fish and cat fish“, ”aeiou“) # returns: 5

還提供了兩個文字過濾器,RunLengthEncode與RunLengthDecode,兩者都是轉換器,下面展示瞭如何對其進行使用:

rle_encoder = RunLengthEncode()

rle_text = rle_encoder(text)

rle_decoder = RunLengthDecode()

original_text = rle_decoder(rle_text)

執行長度編碼器將字串轉換為UTF-8編碼的位元組,並使用序列0x00, 0x01, 0x00 替換0x00,使用序列0x00, count, byte替換包含3到255個重複位元組的任意序列。如果該字串包含大量4個或多個相同的連續字元,則這種編碼會產生比原始的UTF-8 編碼更短的位元組字串。執行長度解碼器接受執行長度編碼器編碼所得的位元組字串, 並返回原始的字串。下面給出的是RunLengthDecode類的起點:

class RunLengthDecode(TextFilter):

@property

def is_transformer(self):

return True

def __call__(self, rle_bytes):

。。。

我們忽略了__call__()方法的主體,在本書的原始碼中可以找到。RunLengthEncode類的結構是完全一樣的。

我們將檢視的最後一個ABC提供了應用程式設計介面(API)以及撤銷機制的預設實現,下面給出的是完整的ABC (取自Abstractly檔案):

class Undo(metaclass=abc。ABCMeta):

©abc。abstractmethod

def __init__(self):

self。__undos =[]

@abc。abstractproperty

def can_undo(self):

return bool(self。__undos)

@abc。abstractmethod

def undo(self):

assert self。__undos, ”nothing left to undo“

self。__undos。pop()(self)

def add_undo(self, undo):

self。__undos。append(undo)

__init__()方法與undo()方法必須重新實現,因為兩者都是抽象的,只讀的can_undo 特性也是如此。子類不必重新實現add_undo()方法,儘管允許這樣做。undo()方法稍有些微妙。self。__undos列表應該存放對方法的物件引用,每個方法被呼叫後都必須使相應操作被撤銷——稍後我們看一個Undo子類時會更清晰地理解。因此,為執行撤銷操作,我們從self。__undos列表中彈出最後一個撤銷方法,之後將該方法作為函式進行呼叫,並以self作為一個引數(我們必須傳遞self,因為該方法是作為函式被呼叫的,而非作為方法被呼叫)。

#下面給出Stack類的起始處,該類繼承自Undo,因此,很多施加於其上的操作可以透過呼叫Stack。undo()(沒有引數)來撤銷。

class Stack(Undo):

def __init__(self):

super()。__init__()

self。__stack = []

@property

def can_undo(self):

return super()。can_undo

def undo(self):

super()。undo()

def push(self, item):

self。__stack。append(item)

self。add_undo(lambda self: self。__stack。pop())

def pop(self):

item = self。__stack。pop()

self。add_undo(lambda self: self。__stack,append(item))

return item

我們忽略了 Stack。top()方法與Stack。__str__()方法,因為兩者都沒有什麼新內容, 也都不與Undo基類進行互動。對can_undo特性與undo()方法,我們簡單地將相關工作傳遞給基類。如果這兩者不是抽象的,我們就不需要對其進行重新實現,並可以達到同樣的效果,但在這裡,我們強制子類對其進行重新實現,以使撤銷操作在子類內進行。對push()方法與pop()方法,我們執行相應的操作,並向撤銷列表中新增相應函式,函式的功能就是撤銷剛執行的操作。

在大規模程式、庫以及應用程式框架中,抽象基類的作用最明顯,有助於確保不管實現細節或作者有哪些差別,類都可以協同工作,因為其提供的API都是由其ABC 指定的。

多繼承(Multiple Inheritance)

多繼承是指某個類繼承自兩個或多個類。Python (以及C++等語言)完全支援多繼承,有些語言(比如Java)則不支援這種機制。多繼承存在的問題是,可能導致同 一個類被繼承多次(比如,基類中的某兩個繼承自同一個類)。這意味著,某個被呼叫的方法如果不在子類中,而是在兩個或多個基類中(或基類的基類中),那麼被呼叫方法的具體版本取決於方法的解析順序,從而使得使用多繼承得到的類存在模糊的可能。

透過使用單繼承(一個基類),並設定一個元類(如果需要支援附加的API),可以避免使用多繼承,在下一小節中我們將會看到,元類可用於提供關於要提供的API 的許諾,但實際上沒有真正繼承任何方法與資料屬性。還有一種替代方案是使用多繼承與一個具體的類,以及一個或多個抽象基類(用於提供附加的API)。另一種替代方案是使用單繼承並對其他類的例項進行聚集

儘管如此,有些情況下,使用多繼承仍然可以提供非常方便的解決方案。比如, 假定需要建立新版本的Stack類(上一小節中定義),但希望該類可以支援使用pickle 的載入與儲存操作。我們可能需要向幾個類中新增載入與儲存功能,因此,我們將在自己的類中實現:

class LoadSave:

def __init__(self, filename, *attribute_names):

self。filename = filename

self。__attribute_names =[]

for name in attribute_names:

if name。startswith(”__“):

name = ”_“+self。__class__。__name__+self。__attribute_names。append(name)

def save(self):

with open(self。filename, “wb”) as fh:

data =[]

for name in self。__attribute_names:

data。append(getattr(self, name))

pickle。dump(data, fh, pickle。HIGHEST_PROTOCOL)

def load(self):

with open(self。filename, “rb”) as fh:

data = pickle。load(fh)

for name, value in zip(self。__attribute_names, data):

setattr(self, name, value)

該類有兩個屬性:filename,是一個公開屬性,可以在任何時候進行修改;__attribute_names,固定的,只能在例項建立時進行設定。save()方法首先對所有屬性名進行迭代,並建立一個名為data的列表,其中存放每個待儲存的屬性的值,之後將資料儲存到pickle中。with語句可以保證正確開啟的檔案得以關閉,並將任何檔案或 pickle異常傳遞給呼叫者。load()方法對所有屬性名以及被載入的相應資料項進行迭代, 並將每個屬性值設定為載入的值。

下面給出FileStack類的起點,該類繼承了上一小節的Undo類以及本小節的 LoadSave 類:

class FileStack(Undo, LoadSave):

def __init__(self, filename):

Undo。__init__(self)

LoadSave。__init__(self, filename, “_stack”)

self。__stack =[]

def load(self):

super()。load()

self。clear()

def clear(self): # In class Undo

self。__undos = []

該類的其餘部分與Stack類一樣,因此這裡不再贅述。此外,這裡沒有在__init__() 方法使用super(),而是必須指定我們要進行初始化的基類,因為super()並不能推斷我們的意圖。為對LoadSave進行初始化,我們將要使用的檔名以及需要儲存的屬性名作為引數,這裡僅有一個,即私有的__stack (我們不需要儲存__undos,這裡也無法儲存,因為 __undos是一個方法列表,因此是unpicklable)。

FileStack類包含所有撤銷方法,也包含LoadSave類的save()與load()方法。我們 沒有對save()進行重新實現,因為該方法可以正常工作,但對於load()方法,我們必須在載入後清空撤銷棧,這樣做是必要的,因為我們可以先進行儲存,之後進行多種改變,再之後進行載入。載入操作會擦除以前所做的操作,因此任何撤銷操作都不再有意義。原始的Undo類不包含clear()方法,因此我們必須新增一個:

在Stack。load()方法中,我們使用super()來呼叫LoadSave。load(),因為沒有 Undo。load()方法會導致二義性。如果兩個基類都有load()方法,那麼具體被呼叫的方法依賴於Python的方法解析順序。在不至於導致二義性的情況下,我們只使用super(), 否則就使用適當的基類名,因此我們一直不會依賴方法解析順序。對self。clear()呼叫, 也不存在二義性,因為只有Undo類有一個clear()方法,我們也不需要使用super(),因為(與load()不同)FileStack不包括clear()方法。

如果後來向FileStack中新增clear()方法會有哪些影響?影響就是將破壞load()方法,一種解決方案是在load()內部呼叫super()。clear(),而非self。clear(),這將使第一個super類的clear()方法被使用。為避免出現這一問題,我們可以制定一種策略,要求在多繼承時使用硬編碼的基類(在這一例項中,呼叫Undo。clear(self))。或者,我們可以避免使用多繼承,並使用聚集,比如,繼承Undo類,並建立一個用於聚集的LoadSave類。

這裡,多繼承給予我們的是兩個相當不同的類的混合,而不需要自己實現撤銷、 載入與儲存等方法,因為基類提供了這些功能。這是非常便利的,在繼承得來的類沒有交疊的API時尤其有效。

元類(Metaclasses)

元類之於類,就像類之於例項。也就是說,元類用於建立類,正如類用於建立例項一樣。並且,正如我們可以使用isinstance()來判斷某個例項是否屬於某個類。我們 也可以使用issubclass()來判斷某個類物件(比如dict、int或SortedList)是否繼承了其他類。

元類最簡單的用途是使自定義類適合Python標準的ABC體系,比如,為使得 SortedList是一個collections。Sequence,可以不繼承ABC (如前面所展示的),而只是簡單地將 SortedList 註冊為一個 collections。Sequence:

class SortedList:

。。。

collections。Sequence。register(SortedList)

在像通常一樣對類進行定義後,我們將其註冊到collections。Sequence ABC。以這種方式對類進行註冊會使其成為一個虛擬子類。註冊之後,虛擬子類會報告其自身為註冊類(或多個註冊類)的子類(比如,使用isinstance()或issubclass()),但並不會從其註冊到的任何類中繼承資料或方法。

以這種方式註冊一個類會提供一個許諾,即該類會提供其註冊類的API,但並不能保證一定遵守這個許諾。元類的用途之一就是同時提供這種許諾與保證,另一個用途是以某種方式修改一個類(就像類修飾器所做的),當然,元類也可同時用於這兩個目的。

假定我們需要建立一組類,都提供load()方法與save()方法。為此,我們可以建立 一個類,該類用作元類時,可檢測這些方法是否存在:

class LoadableSaveable(type):

def __init__(cls, classname, bases, dictionary):

super()。__init__(classname, bases, dictionary)

assert hasattr(cls, “load”) and isinstance(getattr(cls, “load”),collections。Callable), (“class ” +classname + “ must provide a load() method”)

assert hasattr(cls, “save”) and isinstance(getattr(cls, “save”),collections。Callable), (“class” + classname +“ must provide a save() method”)

如果某個類需要充當元類,就必須繼承自根本的元類基類type一或其某個子類。

注意,只有在使用該類的類被初始化時,才會呼叫該類,很可能這並不常見,因 此執行時開銷極低。還要注意,在類被建立後(使用super()呼叫),我們必須對其進行檢測,因為只有在這之後,類的屬性在類自身中才是可用的(屬性在字典中,但在進行檢測時,我們更願意對實際的初始化之後的類進行操作。)

我們可以透過使用hasattr()檢測出其具有__call__屬性,並據此判斷load屬性與 save屬性是可呼叫的,但我們更願意透過檢測其是否是collections。Callable的例項來進行判斷,抽象基類collections。Callable提供了許諾(但並不保證)——其子類(或虛擬子類)的例項是可呼叫的。

在類被建立後(使用type。__new__(),或重新實現的__new__()),元類的初始化是透過呼叫其__init__()方法實現的。賦予__init__()方法的引數包括cls,剛剛建立的類; classname,類的名稱(也可以從cls。__name__獲取);bases,該類的基類列表(object除外,並可以為空);dictionary,存放屬性,在cls被建立時成為類屬性除非我們在重新實現元類的__new__()方法時進行干預)。

這裡有兩個互動式例項,展示了在使用元類LoadableSaveable建立類時的情況:

>>> class Bad(metaclass=Meta。LoadableSaveable):

。。。 def some_method(self): pass

Traceback (most recent call last):

。。。

AssertionError: class ‘Bad’ must provide a load() method

元類規定,使用該元類的類必須提供某些方法,如果不能提供,比如這裡,就會產生 AssertionError 異常:

>>> class Good(metaclass=Meta。LoadableSaveable):

。。。 def load(self): pass

。。。 def save(self): pass

>>> g = Good()

Good類遵守元類的API需求(即使不滿足我們對該類行為的一些非正式的期待)。 我們也可以使用元類來改變使用該元類的類,如果改變涉及被建立的類的名稱、 基類或字典(比如,其slots),我們就需要重新實現元類的__new__()方法,但對於其他改變,比如新增方法或資料屬性,重新實現__init__()就已足夠,儘管這也可以在 __new__()中實現。我們將檢視一個元類修改使用它的類的例項,純粹透過__new__() 方法實現。

作為對使用@property與@name。setter修飾器的一種替代,我們將建立相應類,並使用簡單的命名約定來標識特性。比如,某個類有形如get_name()與set_name()的方法, 我們就可以期待該類有一個私有的__name特性,可以使用instance。name進行存取,以便獲取並進行設定,這些都可以使用元類實現。下面給出一個使用這種約定的例項類:

class Product(metaclass=AutoSlotProperties):

def __init__(self, barcode, description):

self。__barcode = barcode

self。description = description

def get_barcode(self):

return self。__barcode

def get_description(self):

return self。__description

def set_description(self, description):

if description is None or len(description) < 3:

self。__description = “

else:

self。__description = description

我們必須在初始化程式中對私有的__barcode特性賦值,因為沒有用於它的setter, 這種做法的另一個後果是使barcode為一個只讀特性,description則為可讀/可寫的特性。下面給出幾個互動式使用的例項:

>>> product = Product(”101110110“, ”8mm Stapler“)

>>> product。barcode, product。description

(‘101110110’, ‘8mm Stapler’)

>>> product。description = ”8mm Stapler (long)“

>>> product。barcode, product。description

(‘101110110’, ‘8mm Stapler (long)’)

如果我們嘗試對條形碼進行賦值,就會產生AttributeError異常,並展示錯誤文字 “can‘t set attribute ”。

如果我們査看Product類的屬性(比如使用dir()),就會發現公開屬性只有barcode 與description, get_name()方法與set_name()方法不復存在已經被name特性替代。存放條形碼與描述資訊的變數也變為私有(__barcode與__description),並被新增為 slots,以便最小化類的記憶體使用。所有這些操作都是使用元類AutoSlotProperties實現的,該元類只包含一個單獨的方法:

class AutoSlotProperties(type):

def __new__(mcl, classname, bases, dictionary):

slots = list(dictionary。get(“__slots__”, []))

for getter_name in [key for key in dictionary if key。startswith(“get_”)]:

if isinstance(dictionary[getter_name], collections。Callable):

name = getter_name[4:]

slots。append(“_” + name)

getter = dictionary。pop(getter_name)

setter_name = “set_” + name

setter = dictionary。get(setter_name, None)

if (setter is not None and isinstance(setter, collections。Callable)):

del dictionary[setter_name]

dictionary[name] = property(getter, setter)

dictionary[“__slots__”] = tuple(slots)

return super()。__new__(mcl, classname, bases, dictionary)

呼叫元類的__new__()方法時,要使用元類以及待建立類的類名、基類、字典作為引數。我們必須使用重新實現後的__new__(),而非__init__(),因為我們需要在類建立前改變字典。

我們從複製組合型別__slots__開始,如果不存在就建立一個,並確保是一個列表而非元組,以便可以對其進行修改。對字典中的每個屬性,我們挑選出那些名稱以 “get_“開始並且是可呼叫的,也就是說那些getter方法。對每個getter,我們向slots 中新增一個私有名稱以便儲存相應的資料,比如,給定getter get_name(),我們就向slots 中新增__name。之後,設定對getter的引用,並在字典中其原始名下將其刪除(這可以使用dict。pop()一次完成)。對setter (如果存在)進行同樣的處理,之後建立一個新字典項,並以需要的特性名作為其鍵。比如,getter是get_name(),則特性名為name。 我們將項的值設定為特性,並將getter與setter (可以是None)從字典中刪除。

最後,我們使用修改後的slots列表(對每個新增的特性,有一個私有的slot)來替換原始的slots,並呼叫基類實際完成建立類的工作(但使用的是我們修改後的字典)。 注意,這裡我們必須顯式地在super()呼叫中傳遞基類,對__new__()的呼叫總是這種格式,因為這是一個類方法而非一個例項方法。

對這一例項,我們不需要編寫一個__init__()方法,因為所有工作都已在__new__() 中完成,但同時重新實現__new__與__init__()方法並分別完成不同工作則是完全可能的。

以上內容部分摘自影片課程05後端程式設計Python17高階程式設計(面向物件-下),更多實操示例請參照影片講解。跟著張員外講程式設計,學習更輕鬆,不花錢還能學習真本領。

後端程式設計Python3-高階程式設計(面向物件-下)

相關文章