# make\_dataclass(), replace(), is\_dataclass(), \_\_post\_init\_\_

### make\_dataclass()

***

創建一個新的 Dataclass。

此函數並不是嚴格必需的，提供此函數只是為了方便。你可以使用 `dataclass()` 來代替 `make_dataclass()`。

{% code title="PYTHON" %}

```python
make_dataclass(cls_name, 
               fields, *, 
               bases=(), 
               namespace=None, 
               init=True, 
               repr=True, 
               eq=True, 
               order=False, 
               unsafe_hash=False, 
               frozen=False, 
               match_args=True, 
               kw_only=False, 
               slots=False, 
               weakref_slot=False, 
               module=None)
```

{% endcode %}

<table><thead><tr><th width="188">參數</th><th>說明</th></tr></thead><tbody><tr><td><strong>cls_name</strong></td><td>Dataclass 名稱。</td></tr><tr><td><strong>fields</strong></td><td>fields 的定義，是一個 iterable 物件。<br><code>fields</code> 的元素可以是 <code>name</code>、<code>(name, type)</code> 或 <code>(name, type, Field)</code>。如果僅提供 <code>name</code> ，則轉變為 <code>(name, typing.Any)</code> (應用 <code>typing.Any</code>)。</td></tr><tr><td><strong>bases</strong></td><td>為正在建立的 Dataclass 指定多個 Base class。 生成的 Class 將從一個或多個 Base class 繼承。</td></tr><tr><td><strong>namespace</strong></td><td>向 Class 新增其他方法或屬性。<br>使用 <code>namespace</code> 中給定的命名空間進行初始化)。您可以提供一個 Dictionary，其中 Keys 是方法或屬性的名稱，Values 是實際的實現。</td></tr><tr><td><strong>module</strong></td><td>如果定義了<code>module</code>，則 Dataclass 的 <code>__module__</code> 屬性將設置為該值。 預設情況下，設置為調用者的 module 名稱。</td></tr></tbody></table>

{% hint style="info" %}
**`init`、`repr`、`eq`、`order`、`unsafe_hash`、`frozen`、`match_args`、`kw_only`、`slots` 和 `weakref_slot` 與它們在 `dataclass()` 中的含義相同。**
{% endhint %}

{% hint style="success" %}
**當您需要動態創建新 Classes 時，`make_dataclass()` 會非常有用。 對於靜態定義的 Classes，使用 `@dataclass` 可以更具可讀性。**
{% endhint %}

{% hint style="warning" %}
**`bases` 和 `namespace` 參數可以成為動態創建複雜 Dataclasses 的強大工具。 但是，它們也會使您的程式碼更難以理解和 debug。**&#x20;

如果您熟悉傳統的 Python Class 和繼承，在許多情況下，您最好使用 `@dataclass` 和傳統的 Class 來定義。
{% endhint %}

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, make_dataclass

# =========================================================
# Creating a simple dataclass
Person = make_dataclass('Person', ['name', 'age', 'city'])

p = Person('John', 30, 'New York')
print(p)  # Output: Person(name='John', age=30, city='New York')

# =========================================================
# Use @dataclass
@dataclass
class Person:
    name: str
    age: int
    city: str

p = Person('John', 30, 'New York')
print(p)  # Output: Person(name='John', age=30, city='New York')
```

{% endcode %}

#### 範例 – 基礎

***

{% code title="PYTHON" %}

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

# =========================================================
fields = [
    ('name', str, field(default='Unknown')),
    ('age', int, field(default=0)),
    ('city', str, field(default='Unknown')),]

namespace = {
    'greet': lambda self: f"Hello, I'm {self.name} from {self.city}!"}

Person = make_dataclass('Person', fields, namespace=namespace)

p = Person('John', 30, 'New York')
print(p.greet())  # Output: Hello, I'm John from New York!

p = Person()
print(p.greet())  # Output: Hello, I'm Unknown from Unknown!

# =========================================================
# Use @dataclass
@dataclass
class Person:
    name: str = field(default='Unknown')
    age: int = field(default=0)
    city: str = field(default='Unknown')

    def greet(self):
        return f"Hello, I'm {self.name} from {self.city}!"

p = Person('John', 30, 'New York')
print(p.greet())  # Output: Hello, I'm John from New York!

p = Person()
print(p.greet())  # Output: Hello, I'm Unknown from Unknown!
```

