Kế thừa lớp trong dữ liệu Python 3.7


84

Tôi hiện đang thử các cấu trúc dataclass mới được giới thiệu trong Python 3.7. Tôi hiện đang cố gắng thực hiện một số kế thừa của một lớp cha. Có vẻ như thứ tự của các đối số bị sai bởi cách tiếp cận hiện tại của tôi sao cho tham số bool trong lớp con được truyền trước các tham số khác. Điều này gây ra lỗi loại.

from dataclasses import dataclass

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f'The Name is {self.name} and {self.name} is {self.age} year old')

@dataclass
class Child(Parent):
    school: str
    ugly: bool = True


jack = Parent('jack snr', 32, ugly=True)
jack_son = Child('jack jnr', 12, school = 'havard', ugly=True)

jack.print_id()
jack_son.print_id()

Khi tôi chạy mã này, tôi nhận được TypeError:

TypeError: non-default argument 'school' follows default argument

Làm cách nào để sửa lỗi này?

Câu trả lời:


124

Cách dataclasses kết hợp các thuộc tính ngăn bạn không thể sử dụng các thuộc tính có giá trị mặc định trong lớp cơ sở và sau đó sử dụng các thuộc tính không có mặc định (thuộc tính vị trí) trong lớp con.

Đó là bởi vì các thuộc tính được kết hợp bằng cách bắt đầu từ cuối MRO và xây dựng một danh sách có thứ tự các thuộc tính theo thứ tự nhìn thấy đầu tiên; ghi đè được giữ ở vị trí ban đầu của chúng. Vì vậy, hãy Parentbắt đầu với ['name', 'age', 'ugly'], nơi uglycó mặc định, và sau đó Childthêm ['school']vào cuối danh sách đó (với uglyđã có trong danh sách). Điều này có nghĩa là bạn kết thúc với ['name', 'age', 'ugly', 'school']và bởi vì schoolkhông có mặc định, điều này dẫn đến danh sách đối số không hợp lệ cho __init__.

Điều này được ghi lại trong PEP-557 Dataclasses , dưới sự kế thừa :

Khi Lớp dữ liệu đang được tạo bởi người @dataclasstrang trí, nó sẽ xem xét tất cả các lớp cơ sở của lớp trong MRO ngược lại (nghĩa là bắt đầu từ object) và đối với mỗi Lớp dữ liệu mà nó tìm thấy, thêm các trường từ lớp cơ sở đó vào một thứ tự lập bản đồ các lĩnh vực. Sau khi tất cả các trường của lớp cơ sở được thêm vào, nó sẽ thêm các trường của chính nó vào ánh xạ có thứ tự. Tất cả các phương thức được tạo sẽ sử dụng ánh xạ có thứ tự được tính toán kết hợp này của các trường. Vì các trường theo thứ tự chèn, các lớp dẫn xuất sẽ ghi đè các lớp cơ sở.

và dưới Đặc điểm kỹ thuật :

TypeErrorsẽ được nâng lên nếu một trường không có giá trị mặc định theo sau một trường có giá trị mặc định. Điều này đúng khi điều này xảy ra trong một lớp đơn lẻ hoặc là kết quả của việc kế thừa lớp.

Bạn có một số tùy chọn ở đây để tránh vấn đề này.

Tùy chọn đầu tiên là sử dụng các lớp cơ sở riêng biệt để buộc các trường có giá trị mặc định vào vị trí sau đó trong thứ tự MRO. Bằng mọi giá, hãy tránh đặt các trường trực tiếp trên các lớp sẽ được sử dụng làm lớp cơ sở, chẳng hạn như Parent.

Hệ thống phân cấp lớp sau hoạt động:

# base classes with fields; fields without defaults separate from fields with.
@dataclass
class _ParentBase:
    name: str
    age: int

@dataclass
class _ParentDefaultsBase:
    ugly: bool = False

@dataclass
class _ChildBase(_ParentBase):
    school: str

@dataclass
class _ChildDefaultsBase(_ParentDefaultsBase):
    ugly: bool = True

