物件導向程式設計#

物件導向程式設計(Object-Oriented Programming,OOP) 是一種程式設計的思維方式:把程式裡的資料和操作資料的行為,打包成一個個獨立的「物件」來管理。

舉個生活例子:

  • 類別(Class) 是「汽車的設計藍圖」,定義了汽車有哪些零件(屬性)和功能(方法)
  • 物件(Object) 是根據藍圖「實際製造出來的一台汽車」,每台車的顏色、里程數可以不同

OOP 的四大核心概念:

概念說明
封裝 (Encapsulation)將資料和方法包在一起,隱藏內部細節
繼承 (Inheritance)子類別繼承父類別的屬性和方法,再加以擴充
多型 (Polymorphism)不同類別的物件,可以用相同的方式操作
抽象 (Abstraction)只暴露必要的介面,隱藏實作細節

定義類別與建立物件#

class 關鍵字定義類別,__init__ 是初始化方法(建構子),建立物件時自動呼叫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Dog:
    def __init__(self, name, age):
        # self 代表物件本身
        # self.name 是「實例屬性」,每個物件各自獨立
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name} 說:汪!")

    def info(self):
        print(f"{self.name}{self.age} 歲")


# 建立物件(實例化)
dog1 = Dog("小白", 3)
dog2 = Dog("黑豆", 5)

dog1.bark()       # 小白 說:汪!
dog2.info()       # 黑豆,5 歲

# 直接存取屬性
print(dog1.name)  # 小白
print(dog2.age)   # 5

self 是什麼?#

self 代表「這個物件自己」,是所有實例方法的第一個參數。呼叫方法時 Python 會自動傳入,不需要手動帶入:

1
2
3
dog1.bark()
# 等同於
Dog.bark(dog1)

類別屬性 vs 實例屬性#

  • 實例屬性 :每個物件各自擁有,在 __init__ 裡用 self.屬性名稱 定義
  • 類別屬性 :所有物件共享,直接定義在 class 內
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Car:
    # 類別屬性(所有 Car 物件共享)
    total_cars = 0
    wheels = 4

    def __init__(self, brand, color):
        # 實例屬性(每台車各自獨立)
        self.brand = brand
        self.color = color
        Car.total_cars += 1   # 每建立一台車,計數加一

    def info(self):
        print(f"{self.color} {self.brand}{self.wheels} 輪")


car1 = Car("Toyota", "紅色")
car2 = Car("BMW", "黑色")

car1.info()              # 紅色 Toyota,4 輪
car2.info()              # 黑色 BMW,4 輪
print(Car.total_cars)    # 2(透過類別存取)
print(car1.total_cars)   # 2(也可以透過物件存取)

類別方法與靜態方法#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Circle:
    pi = 3.14159

    def __init__(self, radius):
        self.radius = radius

    # 一般實例方法:操作實例屬性,第一個參數是 self
    def area(self):
        return Circle.pi * self.radius ** 2

    # 類別方法(@classmethod):操作類別屬性,第一個參數是 cls
    @classmethod
    def from_diameter(cls, diameter):
        """用直徑建立圓形(替代建構子)"""
        return cls(diameter / 2)

    # 靜態方法(@staticmethod):不需要 self 或 cls,只是邏輯上屬於這個類別
    @staticmethod
    def is_valid_radius(r):
        return r > 0


c1 = Circle(5)
c2 = Circle.from_diameter(10)  # 用直徑 10 建立,等同 Circle(5)

print(c1.area())                    # 78.53975
print(c2.radius)                    # 5.0
print(Circle.is_valid_radius(-1))   # False

封裝(Encapsulation)#

封裝的目的是 保護資料 ,避免外部程式碼直接修改物件的內部狀態,造成不合理的值。

Python 用命名慣例來表示存取限制:

寫法意義
self.name公開屬性,任何地方都能存取
self._name保護屬性(慣例),表示「不建議從外部直接存取」
self.__name私有屬性,Python 會改名為 _類別名__name,外部難以直接存取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner         # 公開
        self._balance = balance    # 保護(不建議外部直接修改)

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("存款金額必須大於 0")
        self._balance += amount
        print(f"存入 {amount:,},餘額:{self._balance:,}")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("提款金額必須大於 0")
        if amount > self._balance:
            raise ValueError("餘額不足")
        self._balance -= amount
        print(f"提取 {amount:,},餘額:{self._balance:,}")

    def get_balance(self):
        return self._balance


acc = BankAccount("Alice", 10000)
acc.deposit(5000)      # 存入 5,000,餘額:15,000
acc.withdraw(3000)     # 提取 3,000,餘額:12,000
print(acc.get_balance())  # 12000

