在 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

程式碼解密

  1. to_dict(self): 這個方法是 Mixin 的主要入口,它會呼叫 _traverse_dict 方法來處理物件的 __dict__ 屬性,也就是物件的例項變數。
  2. _traverse_dict(self, instance_dict): 這個方法會迭代物件的所有例項變數,並針對每個變數呼叫 _traverse 方法進行處理。
  3. _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())

程式碼解密

  1. from_json(cls, data): 這個類別方法用於從 JSON 字串反序列化物件。它首先使用 json.loads 方法將 JSON 字串轉換為字典,然後使用字典中的鍵值對作為關鍵字引數呼叫類別的 __init__ 方法,建立物件例項。
  2. to_json(self): 這個方法用於將物件序列化為 JSON 字串。它首先呼叫 to_dict 方法將物件轉換為字典,然後使用 json.dumps 方法將字典轉換為 JSON 字串。

現在,我們可以將 ToDictMixinJsonMixin 組合使用,建立可以輕鬆序列化和反序列化為 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 類別都繼承了 ToDictMixinJsonMixin,因此它們都自動獲得了 to_dict, to_jsonfrom_json 方法。
  • DatacenterRack__init__ 方法使用關鍵字引數建立 SwitchMachine 例項。

以下範例,我們驗證資料可以透過序列化和反序列化進行往返:

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假定開發者都是負責任的,不會故意去破壞或濫用私有變數。

這種設計哲學有幾個好處:

  1. 靈活性:Python的靈活性讓你可以自由地修改和擴充套件現有程式碼,而不會受到過多的限制。
  2. 除錯:在除錯時,你可以輕鬆地存取和修改私有變數,而不需要繞過重重障礙。
  3. 元程式設計: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的私有變數並非真的私有,但在某些情況下,仍然建議使用:

  1. 避免命名衝突:當子類別可能會意外地定義與父類別相同的變數名稱時,可以使用私有變數來避免命名衝突。
  2. 提示開發者:使用雙底線可以向其他開發者(包括未來的自己)發出訊號,表示該變數是內部使用的,不應該在外部直接存取。

玄貓的建議

玄貓建議,在大多數情況下,使用受保護的變數(單底線)就足夠了。這樣可以讓子類別更容易擴充套件和修改你的程式碼,同時也能向其他開發者傳達你的意圖。只有在需要避免命名衝突或強烈暗示變數是內部使用時,才考慮使用私有變數(雙底線)。

總之,Python的私有變數是一種約定,而不是一種強制性的限制。理解這種設計哲學可以幫助你更好地使用Python,並編寫出更具彈性和可維護性的程式碼。