# public classes, deriving from base-with, base-without field classes
# subclasses of public classes should put the public base class up front.

@dataclass
class Parent(_ParentDefaultsBase, _ParentBase):
    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@dataclass
class Child(Parent, _ChildDefaultsBase, _ChildBase):
    pass

Bằng cách kéo các trường thành các lớp cơ sở riêng biệt với các trường không có giá trị mặc định và các trường có giá trị mặc định và thứ tự kế thừa được lựa chọn cẩn thận, bạn có thể tạo một MRO đặt tất cả các trường không có giá trị mặc định trước những trường có giá trị mặc định. MRO đảo ngược (bỏ qua object) Childlà:

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Lưu ý rằng điều Parentđó không thiết lập bất kỳ trường mới nào, vì vậy không quan trọng ở đây nó kết thúc 'cuối cùng' trong thứ tự danh sách trường. Các lớp có trường không có giá trị mặc định ( _ParentBase_ChildBase) đứng trước các lớp có trường có giá trị mặc định ( _ParentDefaultsBase_ChildDefaultsBase).

Kết quả là ParentChildcác lớp có trường sane cũ hơn, trong khi Childvẫn là một lớp con của Parent:

>>> from inspect import signature
>>> signature(Parent)
<Signature (name: str, age: int, ugly: bool = False) -> None>
>>> signature(Child)
<Signature (name: str, age: int, school: str, ugly: bool = True) -> None>
>>> issubclass(Child, Parent)
True

và do đó bạn có thể tạo các phiên bản của cả hai lớp:

>>> jack = Parent('jack snr', 32, ugly=True)
>>> jack_son = Child('jack jnr', 12, school='havard', ugly=True)
>>> jack
Parent(name='jack snr', age=32, ugly=True)
>>> jack_son
Child(name='jack jnr', age=12, school='havard', ugly=True)

Một tùy chọn khác là chỉ sử dụng các trường có giá trị mặc định; bạn vẫn có thể mắc lỗi không cung cấp schoolgiá trị bằng cách tăng một giá trị trong __post_init__:

_no_default = object()

@dataclass
class Child(Parent):
    school: str = _no_default
    ugly: bool = True

    def __post_init__(self):
        if self.school is _no_default:
            raise TypeError("__init__ missing 1 required argument: 'school'")

nhưng điều này làm thay đổi thứ tự trường; schoolkết thúc sau ugly:

<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

và trình kiểm tra gợi ý kiểu sẽ phàn nàn về việc _no_defaultkhông phải là một chuỗi.

Bạn cũng có thể sử dụng attrsdự án , đó là dự án đã truyền cảm hứng dataclasses. Nó sử dụng một chiến lược hợp nhất thừa kế khác nhau; nó kéo các trường bị ghi đè trong một lớp con đến cuối danh sách các trường, do đó ['name', 'age', 'ugly']trong Parentlớp trở thành ['name', 'age', 'school', 'ugly']trong Childlớp; bằng cách ghi đè trường bằng một mặc định, attrscho phép ghi đè mà không cần thực hiện bước nhảy MRO.

attrshỗ trợ xác định các trường mà không có gợi ý loại, nhưng hãy bám vào chế độ gợi ý loại được hỗ trợ bằng cách cài đặt auto_attribs=True:

import attr

@attr.s(auto_attribs=True)
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@attr.s(auto_attribs=True)
class Child(Parent):
    school: str
    ugly: bool = True

1
Cảm ơn rất nhiều vì câu trả lời chi tiết
Mysterio

Điều này rất hữu ích. Tôi nhầm lẫn về mro mặc dù. Chạy print (Child.mro ()) Tôi nhận được: [<class ' main .Child'>, <class ' main .Parent'>, <class ' main ._ChildDefaultsBase'>, <class ' main ._ParentDefaultsBase'>, < class ' main ._ChildBase'>, <class ' main ._ParentBase'>, <class 'object'>] Vì vậy, không phải các cơ sở mặc định đứng trước các lớp cơ sở?
Ollie

