Page cover

make_dataclass()


創建一個新的 Dataclass。

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

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)
參數
說明

cls_name

Dataclass 名稱。

fields

fields 的定義,是一個 iterable 物件。 fields 的元素可以是 name(name, type)(name, type, Field)。如果僅提供 name ,則轉變為 (name, typing.Any) (應用 typing.Any)。

bases

為正在建立的 Dataclass 指定多個 Base class。 生成的 Class 將從一個或多個 Base class 繼承。

namespace

向 Class 新增其他方法或屬性。 使用 namespace 中給定的命名空間進行初始化)。您可以提供一個 Dictionary,其中 Keys 是方法或屬性的名稱,Values 是實際的實現。

module

如果定義了module,則 Dataclass 的 __module__ 屬性將設置為該值。 預設情況下,設置為調用者的 module 名稱。

initrepreqorderunsafe_hashfrozenmatch_argskw_onlyslotsweakref_slot 與它們在 dataclass() 中的含義相同。

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')

範例 – 基礎


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!

範例 – bases, namespace (參數)


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!

replace()


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

PYTHON
replace(obj, /, **changes)

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

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

新返回的物件是透過調用 dataclass 的 __init__() 方法創建的。 這確保了__post_init__ (如果存在)也會被調用。

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])
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))

範例 – init=False


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

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

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

  1. 如果提供了預設值,則保留預設值。

  2. 如果設定了它們,則在 __post_init__ 中進行設定。

  3. 未初始化。(如果以上都不成立)

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)

在這個例子中:

  • id 是一個 init=False field。實例化後,呼叫 __post_init__,它設定 id

  • 當呼叫 replace(product, name="Chair") 時,會有效地建立一個新的 Product 實例,並將 name 設定為 Chairid 不是從 product 複製的,而是再次呼叫 __post_init__,根據新的 name 產生新的 id

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

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

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)

is_dataclass()


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

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

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

範例:

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

__post_init__


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

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

__post_init__ 的用法:

  1. 初始化驗證: 在物件初始化後使用 __post_init__ 來驗證資料。在範例中,它檢查 unit_price 是否為負數,如果是則引發 ValueError

  2. 派生屬性: 計算從傳遞給 __init__ 方法的屬性派生的屬性。在範例中,total_price 是根據 unit_pricequantity_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 實例,在應用程式的其餘部分使用之前,始終處於有效狀態,從而使其成為強大且自我驗證的資料模型的強大功能。

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'])

MISSING, KW_ONLY


  • MISSING Sentinel value 僅提供 defaultdefault_factory 設置預設值。

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

fields 的預設值是 default=MISSING,而不是 default=None

這樣做主要是為了確定使用者是否實際將 defaultdefault_factory 的值傳遞給工廠函數 fields

傳遞 field(default=None) 是完全有效的; 由於預設值實際上是 MISSING,因此dataclasses 能夠檢測到此參數已經傳遞了一個 None 值。詳見: field()

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)

  • 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 的名稱將被忽略,並且按照慣例,使用 _

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

參考資料


dataclasses — Data Classes — Python 3.12.0b4 documentation

PEP 526 – Syntax for Variable Annotations | peps.python.org

PEP 557 – Data Classes | peps.python.org

Last updated

Was this helpful?