{% endcode %}

#### 範例 – bases, namespace (參數)

***

{% code title="PYTHON" %}

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

# =========================================================
# Define a base class
class Mammal:
    def breathe(self):
        return "I can breathe!"

# Define the fields for the new dataclass
fields = [('name', str, field(default='Unknown'))]

# Define additional methods
namespace = {
    'greet': lambda self: f"Hello, I'm {self.name}!",}

# Create a new dataclass that inherits from Mammal
Person = make_dataclass('Person', fields, bases=(Mammal,), namespace=namespace)

p = Person('John')
print(p.greet())    # Output: Hello, I'm John!
print(p.breathe())  # Output: I can breathe!

# =========================================================
# Use @dataclass
class Mammal:
    def breathe(self):
        return "I can breathe!"

@dataclass
class Person(Mammal):
    name: str = field(default='Unknown')

    def greet(self):
        return f"Hello, I'm {self.name}!"

p = Person('John')
print(p.greet())    # Output: Hello, I'm John!
print(p.breathe())  # Output: I can breathe!

```

{% endcode %}

### replace()

***

建立一個新物件實例，與 `obj` 類型相同，且用 `changes` 中的值替換 fields。

{% code title="PYTHON" %}

```python
replace(obj, /, **changes)
```

{% endcode %}

當您想要建立新的實例改變某些 fields，但希望保持原始 Dataclass 實例不變時，`replace()` 函數非常有用。

例如：如果您有一個 Dataclass 實例，表示特定時間點某事物的狀態 ，並且您想要表示一個新狀態。但您不想更改原始實例，而是建立一個表示新狀態的新實例。

{% hint style="info" %}
**新返回的物件是透過調用 dataclass 的 `__init__()` 方法創建的。 這確保了`__post_init__` (如果存在)也會被調用。**
{% endhint %}

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, replace

@dataclass
class Container:
    values: list

c = Container(values=[1, 2, 3, 4])

# Replace the 'values' list in the dataclass
c1 = replace(c, values=[10, 20, 30, 40])

print(c)   # Output: Container(values=[1, 2, 3, 4])
print(c1)  # Output: Container(values=[10, 20, 30, 40])
```

{% endcode %}

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, replace

@dataclass
class Point:
    x: int
    y: int

@dataclass
class Line:
    start: Point
    end: Point

line1 = Line(start=Point(0, 0), 
            end=Point(10, 10))

# Replace the 'start' point of the line
line2 = replace(line1, start=Point(5, 5))

print(line1)  # Output: Line(start=Point(x=0, y=0), end=Point(x=10, y=10))
print(line2)  # Output: Line(start=Point(x=5, y=5), end=Point(x=10, y=10))
```

{% endcode %}

#### 範例 – init=False

***

在 Python 的 `dataclasses` 中，`init=False` field 是一個不打算作為參數傳遞給產生的 `__init__` 方法的 field。相反，預計該欄位將在 `__post_init__` 方法中，設定或保留為指定的預設值。

但是，當使用 `dataclasses.replace()` (目的在建立一個新物件作為現有物件的副本，並將某些欄位替換為新值) 時，`init=False` field 的行為有所不同。

它們不會從 "source" 物件複製。相反，它們將被設定為就像正在創建一個新物件一樣，這意味著它們將是：

1. 如果提供了預設值，則保留預設值。
2. 如果設定了它們，則在 `__post_init__` 中進行設定。
3. 未初始化。(如果以上都不成立)

{% code title="PYTHON" %}

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

@dataclass
class Product:
    name: str
    category: str = "General"  # This field has a default value and uses the init constructor
    id: int = field(default=None, init=False)  # This field is not included in the init constructor

    def __post_init__(self):
        if self.id is None:  # Initialize the id if not already set
            self.id = self.generate_id()

    def generate_id(self):
        # In a real scenario, this would generate a unique ID.
        return hash((self.name, self.category))


product = Product("Table")
print(product)      # Output: Product(name='Table', category='General', id=some_hash)

new_product = replace(product, name="Chair")
print(new_product)  # Output: Product(name='Chair', category='General', id=different_hash)
```

{% endcode %}

在這個例子中：