1
@Ollie đó là thứ tự chính xác; lưu ý rằng tôi đã liệt kê nó trong câu trả lời của mình. Khi bạn có nhiều lớp cơ sở, bạn cần một cách tuyến tính hóa các lớp liên quan để quyết định lớp nào đứng trước lớp khác khi kế thừa. Python sử dụng phương pháp phân loại dòng C3 và câu trả lời của tôi tận dụng cách hoạt động của phương pháp này để đảm bảo các thuộc tính có giá trị mặc định luôn đứng sau tất cả các thuộc tính không có giá trị mặc định.
Martijn Pieters

Trên thực tế, attrs có thể hoạt động nhưng bạn cần sử dụng attr.ib(kw_only=True), hãy xem github.com/python-attrs/attrs/issues/38
laike9m

8

Bạn gặp lỗi này vì đối số không có giá trị mặc định đang được thêm vào sau đối số có giá trị mặc định. Thứ tự chèn của các trường kế thừa vào dataclass là ngược lại với Thứ tự phân giải phương pháp , có nghĩa là các Parenttrường đến trước, ngay cả khi chúng được con của chúng viết sau.

Ví dụ từ PEP-557 - Lớp dữ liệu :

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

Danh sách cuối cùng của các trường, theo thứ tự x, y, z,. Loại cuối cùng của xint, như được chỉ định trong lớp C.

Thật không may, tôi không nghĩ có bất kỳ cách nào để giải quyết vấn đề này. Sự hiểu biết của tôi là nếu lớp cha có đối số mặc định, thì không lớp con nào có thể có đối số không mặc định.


Tôi hiểu rằng đối số không mặc định phải đến trước đối số mặc định nhưng làm thế nào có thể khi đối số mẹ khởi tạo trước khi thêm đối số con?
Mysterio

3
Tôi không nghĩ rằng có bất kỳ cách nào xung quanh nó không may. Sự hiểu biết của tôi là nếu lớp cha có đối số mặc định, thì không lớp con nào có thể có đối số không mặc định.
Patrick Haugh

1
Bạn có thể thêm thông tin đó vào câu trả lời trước khi tôi đánh dấu không? Nó sẽ giúp ai đó một ngày nào đó. Khá đáng tiếc là hạn chế của kính dữ liệu. Kết xuất nó tranh luận về dự án python hiện tại của tôi. Thật tuyệt khi thấy những triển khai như vậy tho
Mysterio

5

Bạn có thể sử dụng các thuộc tính có giá trị mặc định trong các lớp cha nếu bạn loại trừ chúng khỏi hàm init. Nếu bạn cần khả năng ghi đè mặc định tại init, hãy mở rộng mã với câu trả lời của Praveen Kulkarni.

from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(default=False, init=False)

@dataclass
class Child(Parent):
    school: str

jack = Parent('jack snr', 32)
jack_son = Child('jack jnr', 12, school = 'havard')
jack_son.ugly = True

Tôi nghĩ câu trả lời này nên được công nhận nhiều hơn. Nó đã giải quyết vấn đề có một trường mặc định trong lớp cha, do đó loại bỏ TypeError.
Nils Bengtsson

5

dựa trên giải pháp Martijn Pieters, tôi đã làm như sau:

1) Tạo hỗn hợp triển khai post_init

from dataclasses import dataclass

no_default = object()


@dataclass
class NoDefaultAttributesPostInitMixin:

    def __post_init__(self):
        for key, value in self.__dict__.items():
            if value is no_default:
                raise TypeError(
                    f"__init__ missing 1 required argument: '{key}'"
                )

2) Sau đó, trong các lớp có vấn đề kế thừa:

from src.utils import no_default, NoDefaultAttributesChild

@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
    attr1: str = no_default

BIÊN TẬP:

Sau một thời gian, tôi cũng tìm thấy sự cố với giải pháp này với mypy, đoạn mã sau sẽ khắc phục sự cố.

from dataclasses import dataclass
from typing import TypeVar, Generic, Union

T = TypeVar("T")


class NoDefault(Generic[T]):
    ...


