# dataclasses 【數據 Class】

## 簡介

***

`dataclasses` module 用於向用戶定義的 Class 新增 special methods (特殊方法)，例如：`__init__` 或 `__repr__`。

使用 Dataclasses 的主要優點是在建立 Class 時減少 [Boilerplate code](https://en.wikipedia.org/wiki/Boilerplate_code)，特別是那些主要儲存數據，但沒有太多行為 (方法) 的 Class。

Dataclasses 自動向 Class 加入 special methods，包括：`__init__`、`__repr__` 和 `__eq__`。 這些 special methods 手動編寫通常很乏味，特別是對於具有許多屬性的 Class。如果需要，也可以自定義和微調這些方法的行為。

數據類的裝飾器是 `@dataclasses.dataclass`。 這是一個簡單的例子：

{% code title="PYTHON" %}

```python
import dataclasses

@dataclasses.dataclass
class Point:
    x: int
    y: int

# Create instances of Point (__init__)
p1 = Point(2, 3)
p2 = Point(2, 3)
p3 = Point(3, 4)

# Demonstrates __repr__
print(p1)        # Output: Point(x=2, y=3)

# Demonstrates __eq__
print(p1 == p2)  # Output: True
print(p1 == p3)  # Output: False
```

{% endcode %}

Class `Point` 有兩個 fields `x` 和 `y`，這 2 個 fields 都應該是 `int`。

`@dataclass` 自動加入一個以 `x` 和 `y` 作為參數的 `__init__` 方法，一個返回物件的字串表示形式的 `__repr__` 方法，以及一個 `__eq__` 方法，允許根據 `Point` 的兩個實例的 `x` 屬性和 `y` 屬性來比較它們的相等性。

### Field

***

在 Python Dataclasses 的 context 中，field 是綁定到 Class 的變數，並且該 class 的每個實例都將具有其自己的單獨的 field 副本，來存儲特定於實例的狀態。

在傳統的物件導向術語中，這些通常被稱為 "attributes" or "properties"。Dataclasses 中的 fields 直接在 Class 中定義，並且可以指定它們的類型和預設值。

{% code title="PYTHON" %}

```python
import dataclasses

@dataclasses.dataclass
class Person:
    name: str
    age: int = 0
```

{% endcode %}

在此範例中，`name` 和 `age` 是 `Person` class 的 fields。

`name` field 應為 `str`，`age` field 應為 `int`，預設值為 0。每個 `Person` 實例都有自己的 `name` 和 `age` fields。

## @dataclass

***

`@dataclass` 用於生成的 special methods，然後新增到 Class 中。

當 `@dataclass` 應用於 Class 時，它會掃描該 Class 的 fields。 在這種情況下，fields 是包含類型註釋的 Class-level 變數。

{% code title="PYTHON" %}

```python
@dataclass(*, init=True, 
              repr=True, 
              eq=True, 
              order=False, 
              unsafe_hash=False, 
              frozen=False, 
              match_args=True, 
              kw_only=False, 
              slots=False, 
              weakref_slot=False)
```

{% endcode %}

<table><thead><tr><th width="179">參數</th><th>說明</th></tr></thead><tbody><tr><td><strong>init</strong></td><td>是否將生成 <code>__init__</code> 方法。<br>如果 Class 已經定義了 <code>__init__</code>，則忽略此參數。</td></tr><tr><td><strong>repr</strong></td><td>是否將生成 <code>__repr__</code> 方法。<br>生成的 repr String 將包含 Class 名稱以及每個 field 的名稱，按照它們在 Class 中定義的順序排列。 不包括標記為從 repr 中排除的 fields 。如果 Class 已經定義了 <code>__repr__</code>，則忽略此參數。</td></tr><tr><td><strong>eq</strong></td><td>是否將生成 <code>__eq__</code> 方法。<br>此方法按順序比較 Class，僅當它們對應的 fields 相等時，該 Class 的兩個實例才會被比較為相等。如果 Class 已經定義了 <code>__eq__</code> ，則忽略此參數。</td></tr><tr><td><strong>order</strong></td><td>是否將生成 <code>__lt__</code> (小於)、<code>__le__</code> (小於或等於)、<code>__gt__</code> (大於) 和 <code>__ge__</code> (大於或等於) 方法。<br>這些使得 Class 的實例可排序且具有可比性。如果 field's type 不支援比較，這將引發 TypeError。如果 Class 已經定義了 <code>__lt__</code>、<code>__le__</code>、<code>__gt__</code> 和 <code>__ge__</code> 中的任何一個，則會引發 TypeError。</td></tr><tr><td><strong>unsafe_hash</strong></td><td>如果為 <code>True</code>， 會強制生成 <code>__hash__</code> 方法，但不建議這樣做，因為它違反了 Python's object model，其中 mutable 物件是 unhashable。<br>如果為 <code>False</code>，則根據 <code>eq</code> 和 <code>frozen</code> 的設置方式生成 <code>__hash__</code> 方法。</td></tr><tr><td><strong>frozen</strong></td><td>在實例建立後，指派 fields 將會引發 Exception。<br>類似於其他語言中的唯讀 (凍結；frozen) 數據類型，就像 Tuple 一樣。如果 Class 中定義了 <code>__setattr__</code> 或 <code>__delattr__</code>，則會引發 TypeError。</td></tr><tr><td><strong>match_args</strong></td><td></td></tr><tr><td><strong>kw_only</strong></td><td></td></tr><tr><td><strong>slots</strong></td><td></td></tr><tr><td><strong>weakref_slot</strong></td><td></td></tr></tbody></table>

{% hint style="info" %}
**生成的方法按照 fields 在 Class 中定義的順序使用 fields。**&#x20;

例如: `__init__` 方法中的參數順序遵循 Class 中 field 定義的順序。
{% endhint %}

{% hint style="info" %}
**以下是管理 `__hash__` 方法隱式建立的規則。**&#x20;

如果 `frozen=True, eq=True`，則將生成 `__hash__` 方法。&#x20;

如果 `frozen=False, eq=True`，則 `__hash__` 方法將被設置為 None，將其標記為 unhashable。&#x20;

如果 `frozen=False, eq=False`，則 `__hash__` 將保持不變，這意味著將使用 Superclass 的 `__hash__` 方法 (如果 Superclass 是 `object`，將回退到  id-based hashing)。
{% endhint %}

{% hint style="info" %}
**默認情況下，dataclass() 不會隱式生成 `__hash__` 方法 (無法對實例使用 `hash()`)。 它也不會更改現有的顯式定義的 `__hash__` 方法。**
{% endhint %}

{% hint style="warning" %}
**您不能在 Class 中同時擁有顯式 `__hash__` 方法並設置 `unsafe_hash=True`；這將導致 TypeError。**
{% endhint %}

### 範例 – unsafe\_hash (參數)

***

{% code title="PYTHON" %}

```python
import dataclasses

@dataclasses.dataclass(unsafe_hash=True)
class Person:
    name: str
    age: int
    address: str

p = Person("John", 25, "123 Oak St")
print(p)       # Output: Person(name='John', age=25, address='123 Oak St')
print(hash(p)) # -537977084494215504

# =========================================================
@dataclasses.dataclass(unsafe_hash=True)
class AdvancedExample:
    name: str
    age: int
    mutable_field: dict  # mutable field which normally prevents __hash__()

adv_ex = AdvancedExample("John", 25, {"address": "123 Oak St", "phone": "123-456-7890"})

print(f"Hash of the object: {hash(adv_ex)}")
# Output: TypeError: unhashable type: 'dict'
```

{% endcode %}

## field()

***

該函數用於自定義 Data class 中的各個 fields。就像指定了預設值本身一樣。

{% code title="PYTHON" %}

```python
field(*, 
      default=MISSING, 
      default_factory=MISSING, 
      init=True, 
      repr=True, 
      hash=None, 
      compare=True, 
      metadata=None, 
      kw_only=MISSING)
```

{% endcode %}

<table><thead><tr><th width="184">參數</th><th>說明</th></tr></thead><tbody><tr><td><strong>default</strong></td><td>Field 的預設值。</td></tr><tr><td><strong>default_factory</strong></td><td>為 field 提供預設值的零參數函數，當該 field 需要預設值時將調用。 如果同時指定了 <code>default</code> 和 <code>default_factory</code>，則會引發 <code>TypeError</code>。</td></tr><tr><td><strong>init</strong></td><td>Field 將作為參數包含在生成的 <code>__init__</code> 方法中。</td></tr><tr><td><strong>repr</strong></td><td>Field 將包含在生成的 <code>__repr__</code> 方法，返回的 String 中。</td></tr><tr><td><strong>hash</strong></td><td>如果為 <code>True</code>，則該 field 包含在生成的 <code>__hash__</code> 方法中。<br>如果為 None (預設值)，則使用 <code>compare</code></td></tr><tr><td><strong>compare</strong></td><td>Field 將包含在生成的 <code>__eq__</code>, <code>__gt__</code>... 方法中。</td></tr><tr><td><strong>metadata</strong></td><td>將值包裝在 <a href="https://docs.python.org/3.12/library/types.html#types.MappingProxyType">MappingProxyType()</a> 中，以使其唯讀，並在 field 物件上公開。 它主要用於第三方擴展，Data Classes 本身不使用。</td></tr><tr><td><strong>kw_only</strong></td><td></td></tr></tbody></table>

{% hint style="info" %}
**`MISSING` 是一個** [**Sentinel value**](https://en.wikipedia.org/wiki/Sentinel_value) **物件，用於檢測使用者是否提供了某些參數。**&#x20;

使用此標記是因為 None 對於某些具有不同含義的參數來說是有效值。 任何程式碼都不應直接使用 `MISSING`。
{% endhint %}

### 範例 – 基礎

***

Data class `Employee` 有兩個 fields `name` 和 `age`，這兩個 fields 都具有使用 `dataclasses.field()` 指定的預設值。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, field

@dataclass
class Employee:
    name: str = field(default='John Doe')
    age: int = field(default=25)

employee = Employee()
print(employee)  # Output: Employee(name='John Doe', age=25)
```

{% endcode %}

**default\_factory 參數:** 當您想要生成每次都應該是新物件的預設值時，`default_factory` 非常有用。(例如：空 List 或當前時間戳)

{% code title="PYTHON" %}

```python
from datetime import datetime
from dataclasses import dataclass, field

@dataclass
class LogMessage:
    timestamp: datetime = field(default_factory=datetime.now)
    message: str = field(default="")

log1 = LogMessage()
print(log1)  
# Output: LogMessage(timestamp=datetime.datetime(2023, 7, 21, 2, 18, 12, 994307), message='')

log2 = LogMessage(message="Hello, World!")
print(log2)  
# Output: LogMessage(timestamp=datetime.datetime(2023, 7, 21, 2, 18, 12, 994307), message='Hello, World!')
```

{% endcode %}

**repr 參數:** `color` field 從 `__init__`, `__repr__` 方法中排除。因此在建立 `Point` 實例時不需要提供它，在打印 `Point` 物件時不包括它。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float
    color: str = field(init=False, repr=False, default='White')

p = Point(3.6, 4.8)
print(p)  # Output: Point(x=3.6, y=4.8)
```

{% endcode %}

### 範例 – Mutable 物件

***

如果 field 是 mutable (例如: List, Dictionary)，並且您為其提供預設值，則應該使用 `default_factory` 來提供一個函數，該函數在每次創建新實例時生成新值。 這避免了在多個實例之間共享相同的可變的值。

{% code title="PYTHON" %}

```python
import dataclasses

# Correct usage: default_factory for mutable default value
@dataclasses.dataclass
class Right:
    items: list = dataclasses.field(default_factory=list)

r1 = Right()
r2 = Right()
r1.items.append(1)
print(r1.items)  # Outputs: [1]
print(r2.items)  # Outputs: []

# =========================================================
# No default value
@dataclasses.dataclass
class Right:
    items: list

r1 = Right([])
r2 = Right([])
r1.items.append(1)
print(r1.items)  # Outputs: [1]
print(r2.items)  # Outputs: []

# =========================================================
# Incorrect usage: mutable default value
@dataclasses.dataclass
class Wrong:
    items: list = []
# ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
```

{% endcode %}

### fields()

***

`fields(class_or_instance)` 獲取為特定 Dataclasses 或 Dataclass 實例定義的每個 `Field` 物件的 metadata。

透過使用 `fields()`，我們可以對程式碼中的 Dataclasses 進行更詳細的控制和理解。 這對於大量使用 Dataclasses 的複雜程序或 libraries 特別有用。

{% hint style="info" %}
**雖然 `fields()` 函數提供有關 Dataclass 的有用訊息，但您應該謹慎使用它們，因為訪問 metadata 可能會減慢您的程序速度。 大多數時候，您只需要直接訪問 fields 的值。**
{% endhint %}

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, fields

@dataclass
class Point:
    x: int
    y: int

print(fields(Point))
```

{% endcode %}

**執行結果:**

```txt
(Field(name='x',
       type=<class 'int'>,
       default=<dataclasses._MISSING_TYPE object at 0x000002D0DF83BE50>,
       default_factory=<dataclasses._MISSING_TYPE object at 0x000002D0DF83BE50>,
       init=True,
       repr=True,
       hash=None,
       compare=True,
       metadata=mappingproxy({}),
       kw_only=False,
       _field_type=_FIELD),
 Field(name='y',
       type=<class 'int'>,
       default=<dataclasses._MISSING_TYPE object at 0x000002D0DF83BE50>,
       default_factory=<dataclasses._MISSING_TYPE object at 0x000002D0DF83BE50>,
       init=True,
       repr=True,
       hash=None,
       compare=True,
       metadata=mappingproxy({}),
       kw_only=False,
       _field_type=_FIELD))
```

#### 範例 – 基礎

***

{% code title="PYTHON" %}

```python
import dataclasses

@dataclasses.dataclass
class ComplexClass:
    name: str
    value: int = dataclasses.field(default=42, repr=False)
    list_values: list = dataclasses.field(default_factory=list)

# Create an instance
complex_instance = ComplexClass('example')

# Retrieve and print fields information
for f in dataclasses.fields(ComplexClass):
    print(f'Name: {f.name}, Type: {f.type}, Default: {f.default if f.default is not dataclasses.MISSING else "No default"}, Repr: {f.repr}')

# You can also pass an instance of the class
for f in dataclasses.fields(complex_instance):
    print(f'Name: {f.name}, Type: {f.type}, Default: {f.default if f.default is not dataclasses.MISSING else "No default"}, Repr: {f.repr}')
```

{% endcode %}

**執行結果:**

```
Name: name, Type: <class 'str'>, Default: No default, Repr: True
Name: value, Type: <class 'int'>, Default: 42, Repr: False
Name: list_values, Type: <class 'list'>, Default: No default, Repr: True

Name: name, Type: <class 'str'>, Default: No default, Repr: True
Name: value, Type: <class 'int'>, Default: 42, Repr: False
Name: list_values, Type: <class 'list'>, Default: No default, Repr: True
```

### asdict()

***

`asdict(obj, *, dict_factory=dict)` 每個 Dataclass fields 都轉換為 Dictionary，作為 `name: value` pairs。

透過 `dict_factory`，將 Dataclass `obj` 轉換為 `dict`。(deep copy)

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, asdict

@dataclass
class Point:
    x: int
    y: int

p = Point(10, 20)
print(asdict(p))  # Outputs: {'x': 10, 'y': 20}
```

{% endcode %}

#### 範例 – 具有複雜 field 類型的 Dataclass

***

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, asdict

@dataclass
class Address:
    city: str
    country: str

@dataclass
class Person:
    name: str
    address: Address

person = Person('John', Address('NYC', 'USA'))
print(asdict(person))
```

{% endcode %}

**執行結果:**

```txt
Output: {'name': 'John', 
         'address': {'city': 'NYC', 'country': 'USA'}}
```

`collections.OrderedDict` 保證 fields 的顯示順序與 Dataclass 中定義的順序相同。

{% code title="" %}

```python
from pprint import pprint
from collections import OrderedDict
from dataclasses import dataclass, asdict, field

@dataclass
class Employee:
    name: str
    age: int
    skills: list = field(default_factory=list)

@dataclass
class Company:
    name: str
    employees: list = field(default_factory=list)

company = Company('Example Corp.', 
                  [Employee('John', 30, ['Python', 'Java']), 
                   Employee('Jane', 28, ['Go', 'Rust'])])

pprint(asdict(company))
pprint(asdict(company, dict_factory=OrderedDict))  
```

{% endcode %}

**執行結果:**

```txt
{'employees': [{'age': 30, 'name': 'John', 'skills': ['Python', 'Java']},
               {'age': 28, 'name': 'Jane', 'skills': ['Go', 'Rust']}],
 'name': 'Example Corp.'}

OrderedDict([('name', 'Example Corp.'),
             ('employees',
              [OrderedDict([('name', 'John'),
                            ('age', 30),
                            ('skills', ['Python', 'Java'])]),
               OrderedDict([('name', 'Jane'),
                            ('age', 28),
                            ('skills', ['Go', 'Rust'])])])])
```

#### 範例 – shallow\_copy(obj)

***

`asdict()` 方法創建一個 deep copy，它比 shallow copy 的計算量更大，特別是對於大型 Dataclasses。

如果您知道不需要修改 Dataclass 中的任何物件，並且只需要它的簡單 dictionary 表示形式，則 `shallow_copy(obj)` 可能會更有效。

或者，如果您想要維護 dataclass 實例，您可能需要使用 `shallow_copy(obj)`。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, asdict, fields

# =========================================================
# Deep copy

@dataclass
class Employee:
    name: str
    skills: list

emp = Employee('John', ['Python', 'Java'])
emp_dict = asdict(emp)

# Add a skill to the List
emp.skills.append('Go')
print(emp.skills)          # Output: ['Python', 'Java', 'Go']
print(emp_dict['skills'])  # Output: ['Python', 'Java']

# =========================================================
# Shallow copy

@dataclass
class Example:
    name: str
    skills: list

def shallow_copy(obj):
    return dict((field.name, getattr(obj, field.name)) for field in fields(obj))

ex = Example('John', ['Python', 'Java'])
copied_ex = shallow_copy(ex)

# Add a skill to the List
ex.skills.append('Go')
print(ex.skills)            # Output: ['Python', 'Java', 'Go']
print(copied_ex['skills'])  # Output: ['Python', 'Java', 'Go']
```

{% endcode %}

### astuple()

***

`astuple(obj, *, tuple_factory=tuple)` 每個 Dataclass fields 都轉換為 Tuple。透過 `dict_factory`，將 Dataclass `obj` 轉換為 `tuple`。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, astuple, fields

@dataclass
class Point:
    x: int
    y: int

@dataclass
class Line:
    start: Point
    end: Point

# Shallow copy
def shallow_copy(obj):  
    return tuple(getattr(obj, field.name) for field in fields(obj))

line = Line(Point(0, 0), Point(10, 20))
print(astuple(line))  # Output: ((0, 0), (10, 20))
```

{% endcode %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.xiwind-corp.com/tech/python-library/dataclasses-shu-ju-class.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
