在 Python 開發中,物件轉換成字典或 JSON 格式的需求相當常見,例如資料函式庫儲存、API 傳輸等。傳統的逐類別編寫轉換方法既繁瑣又容易出錯。本文介紹 Python Mixin 技術,它能將通用轉換邏輯抽離成可重複使用的模組,大幅簡化程式碼,提升開發效率。Mixin 類別如同外掛,能為其他類別增加特定功能,無需複雜的繼承關係。文章以 ToDictMixin 為例,示範如何將物件遞迴轉換為字典,並特別處理列表、字典等資料結構。接著,以二元樹物件為例,展示如何混入 ToDictMixin,實作簡潔的物件轉換。更進一步,文章探討了客製化 Mixin 行為,例如在處理包含父節點參照的二元樹時,如何避免迴圈參照。最後,文章介紹了 Mixin 的組合應用,例如結合 JsonMixin 實作物件的 JSON 序列化和反序列化,並以資料中心機架的例子展示如何建構複雜的資料結構,並透過 Mixin 簡化資料處理流程。
擺脫大量 Python 物件轉換地獄:Mixin 妙用
在 Python 開發中,經常會遇到需要將大量相關物件轉換為字典 (Dictionary) 的情況,例如將物件序列化為 JSON 格式,或是方便資料函式庫儲存。傳統做法可能需要針對每個類別編寫重複的轉換程式碼,不僅繁瑣,也容易出錯。
身為一個在台灣打滾多年的老鳥工程師,玄貓今天要分享一個更優雅的解決方案:Mixin。透過 Mixin,我們可以將通用的字典轉換邏輯抽離出來,讓所有需要此功能的類別都能輕鬆「混入」這個功能,避免程式碼重複,並提高程式碼的可維護性。
Mixin 基礎:開發你的第一個字典轉換器
首先,我們定義一個名為 ToDictMixin 的 Mixin 類別,它提供了一個 to_dict 方法,用於將物件轉換為字典。
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
程式碼解密
to_dict(self): 這個方法是 Mixin 的主要入口,它會呼叫_traverse_dict方法來處理物件的__dict__屬性,也就是物件的例項變數。_traverse_dict(self, instance_dict): 這個方法會迭代物件的所有例項變數,並針對每個變數呼叫_traverse方法進行處理。_traverse(self, key, value): 這個方法是核心的遞迴處理函式,它會根據變數的型別進行不同的處理:- 如果變數是
ToDictMixin的例項,則遞迴呼叫其to_dict方法。 - 如果變數是字典,則遞迴呼叫
_traverse_dict方法。 - 如果變數是列表,則迭代列表中的每個元素,並遞迴呼叫
_traverse方法。 - 如果變數有
__dict__屬性,表示它是一個物件,則遞迴呼叫_traverse_dict方法處理該物件的例項變數。 - 否則,直接回傳變數的值。
- 如果變數是
實戰演練:將二元樹轉換為字典
現在,我們定義一個 BinaryTree 類別,並將 ToDictMixin 混入其中:
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
程式碼解密
BinaryTree類別繼承了ToDictMixin,因此它自動獲得了to_dict方法。
現在,我們可以建立一個 BinaryTree 例項,並呼叫 to_dict 方法將其轉換為字典:
tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
輸出結果如下:
{'left': {'left': None,
'right': {'left': None, 'right': None, 'value': 9},
'value': 7},
'right': {'left': {'left': None, 'right': None, 'value': 11},
'right': None,
'value': 13},
'value': 10}
Mixin 的進階應用:客製化轉換行為
Mixin 最棒的地方在於,你可以根據需要客製化其行為。例如,如果我們的類別包含迴圈參照,預設的 to_dict 實作可能會導致無限迴圈。
以下範例,我們定義一個 BinaryTreeWithParent 類別,它持有對父節點的參照。
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None,
right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent
為了避免迴圈參照,我們可以覆寫 ToDictMixin._traverse 方法,只處理我們關心的屬性,例如忽略 parent 屬性:
def _traverse(self, key, value):
if (isinstance(value, BinaryTreeWithParent) and
key == 'parent'):
return value.value # Prevent cycles
else:
return super()._traverse(key, value)
程式碼解密
- 我們覆寫了
_traverse方法,當遇到parent屬性時,直接回傳父節點的值,而不是遞迴呼叫其to_dict方法,從而避免了迴圈參照。
現在,我們可以建立一個 BinaryTreeWithParent 例項,並呼叫 to_dict 方法,而不會導致無限迴圈:
root = BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())
輸出結果如下:
{'left': {'left': None,
'parent': 10,
'right': {'left': None,
'parent': 7,
'right': None,
'value': 9},
'value': 7},
'parent': None,
'right': None,
'value': 10}
Mixin 的組合:開發更強大的功能
Mixin 也可以組合使用,以建立更複雜的功能。例如,我們可以建立一個 JsonMixin,它提供通用的 JSON 序列化和反序列化功能:
import json
class JsonMixin(object):
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)
def to_json(self):
return json.dumps(self.to_dict())
程式碼解密
from_json(cls, data): 這個類別方法用於從 JSON 字串反序列化物件。它首先使用json.loads方法將 JSON 字串轉換為字典,然後使用字典中的鍵值對作為關鍵字引數呼叫類別的__init__方法,建立物件例項。to_json(self): 這個方法用於將物件序列化為 JSON 字串。它首先呼叫to_dict方法將物件轉換為字典,然後使用json.dumps方法將字典轉換為 JSON 字串。
現在,我們可以將 ToDictMixin 和 JsonMixin 組合使用,建立可以輕鬆序列化和反序列化為 JSON 的類別:
class DatacenterRack(ToDictMixin, JsonMixin):
def __init__(self, switch=None, machines=None):
self.switch = Switch(**switch)
self.machines = [
Machine(**kwargs) for kwargs in machines]
class Switch(ToDictMixin, JsonMixin):
def __init__(self, ports, speed):
self.ports = ports
self.speed = speed
class Machine(ToDictMixin, JsonMixin):
def __init__(self, cores, ram, disk):
self.cores = cores
self.ram = ram
self.disk = disk
程式碼解密
DatacenterRack,Switch,Machine類別都繼承了ToDictMixin和JsonMixin,因此它們都自動獲得了to_dict,to_json和from_json方法。DatacenterRack的__init__方法使用關鍵字引數建立Switch和Machine例項。
以下範例,我們驗證資料可以透過序列化和反序列化進行往返:
serialized = """{
"switch": {"ports": 5, "speed": 1e9},
"machines": [
{"cores": 8, "ram": 32e9, "disk": 5e12},
{"cores": 4, "ram": 16e9, "disk": 1e12},
{"cores": 2, "ram": 4e9, "disk": 500e9}
]
}"""
deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)
玄貓的 Mixin 心法:避免重複,擁抱彈性
Mixin 是一種強大的工具,可以幫助我們避免程式碼重複,提高程式碼的可維護性,並建立更靈活的類別。
玄貓建議:
- 如果 Mixin 類別可以達到相同的效果,請避免使用多重繼承。
- 在例項層級使用可插拔的行為,以便在 Mixin 類別可能需要時提供每個類別的自定義。
- 組合 Mixin 以從簡單的行為建立複雜的功能。
Python 物件屬性:公開還是私有?
在 Python 中,類別屬性的可見性只有兩種:公開 (Public) 和私有 (Private)。
class MyObject(object):
def __init__(self):
self.public_field = 5
self.__private_field = 10
def get_private_field(self):
return self.__private_field
程式碼解密
public_field是一個公開屬性,可以在類別外部直接存取。__private_field是一個私有屬性,只能在類別內部存取。
玄貓認為,理解 Python 的屬性可見性規則對於編寫可維護和可擴充套件的程式碼至關重要。
為何Python的私有變數並非真的私有?玄貓的解讀
在Python中,我們經常聽到「私有變數」這個概念,但實際上,Python的私有變數並不像其他程式語言(如Java或C++)那樣具有嚴格的存取限制。今天,玄貓就來探討這個有趣的現象。
Python的私有變數:雙底線的秘密
在Python中,如果你想將一個變數宣告為私有,通常會在變數名稱前加上雙底線(__),例如__private_field。這樣的變數在外部直接存取會引發AttributeError。
class MyObject(object):
def __init__(self):
self.public_field = 5
self.__private_field = 10
def get_private_field(self):
return self.__private_field
foo = MyObject()
assert foo.public_field == 5
assert foo.get_private_field() == 10
# 這會引發錯誤
# foo.__private_field
但這裡有個玄機:Python的私有變數並非真的無法存取。
命名修飾(Name Mangling):障眼法
Python的私有變數實際上是透過一種稱為「命名修飾」(name mangling)的機制來實作的。當Python編譯器看到以雙底線開頭的變數時,會將其名稱修改為_ClassName__variable的形式。
class MyParentObject(object):
def __init__(self):
self.__private_field = 71
class MyChildObject(MyParentObject):
def get_private_field(self):
# 這會引發錯誤,因為Python會尋找 _MyChildObject__private_field
# return self.__private_field
# 正確的方式是存取 _MyParentObject__private_field
return self._MyParentObject__private_field
baz = MyChildObject()
assert baz._MyParentObject__private_field == 71
print(baz.__dict__)
# 輸出:{'_MyParentObject__private_field': 71}
這表示,你仍然可以透過_MyParentObject__private_field來存取私有變數,只是Python不鼓勵你這麼做。
「我們都是 consenting adults」:Python哲學
Python社群秉持著「我們都是 consenting adults」的哲學。這意味著,Python假定開發者都是負責任的,不會故意去破壞或濫用私有變數。
這種設計哲學有幾個好處:
- 靈活性:Python的靈活性讓你可以自由地修改和擴充套件現有程式碼,而不會受到過多的限制。
- 除錯:在除錯時,你可以輕鬆地存取和修改私有變數,而不需要繞過重重障礙。
- 元程式設計:Python的元程式設計能力(例如使用
__getattr__、__getattribute__和__setattr__)讓你可以在執行時動態地修改物件的行為。如果Python的私有變數是嚴格私有的,這些功能就無法實作。
單底線:受保護的變數
除了雙底線,Python還有另一種約定:單底線(_protected_field)。這表示該變數是受保護的,不應該在外部直接存取,但Python不會強制執行這個規則。
class MyClass(object):
def __init__(self, value):
self._value = value # 受保護的變數
def get_value(self):
return str(self._value)
何時使用私有變數?
雖然Python的私有變數並非真的私有,但在某些情況下,仍然建議使用:
- 避免命名衝突:當子類別可能會意外地定義與父類別相同的變數名稱時,可以使用私有變數來避免命名衝突。
- 提示開發者:使用雙底線可以向其他開發者(包括未來的自己)發出訊號,表示該變數是內部使用的,不應該在外部直接存取。
玄貓的建議
玄貓建議,在大多數情況下,使用受保護的變數(單底線)就足夠了。這樣可以讓子類別更容易擴充套件和修改你的程式碼,同時也能向其他開發者傳達你的意圖。只有在需要避免命名衝突或強烈暗示變數是內部使用時,才考慮使用私有變數(雙底線)。
總之,Python的私有變數是一種約定,而不是一種強制性的限制。理解這種設計哲學可以幫助你更好地使用Python,並編寫出更具彈性和可維護性的程式碼。