Python類

ouyangxx發表於2024-11-15

五、類

5.1 定義類

  1. 使用 class 關鍵字定義一個類,類名通常採用首字母大寫的駝峰命名法
    class Person:
        pass
    

5.2 建構函式

  1. 基本語法

    class Person:
        def __init__(self, name, age):  # 定義建構函式
            self.name = name            # 初始化 name 屬性
            self.age = age              # 初始化 age 屬性
    
    
  2. python中,有且僅有一個建構函式。

  3. 建構函式不能有返回值

  4. 建構函式第一個引數必須是self

  5. 可以透過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'
      
  6. 如果有多重繼承,可以指定呼叫某個父類的建構函式,

    class D(B, C):
        def __init__(self):
            B.__init__(self)  # 呼叫 B 的建構函式
            C.__init__(self)  # 呼叫 C 的建構函式
            print("D的建構函式")
    
    
    • 也可以直接super().__init__()呼叫父類的建構函式,那麼,先呼叫誰呢?
      • 呼叫順序是由MRO決定的,怎麼檢視呼叫順序?print(D.__mro__)
  7. __new__方法,是建立一個物件,而上述的__init__()是初始化物件。

    • __new__必須返回一個例項物件,返回的例項物件自動傳遞給__init__

5.3 屬性

  1. 例項屬性:在__init__方法中定義,屬於某個具體的例項

  2. 類屬性:屬於類本身,所有例項共享同一屬性。

    • 如何理解類屬性?

    • 有一個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 方法

  1. 例項方法:第一個引數必須是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'
      
  2. 類方法:第一個引數必須是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"    # 改變了類屬性  會影響到其他例項物件的類屬性
    
  3. 靜態方法:繫結到類上,透過@staticmethod裝飾器來定義,但是不訪問屬性,可以認為就是類中的普通函式。

    	@staticmethod
        def Person_greet(cls):
            print("hello person")
            cls.species = "People"
    
  4. 類中的普通函式:屬於這個類的函式,但是不能訪問例項屬性,也不能訪問類屬性,和靜態方法的主要區別就是,靜態方法能夠透過例項物件訪問,但是類中的普通函式只能透過類名來訪問。

    class Person:
        def Person_greet():
            print("hello person")
    Person.Person_greet()    # hello person
    
  5. 注意,方法名不要以__雙下劃線開頭,這有別的含義。

5.5 繼承

  1. 基本語法class Son(Father):Son類繼承了Father類

  2. 重寫

    • 子類的方法簽名(方法名、引數列表)必須與父類方法一致。
    • 直接呼叫,會呼叫子類的,如果想要呼叫父類的可以使用super().方法名
  3. 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 封裝

  1. 在屬性前,加字首_表示「保護許可權」,加字首__表示「私有屬性」
    • 保護許可權,不是強制性的規則,起到「提醒作用」,也就是說,在類外部仍然可以訪問。
    • 私有許可權,禁止外部使用,但是可以透過物件名._類名__私有屬性名來訪問。

5.7 特殊方法

  1. __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__ 方法可以使物件在輸出時顯示定製的資訊,而不是直接使用物件的記憶體地址。

  2. __len__(self)

    • 當呼叫len(例項物件)時,會呼叫__len__方法,如果沒有實現,則會丟擲異常
  3. __getitem__(self, key)

    • 使物件能夠像訪問「字典」一樣,透過key,得到相應的value
  4. __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
      
  5. __del__(self)

    • 析構器,物件銷燬前的清理操作,但是並不建議依賴__del__進行記憶體管理,手動呼叫del obj並不會立刻觸發__del__,而是垃圾回收機制決定何時釋放記憶體。
    • 可以使用上下文管理器替換__del__,更清晰和安全
  6. 上下文管理器

    • __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!")
      
  7. __iter__(self)

    • 當一個物件實現了 __iter__ 方法,它就可以被 Python 的內建函式 iter() 呼叫,並且可以用於 for 迴圈、生成列表、使用 list() 等建構函式。
    • 返回值: __iter__ 方法必須返回一個迭代器物件,也就是一個實現了 __next__ 方法的物件。
  8. __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
      
  9. __add__(self, other)

    • 自定義類的加法
  10. __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)
      
  11. __eq__(self, other)

    • 自定義類的==操作,返回一個布林值
  12. __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
      
      
  13. __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 多型

  1. 先看一個例子

    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 抽象類

  1. 抽象類是一種特殊的類,用來定義一組方法,子類必須實現這些方法。抽象類本身不能被例項化。它透過定義抽象方法來強制子類去實現某些方法。抽象類有兩個主要特性:

    • 抽象方法:這是沒有實現的函式,它只有宣告沒有方法體。子類必須實現全部抽象方法,否則子類也會被視為抽象類,不能例項化。
    • 不能例項化:你不能直接建立一個抽象類的物件,必須透過繼承它的子類來建立例項。
  2. 使用@abstractmethod裝飾器來標記一個方法為抽象方法

  3. 例子

    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