# 類 & 對象 - 程式分享 ###### 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]()