Python 提供了多種物件建構方式,除了常見的 __init__ 建構子外,類別方法也提供了更彈性的物件建構途徑。當需要根據不同資料來源或條件建立不同型別的物件時,類別方法就能派上用場。例如,可以定義一個 InputData 類別,並使用類別方法 from_file 和 from_database 分別從檔案和資料函式庫讀取資料,再建立 InputData 的例項。這種方式解耦了物件建構邏輯,使程式碼更清晰易懂。在分散式系統開發中,類別方法結合類別多型,可以讓 MapReduce 框架更具彈性。例如,定義抽象基底類別 GenericInputData 和 GenericWorker,再實作具體子類別 PathInputData 和 LineCountWorker,就能夠處理不同型別的輸入資料和資料處理任務。最後,定義一個泛型的 mapreduce 函式,接受 worker 類別、輸入類別和設定檔作為引數,即可實作泛型 MapReduce 功能。除了物件建構,繼承也是物件導向程式設計的重要機制。然而,多重繼承可能導致複雜的繼承結構和難以預測的行為。super() 函式可以解決多重繼承中父類別初始化順序和重複呼叫的問題。它會根據方法解析順序(MRO)自動處理呼叫順序,確保共同的父類別只被執行一次。儘管 super() 解決了多重繼承的部分問題,但更推薦使用 Mix-in 類別來實作程式碼的重用和模組化。Mix-in 類別只定義一組附加方法,不定義自己的例項屬性,也不要求呼叫 __init__ 建構子,可以更靈活地將功能注入到不同的類別中,例如將 Python 物件轉換為可序列化的字典。
運用類別方法實作彈性物件建構:玄貓的工廠模式進化之路
Python 的物件導向特性,讓開發者能透過類別來定義和建立物件。一般來說,我們會使用 __init__ 方法作為物件的建構子。但有時,我們需要更彈性的方式來建立物件,例如根據不同的輸入資料來源,建立不同型別的物件。這時,類別方法(classmethod)就能派上用場。
傳統建構子的侷限
傳統的 __init__ 建構子,在建立物件時,通常需要直接傳入所有必要的引數。但如果物件的建立過程比較複雜,例如需要從檔案讀取資料,或是從網路取得資料,直接在 __init__ 中處理這些邏輯,會讓程式碼變得臃腫與難以維護。
類別方法:另一種物件建構途徑
類別方法使用 @classmethod 裝飾器定義,第一個引數 cls 代表類別本身。透過類別方法,我們可以在不直接呼叫 __init__ 的情況下,建立類別的例項。這為物件建構提供了更大的彈性。
範例:從不同資料來源建立物件
假設我們有一個 InputData 類別,負責讀取輸入資料。我們希望能夠從檔案或從資料函式庫讀取資料。
class InputData(object):
def __init__(self, data):
self.data = data
@classmethod
def from_file(cls, path):
"""從檔案建立 InputData 例項"""
with open(path, 'r') as f:
data = f.read()
return cls(data) # 呼叫 cls(...),也就是 InputData(...)
@classmethod
def from_database(cls, connection_string, query):
"""從資料函式庫建立 InputData 例項"""
# 這裡省略資料函式庫連線和查詢的程式碼
data = "從資料函式庫取得的資料"
return cls(data)
# 使用範例
file_input = InputData.from_file('my_file.txt')
database_input = InputData.from_database('...', '...')
在這個例子中,from_file 和 from_database 都是類別方法,它們負責從不同的資料來源讀取資料,並建立 InputData 的例項。
類別方法的多型應用:玄貓的 MapReduce 實戰
讓玄貓分享一個在分散式系統開發中的實際案例。在設計 MapReduce 框架時,我們需要處理不同型別的輸入資料,例如檔案、資料函式庫,甚至是網路串流。同時,我們也希望 MapReduce 的 worker 可以處理不同型別的資料處理任務,例如計算行數、統計單字出現次數等等。
為了實作這個目標,玄貓使用了類別方法和類別多型(class polymorphism)的概念。
1. 定義抽象基底類別
首先,我們定義兩個抽象基底類別:GenericInputData 和 GenericWorker。
import os
from abc import ABCMeta, abstractmethod
class GenericInputData(object):
"""
代表輸入資料的抽象類別。
"""
__metaclass__ = ABCMeta
@abstractmethod
def read(self):
"""
讀取資料,由子類別實作。
"""
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
"""
產生輸入資料物件,由子類別實作。
"""
raise NotImplementedError
class GenericWorker(object):
"""
代表 worker 的抽象類別。
"""
__metaclass__ = ABCMeta
def __init__(self, input_data):
self.input_data = input_data
@abstractmethod
def map(self):
"""
Map 階段,由子類別實作。
"""
raise NotImplementedError
@abstractmethod
def reduce(self, other):
"""
Reduce 階段,由子類別實作。
"""
raise NotImplementedError
@classmethod
def create_workers(cls, input_class, config):
"""
建立 worker 例項。
"""
workers = []
for input_data in input_class.generate_inputs(config):
workers.append(cls(input_data))
return workers
GenericInputData 定義了讀取資料的介面 read(),以及產生輸入資料物件的類別方法 generate_inputs()。GenericWorker 定義了 map 和 reduce 階段的介面,以及建立 worker 例項的類別方法 create_workers()。
2. 實作具體子類別
接著,我們實作具體的子類別,例如 PathInputData 和 LineCountWorker。
class PathInputData(GenericInputData):
"""
從檔案讀取資料。
"""
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
"""
讀取檔案內容。
"""
return open(self.path).read()
@classmethod
def generate_inputs(cls, config):
"""
產生檔案路徑的輸入資料物件。
"""
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))
class LineCountWorker(GenericWorker):
"""
計算行數的 worker。
"""
def map(self):
"""
計算輸入資料的行數。
"""
data = self.input_data.read()
self.result = len(data.splitlines())
def reduce(self, other):
"""
合併其他 worker 的結果。
"""
self.result += other.result
PathInputData 負責從檔案讀取資料,LineCountWorker 負責計算行數。
3. 泛型 MapReduce 函式
最後,我們定義一個泛型的 mapreduce 函式,它接受 worker 類別、輸入類別和設定檔作為引數。
def mapreduce(worker_class, input_class, config):
"""
泛型的 MapReduce 函式。
"""
workers = worker_class.create_workers(input_class, config)
# 這裡省略 execute 函式的實作,它負責執行 map 和 reduce 階段
return execute(workers)
這個 mapreduce 函式使用了 worker_class.create_workers() 這個類別方法,來建立 worker 例項。這就是類別多型的應用:我們可以傳入不同的 worker 類別和輸入類別,mapreduce 函式都能夠正確地建立物件並執行 MapReduce 任務。
4. 實際執行
import tempfile
def write_test_files(tmpdir):
"""
建立測試檔案。
"""
with open(os.path.join(tmpdir, 'file1.txt'), 'w') as f:
f.write("This is file1.\nIt has two lines.")
with open(os.path.join(tmpdir, 'file2.txt'), 'w') as f:
f.write("This is file2.\nIt has three lines.\n")
with tempfile.TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
config = {'data_dir': tmpdir}
result = mapreduce(LineCountWorker, PathInputData, config)
print(f"Total lines: {result}")
類別方法的優勢
透過類別方法,我們可以:
- 彈性地建立物件: 根據不同的資料來源或條件,建立不同型別的物件。
- 解耦物件建構邏輯: 將物件建構的邏輯從
__init__中分離出來,讓程式碼更清晰易懂。 - 實作類別多型: 讓程式碼更具彈性和可擴充性。
玄貓的建議
- 當物件的建立過程比較複雜時,考慮使用類別方法。
- 善用類別方法和類別多型,讓你的程式碼更具彈性和可擴充性。
使用 super() 初始化父類別:告別多重繼承的混亂
在物件導向程式設計中,繼承是一種重要的機制,它允許我們建立根據現有類別的新類別,並繼承其屬性和方法。當子類別需要擴充或修改父類別的行為時,我們通常會在子類別中覆寫(override)父類別的方法。
但當涉及到多重繼承時,直接呼叫父類別的 __init__ 方法可能會導致一些問題,例如呼叫順序不明確、重複呼叫等。這時,super() 就能派上用場。
直接呼叫父類別 __init__ 的問題
在過去,我們通常會直接呼叫父類別的 __init__ 方法來初始化父類別的屬性。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
這種方式在單純的繼承關係中可能沒有問題,但在多重繼承中,可能會導致以下問題:
- 呼叫順序不明確: 當子類別繼承多個父類別時,我們需要決定以什麼順序呼叫父類別的
__init__方法。不同的呼叫順序可能會導致不同的結果。 - 重複呼叫: 在菱形繼承(diamond inheritance)中,同一個父類別可能會被多次呼叫
__init__方法,導致不必要的重複初始化。
範例:多重繼承的混亂
class TimesTwo(object):
def __init__(self):
self.value *= 2
class PlusFive(object):
def __init__(self):
self.value += 5
class OneWay(MyBaseClass, TimesTwo, PlusFive):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
foo = OneWay(5)
print('First ordering is (5 * 2) + 5 =', foo.value) # 輸出:15
class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
bar = AnotherWay(5)
print('Second ordering still is', bar.value) # 輸出:15
在這個例子中,OneWay 和 AnotherWay 繼承了相同的父類別,但父類別的定義順序不同。儘管如此,由於我們直接呼叫父類別的 __init__ 方法,與呼叫順序固定,導致兩個類別的行為不一致。
菱形繼承的問題
class TimesFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 5
class PlusTwo(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 2
class ThisWay(TimesFive, PlusTwo):
def __init__(self, value):
TimesFive.__init__(self, value)
PlusTwo.__init__(self, value)
foo = ThisWay(5)
print('Should be (5 * 5) + 2 = 27 but is', foo.value) # 輸出:27
在這個例子中,ThisWay 繼承了 TimesFive 和 PlusTwo,而這兩個類別又都繼承自 MyBaseClass,形成了菱形繼承。由於我們直接呼叫 MyBaseClass.__init__ 兩次,導致 self.value 被初始化了兩次,結果可能不是我們想要的。
super():解決多重繼承的利器
super() 是一個內建函式,它傳回一個代表父類別的代理物件。透過這個代理物件,我們可以呼叫父類別的方法,而不需要直接指定父類別的名稱。
使用 super() 的好處是:
- 自動處理呼叫順序:
super()會自動根據方法解析順序(Method Resolution Order, MRO)來決定父類別的呼叫順序,避免了我們手動指定順序的麻煩。 - 避免重複呼叫: 在菱形繼承中,
super()可以確保同一個父類別只會被初始化一次。
使用 super() 的範例
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class TimesTwo(object):
def __init__(self):
super().__init__()
self.value *= 2
class PlusFive(object):
def __init__(self):
super().__init__()
self.value += 5
class GoodWay(MyBaseClass, TimesTwo, PlusFive):
def __init__(self, value):
super().__init__(value)
foo = GoodWay(5)
print('Should be 5 + (5 * 2) = 15 and is', foo.value)
class Explicit(MyBaseClass):
def __init__(self, value):
super(Explicit, self).__init__(value)
class AnotherGoodWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
super().__init__(value)
bar = AnotherGoodWay(5)
print('Now the value is', bar.value)
在這個例子中,我們使用 super() 來呼叫父類別的 __init__ 方法。super() 會自動根據 MRO 來決定呼叫順序,確保父類別的初始化順序正確。
玄貓的建議
- 在多重繼承中,務必使用
super()來初始化父類別。 - 理解 MRO 的概念,可以幫助你更好地理解
super()的運作方式。 - 避免過度使用多重繼承,盡量保持繼承關係的簡單清晰。
總之,super() 是處理多重繼承的強大工具。透過 super(),我們可以避免多重繼承帶來的混亂,確保程式碼的正確性和可維護性。
希望這個說明對您有所幫助!玄貓隨時準備好為您提供更多技術上的協助。
避免多重繼承陷阱:使用 Mix-in 類別提升程式碼彈性
在物件導向程式設計中,多重繼承提供了一種便捷的方式來擴充套件類別的功能,但同時也可能導致複雜的繼承結構和難以預測的行為。Python 雖然透過方法解析順序(MRO)來處理多重繼承,但更推薦使用 Mix-in 類別來實作程式碼的重用和模組化。
Mix-in 類別是一種只定義一組附加方法的輕量級類別,它不定義自己的例項屬性,也不要求呼叫 __init__ 建構子。Mix-in 的優勢在於其靈活性,可以輕鬆地將其功能注入到不同的類別中,而無需修改原始類別的結構。
多重繼承的問題
在探討 Mix-in 之前,讓玄貓先來看看多重繼承可能產生的問題。以下面的程式碼為例,展示了多重繼承可能導致的混亂:
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class TimesFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 5
class PlusTwo(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 2
class WrongWay(TimesFive, PlusTwo):
def __init__(self, value):
TimesFive.__init__(self, value)
PlusTwo.__init__(self, value)
foo = WrongWay(5)
print(foo.value)
這段程式碼的預期輸出是 27,因為 (5 * 5) + 2 = 27。但實際上,輸出卻是 7。問題在於呼叫第二個父類別的建構子 PlusTwo.__init__ 時,MyBaseClass.__init__ 又被呼叫了一次,導致 self.value 被重設回 5。
為了避免這個問題,Python 引入了 super 函式和方法解析順序(MRO)。MRO 定義了父類別初始化的順序,確保共同的父類別只被執行一次。
使用 super 解決繼承問題
以下程式碼展示瞭如何使用 super 來正確初始化父類別:
class TimesFiveCorrect(MyBaseClass):
def __init__(self, value):
super(TimesFiveCorrect, self).__init__(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def __init__(self, value):
super(PlusTwoCorrect, self).__init__(value)
self.value += 2
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
def __init__(self, value):
super(GoodWay, self).__init__(value)
foo = GoodWay(5)
print(foo.value)
在這個例子中,MyBaseClass.__init__ 只執行一次,其他父類別按照類別定義中的順序執行。MRO 定義了這個順序,你可以透過 mro 方法來檢視:
from pprint import pprint
pprint(GoodWay.mro())
當呼叫 GoodWay(5) 時,它會依次呼叫 TimesFiveCorrect.__init__、PlusTwoCorrect.__init__ 和 MyBaseClass.__init__。一旦到達繼承結構的頂端,初始化方法會按照與呼叫順序相反的順序執行。
Mix-in 的優勢
Mix-in 類別提供了一種更清晰、更靈活的方式來擴充套件類別的功能。它們不依賴於複雜的繼承結構,而是透過動態屬性存取和型別檢查來實作程式碼的重用。
以下是一個 Mix-in 的範例,它將 Python 物件轉換為可序列化的字典:
class ToDictMixin(object):
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value
這個 ToDictMixin 類別提供了一個 to_dict 方法,可以將任何繼承它的類別轉換為字典。_traverse_dict 和 _traverse 方法使用動態屬性存取和型別檢查來處理不同型別的屬性。
應用 Mix-in 類別
以下是一個使用 ToDictMixin 的範例:
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
在這個例子中,BinaryTree 類別繼承了 ToDictMixin,並自動獲得了將自身轉換為字典的功能。