NoDefaultVar = Union[NoDefault[T], T]
no_default: NoDefault = NoDefault()


@dataclass
class NoDefaultAttributesPostInitMixin:
    def __post_init__(self):
        for key, value in self.__dict__.items():
            if value is NoDefault:
                raise TypeError(f"__init__ missing 1 required argument: '{key}'")


@dataclass
class Parent(NoDefaultAttributesPostInitMixin):
    a: str = ""

@dataclass
class Child(Foo):
    b: NoDefaultVar[str] = no_default

Bạn có định viết "lớp MyDataclass (DataclassWithDefaults, NoDefaultAttributesPostInitMixin)" ở trên trong 2) không?
Scott P.

4

Phương pháp bên dưới giải quyết vấn đề này khi sử dụng python thuần túy dataclasses và không có nhiều mã soạn sẵn.

Trường ugly_init: dataclasses.InitVar[bool]đóng vai trò như một trường giả chỉ để giúp chúng ta khởi tạo và sẽ bị mất khi phiên bản được tạo. While ugly: bool = field(init=False)là một thành viên thể hiện sẽ không được khởi tạo bằng __init__phương thức nhưng có thể được khởi tạo bằng __post_init__phương thức khác (bạn có thể tìm thêm tại đây .).

from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(init=False)
    ugly_init: dataclasses.InitVar[bool]

    def __post_init__(self, ugly_init: bool):
        self.ugly = ugly_init

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f'The Name is {self.name} and {self.name} is {self.age} year old')

@dataclass
class Child(Parent):
    school: str

jack = Parent('jack snr', 32, ugly_init=True)
jack_son = Child('jack jnr', 12, school='havard', ugly_init=True)

jack.print_id()
jack_son.print_id()

xấu_init bây giờ là một tham số bắt buộc không có mặc định
Vadym Tyemirov

2

Tôi quay lại câu hỏi này sau khi phát hiện ra rằng dataclasses thể nhận được tham số decorator cho phép các trường được sắp xếp lại. Đây chắc chắn là một sự phát triển đầy hứa hẹn, mặc dù sự phát triển trên tính năng này dường như đã bị đình trệ phần nào.

Ngay bây giờ, bạn có thể nhận được hành vi này, cùng với một số tính năng khác, bằng cách sử dụng dataclassy , việc thực hiện lại dataclass của tôi để khắc phục những thất vọng như thế này. Sử dụng from dataclassythay thế from dataclassestrong ví dụ ban đầu có nghĩa là nó chạy mà không có lỗi.

Sử dụng thanh tra để in chữ ký của Childlàm cho những gì đang diễn ra rõ ràng; kết quả là (name: str, age: int, school: str, ugly: bool = True). Các trường luôn được sắp xếp lại để các trường có giá trị mặc định đứng sau các trường không có chúng trong tham số của trình khởi tạo. Cả hai danh sách (các trường không có giá trị mặc định và những trường có chúng) vẫn được sắp xếp theo thứ tự định nghĩa.

Đối mặt với vấn đề này là một trong những yếu tố thúc đẩy tôi viết một bản thay thế cho kính dữ liệu. Các cách giải quyết được nêu chi tiết ở đây, mặc dù hữu ích, yêu cầu mã phải được thay đổi đến mức chúng phủ nhận hoàn toàn cách tiếp cận ngây thơ của dataclasses về lợi thế đọc được (theo đó thứ tự trường có thể dự đoán được).


1

Một giải pháp khả thi là sử dụng khỉ vá để nối các trường mẹ

import dataclasses as dc

def add_args(parent): 
    def decorator(orig):
        "Append parent's fields AFTER orig's fields"

        # Aggregate fields
        ff  = [(f.name, f.type, f) for f in dc.fields(dc.dataclass(orig))]
        ff += [(f.name, f.type, f) for f in dc.fields(dc.dataclass(parent))]

        new = dc.make_dataclass(orig.__name__, ff)
        new.__doc__ = orig.__doc__

        return new
    return decorator

class Animal:
    age: int = 0 