* `id` 是一個 `init=False` field。實例化後，呼叫 `__post_init__`，它設定 `id`。
* 當呼叫 `replace(product, name="Chair")` 時，會有效地建立一個新的 `Product` 實例，並將 `name` 設定為 `Chair`。 `id` 不是從 `product` 複製的，而是再次呼叫 `__post_init__`，根據新的 `name` 產生新的 `id`。

如果您期望 `replace()` 從原始物件複製所有 fields，這種行為可能會令人驚訝。因此，在使用 `init=False` fields 時，您應該意識到這一點。

如果您想要不同的行為，例如明確複製 `init=False` 字段，您可以提供自訂替換方法。自訂 `replace` 方法可確保 `init=False` field (本例中為 `id`) 被轉移到新實例，除非它被明確變更。

{% code title="PYTHON" %}

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

@dataclass
class Product:
    name: str
    category: str = "General"  # This field has a default value and uses the init constructor
    id: int = field(default=None, init=False)  # This field is not included in the init constructor

    def __post_init__(self):
        if self.id is None:  # Initialize the id if not already set
            self.id = self.generate_id()

    def generate_id(self):
        # In a real scenario, this would generate a unique ID.
        return hash((self.name, self.category))

    def custom_replace(self, **changes):
        # Manually create a copy of the object
        new_obj = Product(self.name, self.category)
        new_obj.id = self.id  # Explicitly copy the `init=False` field

        # Apply any requested changes
        for attr, value in changes.items():
            setattr(new_obj, attr, value)
        
        # Manually call __post_init__ if necessary
        new_obj.__post_init__()
        return new_obj


product = Product("Table")
print(product)      # Output: Product(name='Table', category='General', id=some_hash)

new_product = product.custom_replace(name="Chair")
print(new_product)  # Output: Product(name='Chair', category='General', id=same_hash_as_product)
```

{% endcode %}

### is\_dataclass()

***

`is_dataclass(obj)` 檢查給定的 `obj` 物件是否是 Dataclass 實例或 Dataclass 本身。 如果 `obj` 物件是 Dataclass 或 Dataclass 實例，則返回 `True`。

如果您需要知道一個 Class 是否是 Dataclass 的實例，而不是 Dataclass 本身。請進一步檢查 `isinstance(obj, type)`，即 `is_dataclass_instance(obj)`。

{% code title="PYTHON" %}

```python
def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)
```

{% endcode %}

**範例：**

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, is_dataclass

@dataclass
class Person:
    name: str
    age: int

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)

p = Person('John Doe', 30)

# Checking if the class is a dataclass or an instance of a dataclass
print(is_dataclass(Person))  # Output: True
print(is_dataclass(p))       # Output: True

print(is_dataclass_instance(Person))  # Output: False
print(is_dataclass_instance(p))       # Output: True
```

{% endcode %}

## \_\_post\_init\_\_

***

此方法在調用 data class 的 `__init__` 方法後立即調用，為初始化後處理提供方便的 hook。

這對於執行其他屬性初始化、驗證或建立 dataclass 的新實例時，需要進行的任何其他自訂設定非常有用。

**`__post_init__` 的用法：**

1. **初始化驗證：** 在物件初始化後使用 `__post_init__` 來驗證資料。在範例中，它檢查 `unit_price` 是否為負數，如果是則引發 `ValueError`。
2. **派生屬性：** 計算從傳遞給 `__init__` 方法的屬性派生的屬性。在範例中，`total_price` 是根據 `unit_price` 和 `quantity_on_hand` 計算得出的。
3. **類型檢查：** 執行自訂類型檢查。該範例確保 `serial_numbers` 是一個列表，如果不是，則會引發 `TypeError`。
4. **預設可變性：** 當使用可變預設值（例如: Lists, Dictionaries）時，使用 `default_factory`。這是因為在使用可變預設值時，預設值在 class 的所有實例之間共享，從而導致潛在的錯誤。
5. **副作用：** 如果您需要在建立實例後執行一些副作用， `__post_init__` 是一個好地方。(例如: logging (日誌記錄)、在某些 registry (註冊表) 中註冊等)
6. **Field 初始化：** 有時你需要設定不是 fields 的實例變數 (未在 class 中定義)。 `__post_init__` 可用來設定這些特定於實例的屬性，這些屬性不需要是 fields。
7. **附加屬性：** 也可以為 dataclass 實例新增未定義為 fields 的附加屬性，在範例中，為 `total_price`。
8. **Non-Field 預設值：** 對於初始化不應作為參數傳遞給 `__init__` 方法的屬性，您可以在`__post_init__` 中指派它們，可以使用直接值或基於其他 fields 的計算。

