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

```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. **屬性的代理訪問**: 將某個實例的屬性訪問代理到內部另一個實例中去,作為**繼承**的一個替代方法或者實現**代理模式**。

+ 簡單代理
```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]()