@property:讓存取像屬性一樣自然#

@property 讓你可以在「像存取屬性」的同時,背後執行驗證邏輯:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age   # 這裡會觸發 age.setter

    @property
    def age(self):
        """getter:讀取 age 時呼叫"""
        return self._age

    @age.setter
    def age(self, value):
        """setter:設定 age 時呼叫,加入驗證"""
        if not isinstance(value, int):
            raise TypeError("年齡必須是整數")
        if value < 0 or value > 150:
            raise ValueError(f"年齡 {value} 不合理")
        self._age = value


p = Person("Alice", 25)
print(p.age)   # 25(透過 getter,寫法和一般屬性一樣)
p.age = 26     # 透過 setter
print(p.age)   # 26

try:
    p.age = -5
except ValueError as e:
    print(e)   # 年齡 -5 不合理

繼承(Inheritance)#

繼承讓子類別直接擁有父類別的屬性和方法,再針對自己的需求覆寫或新增:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    def speak(self):
        print(f"{self.name} 說:{self.sound}!")

    def __str__(self):
        return f"{type(self).__name__}{self.name})"


class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "汪汪")  # 呼叫父類別的 __init__

    # 覆寫(Override)父類別的方法
    def speak(self):
        print(f"{self.name} 熱情地說:{self.sound}!!")

    # 子類別新增的方法
    def fetch(self):
        print(f"{self.name} 去撿球了!")


class Cat(Animal):
    def __init__(self, name):
        super().__init__(name, "喵喵")

    def speak(self):
        print(f"{self.name} 慵懶地說:{self.sound}~")

    def purr(self):
        print(f"{self.name} 發出呼嚕聲...")


dog = Dog("小白")
cat = Cat("咪咪")

dog.speak()   # 小白 熱情地說:汪汪!!
cat.speak()   # 咪咪 慵懶地說:喵喵~
dog.fetch()   # 小白 去撿球了!
cat.purr()    # 咪咪 發出呼嚕聲...

print(dog)    # Dog(小白)
print(cat)    # Cat(咪咪)

super() 的用途#

super() 用來呼叫父類別的方法,最常在 __init__ 中使用,確保父類別的初始化邏輯也被執行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Vehicle:
    def __init__(self, brand, speed):
        self.brand = brand
        self.speed = speed

    def info(self):
        print(f"{self.brand},最高時速 {self.speed} km/h")


class Car(Vehicle):
    def __init__(self, brand, speed, doors):
        super().__init__(brand, speed)   # 先執行父類別的初始化
        self.doors = doors               # 再設定子類別特有的屬性

    def info(self):
        super().info()                   # 呼叫父類別的 info()
        print(f"車門數:{self.doors}")


class ElectricCar(Car):
    def __init__(self, brand, speed, doors, battery):
        super().__init__(brand, speed, doors)
        self.battery = battery

    def info(self):
        super().info()
        print(f"電池容量:{self.battery} kWh")


ev = ElectricCar("Tesla", 250, 4, 100)
ev.info()
# Tesla,最高時速 250 km/h
# 車門數:4
# 電池容量:100 kWh

型別判斷#

1
2
3
4
5
6
print(isinstance(dog, Dog))     # True
print(isinstance(dog, Animal))  # True(Dog 繼承自 Animal)
print(isinstance(dog, Cat))     # False

print(issubclass(Dog, Animal))  # True
print(issubclass(Cat, Animal))  # True

多型(Polymorphism)#

多型指的是: 不同類別的物件,可以用相同的方式呼叫,各自執行自己的版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import math

class Shape:
    def area(self):
        raise NotImplementedError("子類別必須實作 area()")

    def describe(self):
        print(f"{self.__class__.__name__},面積 = {self.area():.2f}")


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height


class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height


# 不同類別的物件,用同一個介面操作
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
    shape.describe()   # 每個物件呼叫自己的 area()

# Circle,面積 = 78.54
# Rectangle,面積 = 24.00
# Triangle,面積 = 12.00

# 計算總面積
total = sum(s.area() for s in shapes)
print(f"總面積:{total:.2f}")   # 總面積:114.54

魔術方法(Magic Methods)#

Python 的魔術方法(又稱 dunder methods,因為前後各有兩個底線)讓你的類別支援 Python 的內建語法:

方法觸發時機範例
__init__建立物件Dog("小白", 3)
__str__print(obj)str(obj)print(dog)
__repr__互動模式顯示、repr(obj)dog
__len__len(obj)len(bag)
__eq__obj1 == obj2v1 == v2
__lt__obj1 < obj2v1 < v2
__add__obj1 + obj2v1 + v2
__contains__x in objitem in bag
__getitem__obj[key]bag[0]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __repr__(self):
        return f"Vector({self.x}, {self.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 __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __len__(self):
        # 回傳向量長度(取整數)
        return int((self.x ** 2 + self.y ** 2) ** 0.5)


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

print(v1 + v2)        # (4, 6)
print(v2 - v1)        # (2, 2)
print(v1 * 3)         # (3, 6)
print(v1 == v2)       # False
print(v1 == Vector(1, 2))  # True
print(len(v2))        # 5(3² + 4² = 25,√25 = 5)

實戰範例:交易帳戶#

綜合運用類別、封裝、繼承、@property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class TradingAccount:
    """基本交易帳戶"""

    def __init__(self, owner, cash):
        self.owner = owner
        self._cash = cash
        self._holdings = {}   # {股票代號: 持有股數}
        self._trades = []     # 交易紀錄

    @property
    def cash(self):
        return self._cash

    @property
    def holdings(self):
        return dict(self._holdings)   # 回傳副本,防止外部直接修改

    def buy(self, symbol, shares, price):
        cost = shares * price
        if cost > self._cash:
            raise ValueError(f"現金不足:需要 {cost:,},現有 {self._cash:,}")
        self._cash -= cost
        self._holdings[symbol] = self._holdings.get(symbol, 0) + shares
        self._trades.append(("買入", symbol, shares, price))
        print(f"買入 {symbol} {shares} 股 @ {price},現金剩餘:{self._cash:,.0f}")

    def sell(self, symbol, shares, price):
        if self._holdings.get(symbol, 0) < shares:
            raise ValueError(f"{symbol} 持股不足")
        self._cash += shares * price
        self._holdings[symbol] -= shares
        if self._holdings[symbol] == 0:
            del self._holdings[symbol]
        self._trades.append(("賣出", symbol, shares, price))
        print(f"賣出 {symbol} {shares} 股 @ {price},現金剩餘:{self._cash:,.0f}")

    def portfolio(self, current_prices):
        total = self._cash
        print(f"\n===== {self.owner} 的投資組合 =====")
        print(f"現金:{self._cash:>12,.0f}")
        for symbol, shares in self._holdings.items():
            price = current_prices.get(symbol, 0)
            value = shares * price
            total += value
            print(f"{symbol}{shares} 股 × {price:,} = {value:>10,.0f}")
        print(f"{'─' * 30}")
        print(f"總資產:{total:>10,.0f}")

    def __str__(self):
        return f"TradingAccount({self.owner}, 現金={self._cash:,.0f})"


class MarginAccount(TradingAccount):
    """融資帳戶(繼承自 TradingAccount,可用槓桿)"""

    def __init__(self, owner, cash, leverage=2):
        super().__init__(owner, cash)
        self.leverage = leverage   # 槓桿倍數

    @property
    def buying_power(self):
        """可用買力 = 現金 × 槓桿"""
        return self._cash * self.leverage

    def buy(self, symbol, shares, price):
        cost = shares * price
        if cost > self.buying_power:
            raise ValueError(f"超過買力上限:{self.buying_power:,}")
        # 直接操作父類別的私有屬性需透過方法
        # 這裡簡化為:只扣實際持有的現金比例
        actual_cost = cost / self.leverage
        self._cash -= actual_cost
        self._holdings[symbol] = self._holdings.get(symbol, 0) + shares
        self._trades.append(("買入(融資)", symbol, shares, price))
        print(f"融資買入 {symbol} {shares} 股 @ {price},買力剩餘:{self.buying_power:,.0f}")


# 一般帳戶
acc = TradingAccount("Alice", 100_000)
acc.buy("2330", 10, 950)
acc.buy("0050", 20, 150)
acc.sell("2330", 5, 1_000)

current_prices = {"2330": 1_000, "0050": 155}
acc.portfolio(current_prices)

print()

# 融資帳戶
margin = MarginAccount("Bob", 50_000, leverage=2)
print(f"買力:{margin.buying_power:,}")   # 100,000
margin.buy("2330", 5, 950)

輸出:

買入 2330 10 股 @ 950,現金剩餘:90,500
買入 0050 20 股 @ 150,現金剩餘:87,500
賣出 2330 5 股 @ 1000,現金剩餘:92,500

===== Alice 的投資組合 =====
現金:        92,500
2330:5 股 × 1,000 =      5,000
0050:20 股 × 155 =      3,100
──────────────────────────────
總資產:    100,600

買力:100,000
融資買入 2330 5 股 @ 950,買力剩餘:95,250