@add_args(Animal)
class Dog:
    name: str
    noise: str = "Woof!"

@add_args(Animal)
class Bird:
    name: str
    can_fly: bool = True

Dog("Dusty", 2)               # --> Dog(name='Dusty', noise=2, age=0)
b = Bird("Donald", False, 40) # --> Bird(name='Donald', can_fly=False, age=40)

Cũng có thể thêm trước các trường không phải mặc định bằng cách kiểm tra if f.default is dc.MISSING, nhưng điều này có lẽ quá bẩn.

Mặc dù khỉ vá thiếu một số tính năng kế thừa, nó vẫn có thể được sử dụng để thêm các phương thức vào tất cả các lớp giả con.

Để kiểm soát chi tiết hơn, hãy đặt các giá trị mặc định bằng dc.field(compare=False, repr=True, ...)


1

Bạn có thể sử dụng phiên bản đã sửa đổi của dataclasses, sẽ tạo ra một __init__phương pháp chỉ từ khóa :

import dataclasses


def _init_fn(fields, frozen, has_post_init, self_name):
    # fields contains both real fields and InitVar pseudo-fields.
    globals = {'MISSING': dataclasses.MISSING,
               '_HAS_DEFAULT_FACTORY': dataclasses._HAS_DEFAULT_FACTORY}

    body_lines = []
    for f in fields:
        line = dataclasses._field_init(f, frozen, globals, self_name)
        # line is None means that this field doesn't require
        # initialization (it's a pseudo-field).  Just skip it.
        if line:
            body_lines.append(line)

    # Does this class have a post-init function?
    if has_post_init:
        params_str = ','.join(f.name for f in fields
                              if f._field_type is dataclasses._FIELD_INITVAR)
        body_lines.append(f'{self_name}.{dataclasses._POST_INIT_NAME}({params_str})')

    # If no body lines, use 'pass'.
    if not body_lines:
        body_lines = ['pass']

    locals = {f'_type_{f.name}': f.type for f in fields}
    return dataclasses._create_fn('__init__',
                      [self_name, '*'] + [dataclasses._init_param(f) for f in fields if f.init],
                      body_lines,
                      locals=locals,
                      globals=globals,
                      return_type=None)


def add_init(cls, frozen):
    fields = getattr(cls, dataclasses._FIELDS)

    # Does this class have a post-init function?
    has_post_init = hasattr(cls, dataclasses._POST_INIT_NAME)

    # Include InitVars and regular fields (so, not ClassVars).
    flds = [f for f in fields.values()
            if f._field_type in (dataclasses._FIELD, dataclasses._FIELD_INITVAR)]
    dataclasses._set_new_attribute(cls, '__init__',
                       _init_fn(flds,
                                frozen,
                                has_post_init,
                                # The name to use for the "self"
                                # param in __init__.  Use "self"
                                # if possible.
                                '__dataclass_self__' if 'self' in fields
                                else 'self',
                                ))

    return cls


# a dataclass with a constructor that only takes keyword arguments
def dataclass_keyword_only(_cls=None, *, repr=True, eq=True, order=False,
              unsafe_hash=False, frozen=False):
    def wrap(cls):
        cls = dataclasses.dataclass(
            cls, init=False, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
        return add_init(cls, frozen)

    # See if we're being called as @dataclass or @dataclass().
    if _cls is None:
        # We're called with parens.
        return wrap

    # We're called as @dataclass without parens.
    return wrap(_cls)

(cũng được đăng dưới dạng ý chính , được thử nghiệm với cổng hỗ trợ Python 3.6)

Điều này sẽ yêu cầu xác định lớp con là

@dataclass_keyword_only
class Child(Parent):
    school: str
    ugly: bool = True

Và sẽ tạo __init__(self, *, name:str, age:int, ugly:bool=True, school:str)(là python hợp lệ). Cảnh báo duy nhất ở đây là không cho phép khởi tạo các đối tượng với các đối số vị trí, nhưng nếu không thì nó hoàn toàn bình thường dataclassvà không có các vụ hack xấu xí.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.