Python 特殊方法 (Magic Methods)

特殊方法(也稱為魔術方法或 dunder methods)是以雙底線開頭和結尾的方法,讓你可以自訂類別的行為。

常用特殊方法

方法用途
__init__建構子,初始化物件
__str__字串表示(給人看)
__repr__官方字串表示(給開發者看)
__len__定義 len() 的行為
__getitem__定義索引存取 obj[key]
__setitem__定義索引設值 obj[key] = value
__iter__定義迭代行為
__call__讓物件可以像函數一樣呼叫

__init__ 建構子

在建立物件時自動呼叫:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("Alice", 25)
print(p.name)  # Alice

__str__ 和 __repr__

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        # 給人看的字串表示
        return f"Point at ({self.x}, {self.y})"
    
    def __repr__(self):
        # 給開發者看的字串表示,通常可以重建物件
        return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(str(p))   # Point at (3, 4)
print(repr(p))  # Point(3, 4)
print(p)        # Point at (3, 4)(print 預設使用 __str__)
如果只定義 repr,它也會用於 str。建議至少定義 repr

比較運算子

方法運算子
__eq__==
__ne__!=
__lt__<
__le__<=
__gt__>
__ge__>=
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        return self.amount == other.amount
    
    def __lt__(self, other):
        return self.amount < other.amount
    
    def __le__(self, other):
        return self.amount <= other.amount

m1 = Money(100)
m2 = Money(200)
m3 = Money(100)

print(m1 == m3)  # True
print(m1 < m2)   # True
print(m2 <= m1)  # False

使用 functools.total_ordering 可以只定義 __eq__ 和一個比較方法,自動產生其他方法:

from functools import total_ordering

@total_ordering
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        return self.amount == other.amount
    
    def __lt__(self, other):
        return self.amount < other.amount

# 現在 <=, >, >= 都可以用了

算術運算子

方法運算子
__add__+
__sub__-
__mul__*
__truediv__/
__floordiv__//
__mod__%
__pow__**
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)  # Vector(4, 6)
print(v2 - v1)  # Vector(2, 2)
print(v1 * 3)   # Vector(3, 6)

__len__ 和 __bool__

class Playlist:
    def __init__(self):
        self.songs = []
    
    def add(self, song):
        self.songs.append(song)
    
    def __len__(self):
        return len(self.songs)
    
    def __bool__(self):
        return len(self.songs) > 0

playlist = Playlist()
print(len(playlist))  # 0
print(bool(playlist)) # False

playlist.add("Song 1")
playlist.add("Song 2")
print(len(playlist))  # 2
print(bool(playlist)) # True

if playlist:
    print("Playlist has songs")

__getitem__ 和 __setitem__

讓物件支援索引存取:

class Matrix:
    def __init__(self, rows, cols):
        self.data = [[0] * cols for _ in range(rows)]
        self.rows = rows
        self.cols = cols
    
    def __getitem__(self, pos):
        row, col = pos
        return self.data[row][col]
    
    def __setitem__(self, pos, value):
        row, col = pos
        self.data[row][col] = value
    
    def __repr__(self):
        return '\n'.join(str(row) for row in self.data)

m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 2
m[2, 2] = 3
print(m[1, 1])  # 2
print(m)

__iter__ 和 __next__

讓物件可以迭代:

class Countdown:
    def __init__(self, start):
        self.start = start
    
    def __iter__(self):
        self.current = self.start
        return self
    
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

for num in Countdown(5):
    print(num)  # 5, 4, 3, 2, 1

__call__

讓物件可以像函數一樣呼叫:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15
print(double(10))  # 20

應用:建立裝飾器

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # Call 1 of greet \n Hello, Alice!
print(greet("Bob"))    # Call 2 of greet \n Hello, Bob!

__enter__ 和 __exit__

實作 context manager(用於 with 語句):

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.end = time.time()
        print(f"Elapsed time: {self.end - self.start:.4f} seconds")
        return False  # 不抑制例外

with Timer():
    import time
    time.sleep(1)
# Elapsed time: 1.0012 seconds

完整範例

class Fraction:
    def __init__(self, numerator, denominator):
        if denominator == 0:
            raise ValueError("Denominator cannot be zero")
        self.numerator = numerator
        self.denominator = denominator
        self._simplify()
    
    def _gcd(self, a, b):
        while b:
            a, b = b, a % b
        return a
    
    def _simplify(self):
        gcd = self._gcd(abs(self.numerator), abs(self.denominator))
        self.numerator //= gcd
        self.denominator //= gcd
        if self.denominator < 0:
            self.numerator *= -1
            self.denominator *= -1
    
    def __repr__(self):
        return f"Fraction({self.numerator}, {self.denominator})"
    
    def __str__(self):
        return f"{self.numerator}/{self.denominator}"
    
    def __add__(self, other):
        num = self.numerator * other.denominator + other.numerator * self.denominator
        den = self.denominator * other.denominator
        return Fraction(num, den)
    
    def __sub__(self, other):
        num = self.numerator * other.denominator - other.numerator * self.denominator
        den = self.denominator * other.denominator
        return Fraction(num, den)
    
    def __mul__(self, other):
        return Fraction(self.numerator * other.numerator,
                       self.denominator * other.denominator)
    
    def __truediv__(self, other):
        return Fraction(self.numerator * other.denominator,
                       self.denominator * other.numerator)
    
    def __eq__(self, other):
        return (self.numerator == other.numerator and 
                self.denominator == other.denominator)
    
    def __float__(self):
        return self.numerator / self.denominator

# 使用
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)

print(f1 + f2)  # 5/6
print(f1 - f2)  # 1/6
print(f1 * f2)  # 1/6
print(f1 / f2)  # 3/2
print(float(f1))  # 0.5