五、類
5.1 定義類
- 使用
class
關鍵字定義一個類,類名通常採用首字母大寫的駝峰命名法class Person: pass
5.2 建構函式
-
基本語法
class Person: def __init__(self, name, age): # 定義建構函式 self.name = name # 初始化 name 屬性 self.age = age # 初始化 age 屬性
-
python中,有且僅有一個建構函式。
-
建構函式不能有返回值
-
建構函式第一個引數必須是self
-
可以透過
super().__init__()
呼叫父類的建構函式-
如果在子類中沒有定義
__init__
方法,會自動呼叫父類的__init__
方法。 -
但是如果子類定義類
__init__
方法,不主動呼叫父類的建構函式,父類的建構函式不會被呼叫。class Animal: def __init__(self, name): print(f"Animal initialized with name: {name}") self.name = name class Dog(Animal): def __init__(self, breed): print(f"Dog initialized with breed: {breed}") self.breed = breed # 建立 Dog 物件,不會呼叫 Animal 的建構函式 dog = Dog("Labrador") dog = Dog( "Labrador") print(dog.name) # 這裡會報錯'Dog' object has no attribute 'name'
-
-
如果有多重繼承,可以指定呼叫某個父類的建構函式,
class D(B, C): def __init__(self): B.__init__(self) # 呼叫 B 的建構函式 C.__init__(self) # 呼叫 C 的建構函式 print("D的建構函式")
- 也可以直接
super().__init__()
呼叫父類的建構函式,那麼,先呼叫誰呢?- 呼叫順序是由
MRO
決定的,怎麼檢視呼叫順序?print(D.__mro__)
- 呼叫順序是由
- 也可以直接
-
__new__
方法,是建立一個物件,而上述的__init__()
是初始化物件。__new__
必須返回一個例項物件,返回的例項物件自動傳遞給__init__
5.3 屬性
-
例項屬性:在
__init__
方法中定義,屬於某個具體的例項 -
類屬性:屬於類本身,所有例項共享同一屬性。
-
如何理解類屬性?
-
有一個
Person
類class Person: species = "Human" # 類屬性 def __init__(self, name, age): self.name = name # 例項屬性 name self.age = age # 例項屬性 age
-
首先,類屬性有點像C++的靜態成員變數,
p = Person("ouyang", 18) p2 = Person("ouyang2", 19) print(p.species) # Human print(p2.species) # Human Person.species = "People" print(p.species) # People print(p2.species) # People
-
但是,python中,修改了其中一個例項物件的類屬性後,並不會影響其他例項物件的類屬性,這怎麼理解?
p = Person("ouyang", 18) p2 = Person("ouyang2", 19) print(p.species) # Human print(p2.species) # Human p2.species = "People" print(p.species) # Human print(p2.species) # People
-
第5行,實際上是p2建立了一個例項屬性,同樣是
species
,以後再訪問species
,會是p2的例項屬性,而不再訪問類屬性。所以,p2修改的是自己的例項屬性,自然不會影響到其他的例項物件,同樣也不會影響到類屬性。
-
5.4 方法
-
例項方法:第一個引數必須是
self
,並且訪問屬性時,必須使用self.屬性
的方式訪問。-
例項方法只能例項物件訪問嗎?類能訪問嗎?答案是可以的,因為python中,「類」是一個特殊的「例項物件」,是python自動幫我們建立好的,如下程式碼
class A : def __init__(self) -> None: self.name = "A" def hi(self): print("hello") A.hi(A) # hello
-
但是,不建議這樣使用,因為例項方法可以使用例項屬性,但是「類」,沒有例項屬性,如下
class A : def __init__(self) -> None: self.name = "A" def hi(self): print("hello", self.name) A.hi(A) # 報錯 AttributeError: type object 'A' has no attribute 'name'
-
-
類方法:第一個引數必須是
cls
,並且需要透過@classmethod
裝飾器來定義,只能操作類屬性class Person: species = "Human" # 類屬性 def __init__(self, name, age): self.name = name # 例項屬性 name self.age = age # 例項屬性 age @classmethod def Person_greet(cls): print("hello person") cls.species = "People" # 改變了類屬性 會影響到其他例項物件的類屬性
-
靜態方法:繫結到類上,透過
@staticmethod
裝飾器來定義,但是不訪問屬性,可以認為就是類中的普通函式。@staticmethod def Person_greet(cls): print("hello person") cls.species = "People"
-
類中的普通函式:屬於這個類的函式,但是不能訪問例項屬性,也不能訪問類屬性,和靜態方法的主要區別就是,靜態方法能夠透過例項物件訪問,但是類中的普通函式只能透過類名來訪問。
class Person: def Person_greet(): print("hello person") Person.Person_greet() # hello person
-
注意,方法名不要以
__
雙下劃線開頭,這有別的含義。
5.5 繼承
-
基本語法
class Son(Father):
Son類繼承了Father類 -
重寫
- 子類的方法簽名(方法名、引數列表)必須與父類方法一致。
- 直接呼叫,會呼叫子類的,如果想要呼叫父類的可以使用
super().方法名
-
super()
函式-
如果有多個父類,
super()
到底是哪個父?有一個叫做方法解析順序(MRO),決定了父類方法的呼叫順序。可以透過print(類名.mro())
來檢視 -
使用
super()
呼叫父類的建構函式時,會根據MRO,呼叫所有父類的建構函式。但是,呼叫一般方法(假設所有父類都有),只會呼叫MRO順序的第一個父類。 -
假如我不希望使用MRO順序呢?我希望呼叫某個父類,看下面例子
class A: def say(self): print("A") class B: def say(self): print("B") class C(B): def say(self): print("C") class D(A, C) : def say(self): B.say(self) # 雖然D只繼承了A、C,但是C繼承了B,所以可以呼叫B print("D"); d = D(); d.say() # 列印B D、
-
5.6 封裝
- 在屬性前,加字首
_
表示「保護許可權」,加字首__
表示「私有屬性」- 保護許可權,不是強制性的規則,起到「提醒作用」,也就是說,在類外部仍然可以訪問。
- 私有許可權,禁止外部使用,但是可以透過
物件名._類名__私有屬性名
來訪問。
5.7 特殊方法
-
__str__(self)
-
當使用print,或者str函式作用於該物件時,獲取該物件的字串形式
-
例子
class Person: _species = "Human" # 類屬性 def __init__(self, name, age): self.__name = name # 例項屬性 name self.age = age # 例項屬性 age def __str__(self) : return f"Person(name={self.__name}, age={self.age})" p = Person("ouyang", 18) print(p) # Person(name=ouyang, age=18)
-
還有個類似的
__repr__
:在除錯和互動模式下顯示物件的更精確或有用的描述。實現__repr__
方法可以使物件在輸出時顯示定製的資訊,而不是直接使用物件的記憶體地址。
-
-
__len__(self)
- 當呼叫
len(例項物件)
時,會呼叫__len__
方法,如果沒有實現,則會丟擲異常
- 當呼叫
-
__getitem__(self, key)
- 使物件能夠像訪問「字典」一樣,透過key,得到相應的value
-
__setitem__(self, key, value)
-
使物件能夠像訪問「字典一樣」,透過key,設定value
-
class Person: _species = "Human" # 類屬性 def __init__(self, name, age): self.__name = name # 例項屬性 name self.age = age # 例項屬性 age def __getitem__(self, key): if key == "name": return self.__name else: return self.age def __setitem__(self, key, val): if key == "name": self.__name = val else : self.age = val p = Person("ouyang", 18) p["name"] = "ouyang2" print(p["name"]) # ouyang2
-
-
__del__(self)
- 析構器,物件銷燬前的清理操作,但是並不建議依賴
__del__
進行記憶體管理,手動呼叫del obj
並不會立刻觸發__del__
,而是垃圾回收機制決定何時釋放記憶體。 - 可以使用上下文管理器替換
__del__
,更清晰和安全
- 析構器,物件銷燬前的清理操作,但是並不建議依賴
-
上下文管理器
-
__enter__
:在進入with
語句程式碼塊時呼叫,負責設定或分配資源,並且可以返回資源物件 -
__exit__
:在離開with
語句程式碼塊時呼叫,負責清理或釋放資源。無論程式碼塊是否正常結束或出現異常,__exit__
都會執行。 -
例子
class FileHandler: def __init__(self, filename, mode): self.file = open(filename, mode) def __enter__(self): return self.file # 返回檔案物件供 with 語句內使用 def __exit__(self, exc_type, exc_value, traceback): self.file.close() # 確保在 with 程式碼塊後關閉檔案 # 使用自定義上下文管理器 with FileHandler("example.txt", "w") as file: file.write("Hello, World!")
-
-
__iter__(self)
- 當一個物件實現了
__iter__
方法,它就可以被 Python 的內建函式iter()
呼叫,並且可以用於for
迴圈、生成列表、使用list()
等建構函式。 - 返回值:
__iter__
方法必須返回一個迭代器物件,也就是一個實現了__next__
方法的物件。
- 當一個物件實現了
-
__next__(self)
-
通常
__iter__
和__next__
搭配使用。 -
__next__
方法是實際提供資料的地方。每次呼叫__next__
時,返回當前值,並將迭代器的狀態更新為下一個值。它在沒有更多資料可返回時丟擲StopIteration
異常,表示迭代結束。 -
在使用
for
迴圈時,Python 首先會呼叫__iter__
方法獲取一個迭代器物件,然後反覆呼叫__next__
來逐步獲取值。 -
例子
class Counter: def __init__(self, start, end): self.current = start self.end = end def __iter__(self): return self # 返回自身作為迭代器 def __next__(self): if self.current >= self.end: raise StopIteration # 無更多值時停止迭代 self.current += 1 return self.current - 1 # 使用 Counter 迭代器 counter = Counter(1, 4) for num in counter: print(num) # 輸出 1, 2, 3
-
-
__add__(self, other)
- 自定義類的加法
-
__sub__(self, other)
-
自定義類的減法
-
類似的還有
__mul__
乘法、__truediv__
除法 -
例子
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, other): return Vector(self.x * other, self.y * other) def __truediv__(self, other): return Vector(self.x / other, self.y / other) # 列印時 def __repr__(self): return f"Vector({self.x}, {self.y})" v1 = Vector(3, 4) print(v1 + v1) # Vector(6, 8) print(v1 - v1) # Vector(0, 0) print(v1 * 3) # Vector(9, 12) print(v1 / 2) # Vector(1.5, 2.0)
-
-
__eq__(self, other)
- 自定義類的
==
操作,返回一個布林值
- 自定義類的
-
__lt__(self, other)
-
自定義類的
<
操作,返回一個布林值 -
類似的還有
__le__
小於等於、__gt__
大於、__ge__
:大於等於、__ne__
:不等於 -
例子
class Person: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): return self.age == other.age def __lt__(self, other): return self.age < other.age def __le__(self, other): return self.age <= other.age def __gt__(self, other): return self.age > other.age def __ge__(self, other): return self.age >= other.age def __ne__(self, other): return self.age != other.age p1 = Person("Alice", 30) p2 = Person("Bob", 25) print(p1 > p2) # 輸出: True print(p1 != p2) # 輸出: True
-
-
__call__(self, ...)
-
允許類的例項像函式一樣被呼叫(C++中的仿函式)。
-
語法
class MyClass: def __call__(self, *args, **kwargs): pass
-
例子
class Greeter: def __init__(self, greeting): self.greeting = greeting def __call__(self, name): return f"{self.greeting}, {name}!" # 建立 Greeter 類的例項 greet = Greeter("Hello") # 使用像呼叫函式一樣呼叫例項 print(greet("Alice")) # 輸出: Hello, Alice!
-
5.8 多型
-
先看一個例子
class Dog: def speak(self): print("Dog barks") class Cat: def speak(self): print("Cat meows") class Human: def speak(self): print("Human talks") # 使用不同型別的物件,只要它們都有相同的方法,Python就能呼叫 def make_sound(animal): animal.speak() make_sound(Dog()) # Dog barks make_sound(Cat()) # Cat meows make_sound(Human()) # Human talks
- 有一個函式
make_sound()
,能夠接收所有,包含speak()
方法的類,然後呼叫speak()
方法。 - 總感覺不倫不類的,因為對比C++,父類指標指向子類物件,但是因為python一個變數,型別是能夠變的,所以這裡的animal,本來就可以接收任何資料型別,所以,python的多型,沒有C++那麼多限制,比如,子類必須要「實現父類的所有抽象方法」。
- 所以,python多型總覺得「不是真正的多型。。。」
- 有一個函式
5.9 抽象類
-
抽象類是一種特殊的類,用來定義一組方法,子類必須實現這些方法。抽象類本身不能被例項化。它透過定義抽象方法來強制子類去實現某些方法。抽象類有兩個主要特性:
- 抽象方法:這是沒有實現的函式,它只有宣告沒有方法體。子類必須實現全部抽象方法,否則子類也會被視為抽象類,不能例項化。
- 不能例項化:你不能直接建立一個抽象類的物件,必須透過繼承它的子類來建立例項。
-
使用
@abstractmethod
裝飾器來標記一個方法為抽象方法 -
例子
from abc import ABC, abstractmethod class Animal(ABC): # Animal 是一個抽象類 @abstractmethod def make_sound(self): pass # 這個方法在抽象類中沒有實現 class Dog(Animal): # Dog 是 Animal 的子類 def make_sound(self): return "Woof" class Cat(Animal): # Cat 是 Animal 的子類 def make_sound(self): return "Meow" # 例項化 dog = Dog() print(dog.make_sound()) # 輸出: Woof cat = Cat() print(cat.make_sound()) # 輸出: Meow