---
# System prepended metadata

title: 類 & 對象 - 程式分享
tags: [python]

---

# 類 & 對象 - 程式分享

###### tags: `python`

## 回顧
1. **實例化**: 呼叫 `__init__` 方法
2. **實例屬性**: 實例使用的變量
3. **實例方法**: 實例使用的方法
4. **類屬性**: 類定義時就被初始化，所有實例共用的屬性
6. `instance.__dict__`: 返回字典，鍵/值分別為 `instance` 的屬性/屬性對應的值 
7. `dir(instance)`: 尋找 `instance` 所有屬性(包括從父類中繼承的屬性)

## 介紹
1. 改變對象的字符串顯示 **x**
2. 自定義字符串的格式化 **x**
3. **讓對象支持上下文管理協議**:使用 `with` 語句。類別實現 `__enter__`, `__exit__`  兩個方法。  
    ```python=
    class File:
      def __init__(self, filename, mode):
        # 設定檔名與開檔模式
        self.filename = filename
        self.mode = mode

      # 配給資源（開啟檔案）
      def __enter__(self):
        print("開啟檔案：" + self.filename)
        self.open_file = open(self.filename, self.mode)
        return self.open_file

      # 回收資源（關閉檔案）
      def __exit__(self, type, value, traceback):
        print("關閉檔案：" + self.filename)
        self.open_file.close()
    
    with File("file.txt", "w") as f:
        print("寫入檔案...")
        f.write("Hello, world.")
    ```
    :::info
    應用場景: 文件，網路連結，鎖
    :::
4. **創建大量對象節省內存方法**:
   + `__slots__` 會為實例使用一種更加緊湊的內部表示。
   + 實例通過一固定大小的**數組**來構建，而不是為每個實例定義一個字典
   + 缺點: 實例無法添加新的屬性，只能用在 `__slots__` 中定義的那些屬性名。
    ```python=
    class Date:
        __slots__ = ['year', 'month', 'day']
        def __init__(self, year, month, day):
            self.year = year
            self.month = month
            self.day = day
    
    date = Date('2022', '04', '12')
    date.weather = 'sunny' # AttributeError: 'Date' object has no attribute 'height' 
    ```
    :::info
    關於 `__slots__` 的一個誤區是它可以作為一個**封裝工具防止用戶給實例增加新的屬性 (xx)**。儘管它可以達到這樣的目的，但是這個並不是它的初衷。
    :::
    
5. **在類中封裝屬性命** `_`, `__`: **Python 程序員不去依賴語言特性去封裝數據，而是通過遵循一定的屬性和方法命名規約來達到這個效果**。
   + `_`: 以單下劃線開頭的名字都應該是**內部實現**。
        ```python=
        class A:
            def __init__(self):
                self._internal = 0 # An internal attribute
                self.public = 1 # A public attribute

            def public_method(self):
                '''
                A public method
                '''
                pass

            def _internal_method(self):
                pass
        ```
   + `__`: 使用雙下劃線開始會導致訪問名稱變成其他形式，比如，下面的類 `B` 中，私有屬性會被分別重命名為 ``_B__private`` 和 ``_B__private_method``。
        ```python=
        class B:
            def __init__(self):
                self.__private = 0

            def __private_method(self):
                pass

            def public_method(self):
                pass
                self.__private_method()
        ```
    :::info
    大多數而言，你應該讓你的非公共名稱以單下劃線開頭。但是，如果你清楚你的代碼會涉及到子類， 並且有些內部屬性應該在子類中隱藏起來，那麼才考慮使用雙下劃線方案。
    :::
