คลาสข้อมูลคืออะไรและแตกต่างจากคลาสทั่วไปอย่างไร


143

ด้วยคลาสข้อมูลPEP 557จะถูกนำมาใช้ในไลบรารีมาตรฐาน python

พวกเขาใช้ประโยชน์จาก@dataclassมัณฑนากรและควรจะเป็น "mutable namedtuples with default" แต่ฉันไม่แน่ใจจริงๆว่าฉันเข้าใจว่าสิ่งนี้หมายถึงอะไรและแตกต่างจากคลาสทั่วไปอย่างไร

คลาสข้อมูล python คืออะไรและควรใช้เมื่อใด


8
ด้วยเนื้อหาที่ครอบคลุมของ PEP คุณต้องการทราบอะไรอีกบ้าง namedtuples ไม่เปลี่ยนรูปและไม่สามารถมีค่าเริ่มต้นสำหรับแอตทริบิวต์ในขณะที่คลาสข้อมูลไม่แน่นอนและสามารถมีได้
jonrsharpe

31
@jonrsharpe ดูเหมือนมีเหตุผลสำหรับฉันที่ควรมีเธรด stackoverflow ในเรื่องนี้ Stackoverflow หมายถึงสารานุกรมในรูปแบบ Q&A ใช่หรือไม่? คำตอบคือไม่ "เพียงแค่ดูในเว็บไซต์อื่นนี้" ไม่น่าจะมีการโหวตดาวน์ที่นี่
Luke Davis

12
มีห้าเธรดเกี่ยวกับวิธีการต่อท้ายรายการเข้ากับรายการ คำถามหนึ่งข้อ@dataclassจะไม่ทำให้เว็บไซต์สลายตัว
eric

2
@jonrsharpe namedtuplesสามารถมีค่าเริ่มต้น ดูได้ที่นี่: stackoverflow.com/questions/11351032/…
MJB

คำตอบ:


153

คลาสข้อมูลเป็นเพียงคลาสปกติที่มุ่งเน้นไปที่สถานะการจัดเก็บมากกว่าที่มีตรรกะจำนวนมาก ทุกครั้งที่คุณสร้างคลาสที่ส่วนใหญ่ประกอบด้วยแอตทริบิวต์ที่คุณสร้างคลาสข้อมูล

สิ่งที่dataclassesโมดูลทำคือช่วยให้สร้างคลาสข้อมูลได้ง่ายขึ้น ดูแลจานหม้อไอน้ำจำนวนมากสำหรับคุณ

นี่เป็นสิ่งสำคัญอย่างยิ่งเมื่อชั้นข้อมูลของคุณต้องสามารถล้างได้ สิ่งนี้ต้องใช้__hash__วิธีการและ__eq__วิธีการ หากคุณเพิ่ม__repr__วิธีการที่กำหนดเองเพื่อความสะดวกในการดีบักสิ่งนั้นอาจกลายเป็นเรื่องละเอียด:

class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def __init__(
            self, 
            name: str, 
            unit_price: float,
            quantity_on_hand: int = 0
        ) -> None:
        self.name = name
        self.unit_price = unit_price
        self.quantity_on_hand = quantity_on_hand

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

    def __repr__(self) -> str:
        return (
            'InventoryItem('
            f'name={self.name!r}, unit_price={self.unit_price!r}, '
            f'quantity_on_hand={self.quantity_on_hand!r})'

    def __hash__(self) -> int:
        return hash((self.name, self.unit_price, self.quantity_on_hand))

    def __eq__(self, other) -> bool:
        if not isinstance(other, InventoryItem):
            return NotImplemented
        return (
            (self.name, self.unit_price, self.quantity_on_hand) == 
            (other.name, other.unit_price, other.quantity_on_hand))

ด้วยdataclassesคุณสามารถลดเป็น:

from dataclasses import dataclass

@dataclass(unsafe_hash=True)
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

มัณฑนากรชั้นเดียวกันยังสามารถสร้างวิธีการเปรียบเทียบ ( __lt__, __gt__ฯลฯ ) และจับเปลี่ยนไม่ได้

namedtupleคลาสยังเป็นคลาสข้อมูล แต่ไม่เปลี่ยนรูปตามค่าเริ่มต้น (เช่นเดียวกับการจัดลำดับ) dataclassesมีความยืดหยุ่นมากกว่าในเรื่องนี้และสามารถจัดโครงสร้างได้ง่ายเพื่อให้สามารถเติมเต็มบทบาทเดียวกันกับnamedtupleชั้นเรียนได้

PEP ได้รับแรงบันดาลใจจากattrsโครงการซึ่งสามารถทำอะไรได้มากกว่านั้น (รวมถึงสล็อตตัวตรวจสอบตัวแปลงข้อมูลเมตา ฯลฯ )

หากคุณต้องการดูตัวอย่างบางส่วนฉันเพิ่งใช้dataclassesเวลาหลายของฉันจุติของรหัสการแก้ปัญหาดูวิธีแก้ปัญหาสำหรับวัน 7 , วันที่ 8 , วันที่ 11และ20 วัน

หากคุณต้องการใช้dataclassesโมดูลใน Python เวอร์ชัน <3.7 คุณสามารถติดตั้งโมดูล backported (ต้องใช้ 3.6) หรือใช้attrsโครงการที่กล่าวถึงข้างต้น


2
ในตัวอย่างแรกคุณตั้งใจซ่อนสมาชิกชั้นเรียนที่มีสมาชิกชื่อเดียวกันหรือไม่ โปรดช่วยทำความเข้าใจสำนวนนี้
VladimirLenin

4
@VladimirLenin: ไม่มีแอตทริบิวต์คลาสมีเฉพาะคำอธิบายประกอบประเภท ดูPEP 526โดยเฉพาะระดับและคำอธิบายประกอบเช่นตัวแปรส่วน
Martijn Pieters

1
@Bananach: @dataclassสร้าง__init__วิธีการเดียวกันโดยมีquantity_on_handอาร์กิวเมนต์คำหลักที่มีค่าเริ่มต้น เมื่อคุณสร้างอินสแตนซ์มันจะตั้งค่าquantity_on_handแอตทริบิวต์อินสแตนซ์เสมอ ดังนั้นตัวอย่างแรกที่ไม่ใช่ dataclass ของฉันใช้รูปแบบเดียวกันเพื่อสะท้อนว่าโค้ดที่สร้าง dataclass จะทำอะไร
Martijn Pieters

1
@Bananach: ดังนั้นในตัวอย่างแรกเราสามารถละเว้นการตั้งค่าแอตทริบิวต์อินสแตนซ์และไม่เงาแอตทริบิวต์คลาสได้ แต่เป็นการตั้งค่าซ้ำซ้อนในแง่นั้น แต่ dataclasses จะตั้งค่าไว้
Martijn Pieters

1
@ user2853437 กรณีการใช้งานของคุณไม่รองรับ dataclasses จริงๆ บางทีคุณอาจจะดีกว่าการใช้ญาติที่ใหญ่กว่า dataclasses' attrs โปรเจ็กต์นั้นรองรับตัวแปลงต่อฟิลด์ที่ให้คุณปรับค่าฟิลด์ให้เป็นปกติ ถ้าคุณต้องการติดกับ dataclasses ใช่ให้ทำการ normalization ใน__post_init__วิธีการนี้
Martijn Pieters

64

ภาพรวม

คำถามได้รับการแก้ไขแล้ว อย่างไรก็ตามคำตอบนี้ได้เพิ่มตัวอย่างที่ใช้ได้จริงเพื่อช่วยในการทำความเข้าใจพื้นฐานเกี่ยวกับแว่นตาข้อมูล

คลาสข้อมูล python คืออะไรและควรใช้เมื่อใด

  1. เครื่องกำเนิดรหัส : สร้างรหัสสำเร็จรูป; คุณสามารถเลือกใช้วิธีพิเศษในคลาสปกติหรือให้คลาสดาต้าใช้โดยอัตโนมัติ
  2. ภาชนะบรรจุข้อมูล : โครงสร้างที่ข้อมูลถือ (เช่น tuples และ dicts) มักจะมีประเข้าถึงแอตทริบิวต์เช่นการเรียนnamedtupleและอื่น ๆ

"mutable namedtuples โดยมีค่าเริ่มต้น [s]"

นี่คือความหมายของวลีหลัง:

  • เปลี่ยนแปลงไม่ได้ : โดยค่าเริ่มต้นแอตทริบิวต์ dataclass สามารถกำหนดใหม่ได้ คุณสามารถเลือกที่จะทำให้ไม่เปลี่ยนรูปได้ (ดูตัวอย่างด้านล่าง)
  • namedtuple : คุณมีจุดการเข้าถึงแอตทริบิวต์เช่น a namedtupleหรือคลาสปกติ
  • ค่าเริ่มต้น : คุณสามารถกำหนดค่าเริ่มต้นให้กับแอตทริบิวต์

เมื่อเทียบกับคลาสทั่วไปคุณจะประหยัดค่าใช้จ่ายในการพิมพ์โค้ดสำเร็จรูปเป็นหลัก


คุณสมบัติ

นี่คือภาพรวมของคุณลักษณะ dataclass (TL; DR หรือไม่ดูตารางสรุปในหัวข้อถัดไป)

สิ่งที่คุณได้รับ

นี่คือคุณสมบัติที่คุณจะได้รับโดยค่าเริ่มต้นจาก dataclasses

คุณสมบัติ + การเป็นตัวแทน + การเปรียบเทียบ

import dataclasses


@dataclasses.dataclass
#@dataclasses.dataclass()                                       # alternative
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

ค่าเริ่มต้นเหล่านี้มีให้โดยการตั้งค่าคำสำคัญต่อไปนี้เป็นTrue:

@dataclasses.dataclass(init=True, repr=True, eq=True)

สิ่งที่คุณสามารถเปิดได้

Trueคุณสมบัติเพิ่มเติมที่มีอยู่หากคำหลักที่เหมาะสมมีการกำหนดให้

ใบสั่ง

@dataclasses.dataclass(order=True)
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

ขณะนี้มีการใช้วิธีการสั่งซื้อ (ตัวดำเนินการมากเกินไป :) < > <= >=เช่นเดียวfunctools.total_orderingกับการทดสอบความเท่าเทียมกันที่แข็งแกร่งขึ้น

แฮส, เปลี่ยนแปลงได้

@dataclasses.dataclass(unsafe_hash=True)                        # override base `__hash__`
class Color:
    ...

แม้ว่าวัตถุอาจเปลี่ยนแปลงได้ (อาจไม่ต้องการ) แต่ก็มีการใช้แฮช

แฮสได้ไม่เปลี่ยนรูป

@dataclasses.dataclass(frozen=True)                             # `eq=True` (default) to be immutable 
class Color:
    ...

ขณะนี้มีการใช้แฮชและไม่อนุญาตให้เปลี่ยนอ็อบเจ็กต์หรือกำหนดให้กับแอ็ตทริบิวต์

โดยรวมแล้ววัตถุนั้นสามารถแฮชได้ถ้าอย่างใดอย่างหนึ่งunsafe_hash=Trueหรือfrozen=True.

ดูตารางลอจิกการแฮชต้นฉบับพร้อมรายละเอียดเพิ่มเติม

สิ่งที่คุณไม่ได้รับ

ในการรับคุณสมบัติดังต่อไปนี้ต้องใช้วิธีพิเศษด้วยตนเอง:

แกะกล่อง

@dataclasses.dataclass
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

    def __iter__(self):
        yield from dataclasses.astuple(self)

การเพิ่มประสิทธิภาพ

@dataclasses.dataclass
class SlottedColor:
    __slots__ = ["r", "b", "g"]
    r : int
    g : int
    b : int

ตอนนี้ขนาดวัตถุลดลง:

>>> imp sys
>>> sys.getsizeof(Color)
1056
>>> sys.getsizeof(SlottedColor)
888

ในบางสถานการณ์__slots__ยังช่วยเพิ่มความเร็วในการสร้างอินสแตนซ์และการเข้าถึงแอตทริบิวต์ นอกจากนี้ช่องไม่อนุญาตให้มีการกำหนดค่าเริ่มต้น มิฉะนั้น a ValueErrorจะถูกยกขึ้น

ดูเพิ่มเติมเกี่ยวกับสล็อตในบล็อกโพสต์นี้


ตารางสรุป

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       Feature        |       Keyword        |                      Example                       |           Implement in a Class          |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| Attributes           |  init                |  Color().r -> 0                                    |  __init__                               |
| Representation       |  repr                |  Color() -> Color(r=0, g=0, b=0)                   |  __repr__                               |
| Comparision*         |  eq                  |  Color() == Color(0, 0, 0) -> True                 |  __eq__                                 |
|                      |                      |                                                    |                                         |
| Order                |  order               |  sorted([Color(0, 50, 0), Color()]) -> ...         |  __lt__, __le__, __gt__, __ge__         |
| Hashable             |  unsafe_hash/frozen  |  {Color(), {Color()}} -> {Color(r=0, g=0, b=0)}    |  __hash__                               |
| Immutable            |  frozen + eq         |  Color().r = 10 -> TypeError                       |  __setattr__, __delattr__               |
|                      |                      |                                                    |                                         |
| Unpacking+           |  -                   |  r, g, b = Color()                                 |   __iter__                              |
| Optimization+        |  -                   |  sys.getsizeof(SlottedColor) -> 888                |  __slots__                              |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+

+วิธีการเหล่านี้ไม่ได้สร้างขึ้นโดยอัตโนมัติและต้องใช้งานด้วยตนเองใน dataclass

* __ne__ไม่จำเป็นจึงไม่ได้ดำเนินการ


คุณลักษณะเพิ่มเติม

โพสต์เริ่มต้น

@dataclasses.dataclass
class RGBA:
    r : int = 0
    g : int = 0
    b : int = 0
    a : float = 1.0

    def __post_init__(self):
        self.a : int =  int(self.a * 255)


RGBA(127, 0, 255, 0.5)
# RGBA(r=127, g=0, b=255, a=127)

มรดก

@dataclasses.dataclass
class RGBA(Color):
    a : int = 0

การแปลง

แปลง dataclass เป็น tuple หรือ dict แบบวนซ้ำ :

>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{r: 128, g: 0, b: 255}

ข้อ จำกัด


อ้างอิง

  • คำพูดของR.Hettingerเกี่ยวกับDataclasses: ตัวสร้างรหัสเพื่อยุติการสร้างโค้ดทั้งหมด
  • คำพูดของ T. Hunner ในเรื่องEasier Classes: Python Classes Without All the Cruft
  • เอกสารของ Python เกี่ยวกับรายละเอียดการแฮช
  • คำแนะนำของ Real Python เกี่ยวกับThe Ultimate Guide to Data Classes ใน Python 3.7
  • บล็อกโพสต์ของ A. Shaw เกี่ยวกับการแนะนำคลาสข้อมูล Python 3.7 โดยย่อ
  • ที่เก็บ githubของE.Smithบนdataclasses

2

จากข้อกำหนด PEP :

มีการจัดเตรียมมัณฑนากรชั้นเรียนซึ่งตรวจสอบนิยามคลาสสำหรับตัวแปรที่มีคำอธิบายประกอบประเภทตามที่กำหนดใน PEP 526 "ไวยากรณ์สำหรับคำอธิบายประกอบตัวแปร" ในเอกสารนี้ตัวแปรดังกล่าวเรียกว่าเขตข้อมูล การใช้ฟิลด์เหล่านี้มัณฑนากรจะเพิ่มนิยามเมธอดที่สร้างขึ้นให้กับคลาสเพื่อรองรับการเริ่มต้นอินสแตนซ์การทำซ้ำวิธีการเปรียบเทียบและวิธีอื่น ๆ ที่เป็นทางเลือกตามที่อธิบายไว้ในส่วนข้อกำหนด คลาสดังกล่าวเรียกว่าคลาสข้อมูล แต่ไม่มีอะไรพิเศษเกี่ยวกับคลาส: มัณฑนากรเพิ่มเมธอดที่สร้างขึ้นให้กับคลาสและส่งคืนคลาสเดียวกันกับคลาสที่ได้รับ

@dataclassกำเนิดเพิ่มวิธีการในชั้นเรียนที่คุณมิฉะนั้นจะกำหนดตัวเองชอบ__repr__, __init__, และ__lt____gt__


2

ลองพิจารณาคลาสง่ายๆนี้ Foo

from dataclasses import dataclass
@dataclass
class Foo:    
    def bar():
        pass  

นี่คือการdir()เปรียบเทียบในตัว ทางซ้ายมือจะเป็นห้องที่Fooไม่มีมัณฑนากร @dataclass และทางขวามือมีมัณฑนากร @dataclass

ใส่คำอธิบายภาพที่นี่

นี่คือความแตกต่างอีกประการหนึ่งหลังจากใช้inspectโมดูลเพื่อเปรียบเทียบ

ใส่คำอธิบายภาพที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.