透過使用 `__post_init__`，您可以確保 data class 實例，在應用程式的其餘部分使用之前，始終處於有效狀態，從而使其成為強大且自我驗證的資料模型的強大功能。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, field
from typing import List, Any

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0
    serial_numbers: List[str] = field(default_factory=list)
    
    def __post_init__(self):
        if self.unit_price < 0:
            raise ValueError("Unit price cannot be negative")
        # Automatically calculate total price and validate
        self.total_price: float = self.unit_price * self.quantity_on_hand
        # Perform type checking, raise error if serial_numbers is not a list
        if not isinstance(self.serial_numbers, list):
            raise TypeError("serial_numbers must be a list of strings")

    def add_serial_number(self, serial_number: str):
        self.serial_numbers.append(serial_number)

# Example usage
try:
    item = InventoryItem("Widget", 10.0, 100)
    item.add_serial_number("SN001")
    print(item)
except ValueError as e:
    print(e)
except TypeError as e:
    print(e)

# Output: InventoryItem(name='Widget', unit_price=10.0, quantity_on_hand=100, serial_numbers=['SN001'])
```

{% endcode %}

## MISSING, KW\_ONLY

***

* `MISSING` [Sentinel value](https://en.wikipedia.org/wiki/Sentinel_value) 僅提供 `default` 和 `default_factory` 設置預設值。

我們應該避免在應用程序代碼中使用 `MISSING` 。`MISSING` 僅在 `dataclasses` module 幕後使用，並發揮它魔法。

{% hint style="info" %}
**fields 的預設值是 `default=MISSING`，而不是 `default=None`。**&#x20;

這樣做主要是為了確定使用者是否實際將 `default` 或 `default_factory` 的值傳遞給工廠函數 `fields`。&#x20;

傳遞 `field(default=None)` 是完全有效的； 由於預設值實際上是 `MISSING`，因此`dataclasses` 能夠檢測到此參數已經傳遞了一個 `None` 值。詳見： [field()](https://docs.xiwind-corp.com/tech/python-library/dataclasses-shu-ju-class/..#field-1)。
{% endhint %}

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class Var:
    get: list[str]
    set: Optional[list[str]] = field(default=None)


print(Var(get=['Title'])) # Output: Var(get=['Title'], set=None)
```

{% endcode %}

* `KW_ONLY` Sentinel value 是一種用於指定 Dataclass 的某些 fields 應為 keyword-only arguments。

對於 Dataclass，這意味著在創建 Dataclass 的實例時，必須使用 keyword syntax 提供這些參數。 當您有許多參數時，這對於增強程式碼可讀性和防止錯誤非常有用。

`KW_ONLY` (pseudo-field) 指示 keyword-only fields 的開始，在此之後定義的任何 fields 都將被視為 keyword-only。 該 pseudo-field 的名稱將被忽略，並且按照慣例，使用 `_`。

{% code title="PYTHON" %}

```python
from dataclasses import dataclass, KW_ONLY

@dataclass
class Student:
    name: str
    _: KW_ONLY  # Pseudo-field to indicate the start of keyword-only fields
    age: int = 20
    course: str = 'Computer Science'
    
# This is OK
student1 = Student('John Doe', age=21, course='Mathematics')

# This is OK - using defaults for keyword-only arguments
student2 = Student('Jane Doe')

student3 = Student('John Doe', 21, 'Mathematics')
# TypeError: Student.__init__() takes 2 positional arguments but 4 were given
```

{% endcode %}

## 參考資料

***

[dataclasses — Data Classes — Python 3.12.0b4 documentation](https://docs.python.org/3.12/library/dataclasses.html)

[PEP 526 – Syntax for Variable Annotations | peps.python.org](https://peps.python.org/pep-0526/)

[PEP 557 – Data Classes | peps.python.org](https://peps.python.org/pep-0557/)