8. **創建可管理屬性**: 為了給某個實例的屬性增加除 **訪問** 與 **修改** 之外的其他處理邏輯，比如 **類型檢查** 或 **合法性驗證**。
   + **方法一** (data, getter, setter, deleter) 
        ```python=
        # 屬性要求特定類型，無法刪除屬性
        class Person:
            def __init__(self, first_name):
                self._first_name = first_name

            # Getter function
            @property
            def first_name(self):
                return self._first_name

            # Setter function
            @first_name.setter
            def first_name(self, value):
                if not isinstance(value, str):
                    raise TypeError('Expected a string')
                self._first_name = value

            # Deleter function (optional)
            @first_name.deleter
            def first_name(self):
                raise AttributeError("Can't delete attribute")

        a = Person('Guido')
        a.first_name # 'Guido'
        a.first_name = 42 # Calls the setter
        # Traceback (most recent call last):
        #     File "<stdin>", line 1, in <module>
        #     File "prop.py", line 14, in first_name
        #         raise TypeError('Expected a string')
        # TypeError: Expected a string
        del a.first_name
        # Traceback (most recent call last):
        #     File "<stdin>", line 1, in <module>
        # AttributeError: can`t delete attribute
        ```
        :::info
        以上初始化的時候無法進行類型檢查，可改成以下程式。
        :::
        ```python=
        class Person:
            def __init__(self, first_name):
                self.first_name = first_name
            ....
        ```
   + **方法二**: 使用 `property` 函數
        ```python=
        class Person:
            def __init__(self, first_name):
                self.set_first_name(first_name)

            # Getter function
            def get_first_name(self):
                return self._first_name

            # Setter function
            def set_first_name(self, value):
                if not isinstance(value, str):
                    raise TypeError('Expected a string')
                self._first_name = value

            # Deleter function (optional)
            def del_first_name(self):
                raise AttributeError("Can't delete attribute")

            # Make a property from existing get/set methods
            name = property(get_first_name, set_first_name, del_first_name)
            
        person = Person('travis')
        person.name # 'travis'
        ```
   + **方法三**: 定義動態計算屬性的方法
        ```python=
        import math
        class Circle:
            def __init__(self, radius):
                self.radius = radius

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

            @property
            def diameter(self):
                return self.radius * 2

            @property
            def perimeter(self):
                return 2 * math.pi * self.radius
        ```
   :::info
   **注意**: 只有確定要對屬性執行其他額外的操作時才應該用到 `property`，否則，它會讓代碼變得很臃腫，迷惑閱讀者。程序運行起來變慢很多。如下例子。
   :::
    ```python=
    class Person:
        def __init__(self, first_name):
            self.first_name = first_name

        @property
        def first_name(self):
            return self.first_name

        @first_name.setter
        def first_name(self, value):
            self._first_name = value
    ```
    :::info
    如何對**多個實例屬性**增加除訪問與修改之外的其他處理邏輯，又不會使程式碼臃腫 --> **描述器**
    :::
10. **調用父類方法**: 在子類中調用父類的某個已經被覆蓋的方法 `super()`。
    + **簡單範例**
        ```python=
        class A:
            def spam(self):
                print('A.spam')

        class B(A):
            def spam(self):
                print('B.spam')
                super().spam()  # Call parent spam()
        ```
    + **MRO**: 列表的構造是通過一個 **C3 線性化算法**來實現的，它實際上就是合併所有父類的 MRO 列表並遵循如下三條準則
      + 子類會先於父類被檢查
      + 多個父類會根據它們在列表中的順序被檢查
      + 如果對下一個類存在兩個合法的選擇，選擇第一個父類
      :::info 
      當使用 `super()` 時，Python 會在 **MRO** 列表上繼續搜索下一個類。只要每個重定義的方法統一使用 super() 並只調用它一次， 那麼控制流最終會遍歷完整個 **MRO** 列表，每個方法也只會被調用一次
      :::
      ```mermaid
      graph LR
      Base --> A
      Base --> B
      A --> C
      B --> C
      ```
        ```python=
        class Base:
            def __init__(self):
                print('Base.__init__')

        class A(Base):
            def __init__(self):
                super().__init__()
                print('A.__init__')

        class B(Base):
            def __init__(self):
                super().__init__()
                print('B.__init__')

        class C(A,B):
            def __init__(self):
                super().__init__()  # Only one call to super() here
                print('C.__init__')

        c = C()
        # Base.__init__
        # B.__init__
        # A.__init__
        # C.__init__
        
        C.__mro__
        # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
        # <class '__main__.Base'>, <class 'object'>)        
        ```


    
12. 子類中擴展 property **x**
 
14. **創建新的類或實例屬性**: 創建一個擁有一些**額外功能**的實例屬性類型，如類型檢查。
    + **描述器** 是一個實現三個核心屬性訪問操作的類 (**封裝功能**)
      + `__get__(self, instance, cls)`
      + `__set__(self, instance, value)` 
      + `__delete__(self, instance)`
    + 使用描述器時，需將這個**描述器的實例作為類屬性**
    + 描述器可實現大部分 Python 類特性中的底層魔法，包括 `@classmethod`, `@staticmethod`, `@property`
    ![](https://i.imgur.com/d6eWMUQ.png)

        ```python=
        # Descriptor attribute for an integer type-checked attribute
        class Integer:
            def __init__(self, name):
                self.name = name

            def __get__(self, instance, cls):
                if instance is None:
                    return self
                else:
                    return instance.__dict__[self.name]

            def __set__(self, instance, value):
                if not isinstance(value, int):
                    raise TypeError('Expected an int')
                instance.__dict__[self.name] = value

            def __delete__(self, instance):
                del instance.__dict__[self.name]


        class Point:
            x = Integer('x')
            y = Integer('y')

            def __init__(self, x, y):
                self.x = x
                self.y = y


        p = Point(2, 3)
        p.x # Calls Point.x.__get__(p,Point)
        # 2
        p.y = 5 # Calls Point.y.__set__(p, 5)
        p.x = 2.3 # Calls Point.x.__set__(p, 2.3)
        # Traceback (most recent call last):
        #     File "<stdin>", line 1, in <module>
        #     File "descrip.py", line 12, in __set__
        #         raise TypeError('Expected an int')
        # TypeError: Expected an int
        ```
    + **類裝飾器**描述器程式碼 (template)
        ```python=
        # Descriptor for a type-checked attribute
        class Typed:
            def __init__(self, name, expected_type):
                self.name = name
                self.expected_type = expected_type
                
            def __get__(self, instance, cls):
                if instance is None:
                    return self
                else:
                    return instance.__dict__[self.name]

            def __set__(self, instance, value):
                if not isinstance(value, self.expected_type):
                    raise TypeError('Expected ' + str(self.expected_type))
                instance.__dict__[self.name] = value
                
            def __delete__(self, instance):
                del instance.__dict__[self.name]

        # Class decorator that applies it to selected attributes
        def typeassert(**kwargs):
            def decorate(cls):
                for name, expected_type in kwargs.items():
                    # Attach a Typed descriptor to the class
                    setattr(cls, name, Typed(name, expected_type))
                return cls
            return decorate

        # Example use
        @typeassert(name=str, shares=int, price=float)
        class Stock:
            def __init__(self, name, shares, price):
                self.name = name
                self.shares = shares
                self.price = price
        ```
        :::info
        只想定義類中單個屬性訪問的話就不用去寫描述器了，使用`property` 會更加恰當。**當程序中有很多重複代碼的時候描述器就很有用了。**
        :::
   
16. 使用延遲計算屬性: **x**

17. **簡化數據結構的初始化**: 要寫很多僅用作數據結構的類，不想寫一堆煩人的 ``__init__()`` 函數
    + 支持位置參數
        ```python=
        import math

        class Structure1:
            # Class variable that specifies expected fields
            _fields = []

            def __init__(self, *args):
                if len(args) != len(self._fields):
                    raise TypeError('Expected {} arguments'.format(len(self._fields)))
                # Set the arguments
                for name, value in zip(self._fields, args):
                    setattr(self, name, value)


        # Example class definitions
        class Stock(Structure1):
            _fields = ['name', 'shares', 'price']

        class Point(Structure1):
            _fields = ['x', 'y']

        class Circle(Structure1):
            _fields = ['radius']

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


        s = Stock('ACME', 50, 91.1)
        p = Point(2, 3)
        c = Circle(4.5)
        s2 = Stock('ACME', 50)
        # Traceback (most recent call last):
        #     File "<stdin>", line 1, in <module>
        #     File "structure.py", line 6, in __init__
        #         raise TypeError('Expected {} arguments'.format(len(self._fields)))
        # TypeError: Expected 3 arguments
        ```
    + 支持關鍵字參數
        ```python=
        class Structure2:
            _fields = []

            def __init__(self, *args, **kwargs):
                if len(args) > len(self._fields):
                    raise TypeError('Expected {} arguments'.format(len(self._fields)))

                # Set all of the positional arguments
                for name, value in zip(self._fields, args):
                    setattr(self, name, value)

                # Set the remaining keyword arguments
                for name in self._fields[len(args):]:
                    setattr(self, name, kwargs.pop(name))

                # Check for any remaining unknown arguments
                if kwargs:
                    raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs)))
        # Example use
        if __name__ == '__main__':
            class Stock(Structure2):
                _fields = ['name', 'shares', 'price']

            s1 = Stock('ACME', 50, 91.1)
            s2 = Stock('ACME', 50, price=91.1)
            s3 = Stock('ACME', shares=50, price=91.1)
            # s3 = Stock('ACME', shares=50, price=91.1, aa=1)
        ```
    + 將不在 `_fields` 中的名稱加入到屬性中去 (template)
        ```python=
        class Structure3:
            # Class variable that specifies expected fields
            _fields = []

            def __init__(self, *args, **kwargs):
                if len(args) != len(self._fields):
                    raise TypeError('Expected {} arguments'.format(len(self._fields)))

                # Set the arguments
                for name, value in zip(self._fields, args):
                    setattr(self, name, value)

                # Set the additional arguments (if any)
                extra_args = kwargs.keys() - self._fields
                for name in extra_args:
                    setattr(self, name, kwargs.pop(name))

                if kwargs:
                    raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))

        # Example use
        if __name__ == '__main__':
            class Stock(Structure3):
                _fields = ['name', 'shares', 'price']

            s1 = Stock('ACME', 50, 91.1)
            s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
        ```


19. **定義接口或抽象基類**: 想定義一個接口或抽像類，並且通過執行類型檢查來確保子類實現了某些特定的方法。
    + 抽像類不能直接被實例化
    + 抽像類的目的是讓別的類繼承它並實現特定的抽象方法 (**多態**)
    ```mermaid
    graph 
    A["元類 (walk, attack, attacked, die)" ] --> B["神獸"]
    A --> C["士兵"]
    A --> D["英雄"]
    ```
    ```python=
    from abc import ABCMeta, abstractmethod

    class IStream(metaclass=ABCMeta):
        @abstractmethod
        def read(self, maxbytes=-1):
            pass

        @abstractmethod
        def write(self, data):
            pass

    a = IStream() # TypeError: Can't instantiate abstract class
                    # IStream with abstract methods read, write

    class SocketStream(IStream):
        def read(self, maxbytes=-1):
            pass

        def write(self, data):
            pass
    ```


19. 實現數據模型的類型約束 **x**
20. **實現自定義容器**: 實現自定義類來模擬容器類的功能，如列表和字典，但是你不確定到底要實現哪些方法。collections 定義很多抽象基類 (`collections.Iterable`, `collections.Sequence`)，自定義容器類時候它們非常有用
    + 繼承
    + 實現抽象方法
        ```python=
        class SortedItems(collections.Sequence):
            def __init__(self, initial=None):
                self._items = sorted(initial) if initial is not None else []

            # Required sequence methods
            def __getitem__(self, index):
                return self._items[index]

            def __len__(self):
                return len(self._items)

            # Method for adding an item in the right location
            def add(self, item):
                bisect.insort(self._items, item)


        items = SortedItems([5, 1, 3])
        print(list(items))
        print(items[0], items[-1])
        items.add(2)
        print(list(items))
        ```
22. **屬性的代理訪問**: 將某個實例的屬性訪問代理到內部另一個實例中去，作為**繼承**的一個替代方法或者實現**代理模式**。
    ![](https://i.imgur.com/i0Scm2D.png)

    + 簡單代理
        ```python=
        class A:
            def spam(self, x):
                pass

            def foo(self):
                pass


        class B1:
            def __init__(self):
                self._a = A()

            def spam(self, x):
                # Delegate to the internal self._a instance
                return self._a.spam(x)

            def foo(self):
                # Delegate to the internal self._a instance
                return self._a.foo()

            def bar(self):
                pass
        ```
    + 大量的方法需要代理
        ```python=
        class B2:
            def __init__(self):
                self._a = A()

            def bar(self):
                pass

            # Expose all of the methods defined on class A
            def __getattr__(self, name):
                """the __getattr__() method is actually a fallback method
                that only gets called when an attribute is not found"""
                return getattr(self._a, name)
            
        b = B()
        b.bar() # Calls B.bar() (exists on B)
        b.spam(42) # Calls B.__getattr__('spam') and delegates to A.spam
        ```
        :::info
        `__getattr__` 方法是在訪問屬性或方法不存在時才被調用。
        :::
    + 代理模式
        ```python=
        # A proxy class that wraps around another object, but
        # exposes its public attributes
        class Proxy:
            def __init__(self, obj):
                self._obj = obj

            # Delegate attribute lookup to internal obj
            def __getattr__(self, name):
                print('getattr:', name)
                return getattr(self._obj, name)

            # Delegate attribute assignment
            def __setattr__(self, name, value):
                if name.startswith('_'):
                    super().__setattr__(name, value)
                else:
                    print('setattr:', name, value)
                    setattr(self._obj, name, value)

            # Delegate attribute deletion
            def __delattr__(self, name):
                if name.startswith('_'):
                    super().__delattr__(name)
                else:
                    print('delattr:', name)
                    delattr(self._obj, name)
        
                    
        class Spam:
            def __init__(self, x):
                self.x = x

            def bar(self, y):
                print('Spam.bar:', self.x, y)

        # Create an instance
        s = Spam(2)
        # Create a proxy around it
        p = Proxy(s)
        # Access the proxy
        print(p.x)  # Outputs 2
        p.bar(3)  # Outputs "Spam.bar: 2 3"
        p.x = 37  # Changes s.x to 37
        ```
        :::info
        通過自定義屬性訪問方法，你可以用不同方式自定義代理類行為。
        :::
        :::info
        1. **繼承** (is a): 人 --> 黃種人，白種人，黑人
        2. **組合** (has a): 人 --> 手，腳，頭，...
        3. **代理**: 總公司 --> 代理商
        :::
    + 繼承
        ```python=
        class A:
            def spam(self, x):
                print('A.spam', x)
                
            def foo(self):
                print('A.foo')

        class B:
            def __init__(self):
                self._a = A()
                
            def spam(self, x):
                print('B.spam', x)
                self._a.spam(x)
                
            def bar(self):
                print('B.bar')
                
            def __getattr__(self, name):
                return getattr(self._a, name)
        ```
    :::info
    **注意**: 
    1. `__setattr__()` 和 `__delattr__()` 需要額外的魔法來區分代理實例和被代理實例 `_obj` 的屬性。**一個通常的約定是只代理那些不以下劃線 `_` 開頭的屬性**
    2. `__getattr__()` 對於大部分以雙下劃線 `__` 開始和結尾的屬性並不適用
    :::

16. **類中定義多個構造器**: 除了使用 `__init__()` 方法，還有其他方式可以初始化它
    ```python=
    class Date:
        # Primary constructor
        def __init__(self, year, month, day):
            self.year = year
            self.month = month
            self.day = day

        # Alternate constructor
        @classmethod
        def today(cls):
            t = time.localtime()
            return cls(t.tm_year, t.tm_mon, t.tm_mday)

    a = Date(2012, 12, 21) # Primary
    b = Date.today() # Alternate
    ```

## 參考資料
1. [__getattr__](https://www.itread01.com/content/1561550644.html)
2. [__setattr__, __delattr__, __getattr__](https://zhuanlan.zhihu.com/p/62569340)
3. [property]()
4. [class variable, class method]()