То, как классы данных объединяют атрибуты, не позволяет вам использовать атрибуты со значениями по умолчанию в базовом классе, а затем использовать атрибуты без значений по умолчанию (позиционные атрибуты) в подклассе.
Это связано с тем, что атрибуты объединяются, начиная с нижней части MRO и создавая упорядоченный список атрибутов в порядке их появления; переопределения сохраняются в исходном месте. Итак, Parent
начинается с ['name', 'age', 'ugly']
, где ugly
есть значение по умолчанию, а затем Child
добавляется ['school']
в конец этого списка ( ugly
уже в списке). Это означает, что вы в конечном итоге получаете, ['name', 'age', 'ugly', 'school']
а поскольку school
значения по умолчанию нет, это приводит к недопустимому списку аргументов для __init__
.
Это отражено в ППК-557 Dataclasses , при наследовании :
Когда класс данных создается @dataclass
декоратором, он просматривает все базовые классы класса в обратном MRO (то есть начиная с object
) и для каждого найденного класса данных добавляет поля из этого базового класса в упорядоченный отображение полей. После добавления всех полей базового класса он добавляет свои собственные поля к упорядоченному отображению. Все сгенерированные методы будут использовать это комбинированное вычисляемое упорядоченное сопоставление полей. Поскольку поля расположены в порядке вставки, производные классы переопределяют базовые классы.
и в соответствии со спецификацией :
TypeError
будет поднят, если поле без значения по умолчанию следует за полем со значением по умолчанию. Это верно либо когда это происходит в одном классе, либо в результате наследования классов.
У вас есть несколько вариантов, чтобы избежать этой проблемы.
Первый вариант - использовать отдельные базовые классы для принудительного переноса полей со значениями по умолчанию на более позднюю позицию в порядке ТОиР. Любой ценой избегайте установки полей непосредственно в классах, которые будут использоваться в качестве базовых классов, например Parent
.
Работает следующая иерархия классов:
@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
@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
Вытягивая поля в отдельные базовые классы с полями без значений по умолчанию и полями со значениями по умолчанию, а также с тщательно выбранным порядком наследования, вы можете создать MRO, который помещает все поля без значений по умолчанию перед полями со значениями по умолчанию. Обратный MRO (игнорирование object
) для Child
:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Обратите внимание, что Parent
это не устанавливает никаких новых полей, поэтому здесь не имеет значения, что он заканчивается «последним» в порядке перечисления полей. Классы с полями без значений по умолчанию ( _ParentBase
и _ChildBase
) предшествуют классам с полями со значениями по умолчанию ( _ParentDefaultsBase
и _ChildDefaultsBase
).
В результате Parent
и Child
классы с полем здравомыслящего старшего, в то время как Child
все еще подкласс 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
и поэтому вы можете создавать экземпляры обоих классов:
>>> 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)
Другой вариант - использовать только поля со значениями по умолчанию; вы все равно можете сделать ошибку, чтобы не указать school
значение, подняв его в __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'")
но это действительно изменяет порядок полей; school
заканчивается после ugly
:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
и средство проверки подсказок будет жаловаться на _no_default
то, что это не строка.
Вы также можете использовать attrs
проект , который вдохновил вас dataclasses
. Он использует другую стратегию слияния наследования; он вытягивает переопределенные поля в подклассе в конец списка полей, так что ['name', 'age', 'ugly']
в Parent
классе становится ['name', 'age', 'school', 'ugly']
в Child
классе; заменив поле значением по умолчанию,attrs
позволяет переопределение без необходимости выполнять танец MRO.
attrs
поддерживает определение полей без подсказок типа, но позволяет придерживаться режима подсказки поддерживаемого типа , установив 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
attr.ib(kw_only=True)
, см github.com/python-attrs/attrs/issues/38Вы видите эту ошибку, потому что аргумент без значения по умолчанию добавляется после аргумента со значением по умолчанию. Порядок вставки унаследованных полей в класс данных является обратным порядку разрешения методов , что означает, что
Parent
поля идут первыми, даже если их потомки перезаписывают позже.Пример из PEP-557 - Классы данных :
К сожалению, я не думаю, что это можно обойти. Я понимаю, что если родительский класс имеет аргумент по умолчанию, то ни один дочерний класс не может иметь аргументы, отличные от аргументов по умолчанию.
источник
Вы можете использовать атрибуты со значениями по умолчанию в родительских классах, если исключите их из функции init. Если вам нужна возможность переопределить значение по умолчанию в init, дополните код ответом Правина Кулкарни.
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
источник
на основе решения Martijn Pieters я сделал следующее:
1) Создайте микширование, реализующее 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) Затем в классах с проблемой наследования:
from src.utils import no_default, NoDefaultAttributesChild @dataclass class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin): attr1: str = no_default
РЕДАКТИРОВАТЬ:
Через некоторое время я также обнаружил проблемы с этим решением с mypy, следующий код устраняет проблему.
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
источник
Приведенный ниже подход решает эту проблему при использовании чистого Python.
dataclasses
и без особого шаблонного кода.Он
ugly_init: dataclasses.InitVar[bool]
служит псевдополем, чтобы помочь нам выполнить инициализацию, и будет утерян после создания экземпляра. Покаugly: bool = field(init=False)
- это член экземпляра, который не будет инициализирован__init__
методом, но может быть инициализирован с помощью__post_init__
метода (подробнее вы можете найти здесь ).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()
Если вы хотите использовать шаблон, где
ugly_init
это необязательно, вы можете определить метод класса в родительском элементе, который включаетugly_init
в качестве необязательного параметра:from dataclasses import dataclass, field, InitVar @dataclass class Parent: name: str age: int ugly: bool = field(init=False) ugly_init: InitVar[bool] def __post_init__(self, ugly_init: bool): self.ugly = ugly_init @classmethod def create(cls, ugly_init=True, **kwargs): return cls(ugly_init=ugly_init, **kwargs) 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.create(name='jack snr', age=32, ugly_init=False) jack_son = Child.create(name='jack jnr', age=12, school='harvard') jack.print_id() jack_son.print_id()
Теперь вы можете использовать
create
метод класса в качестве фабричного метода для создания родительских / дочерних классов со значением по умолчанию дляugly_init
. Обратите внимание, что для работы этого подхода необходимо использовать именованные параметры.источник
Я вернулся к этому вопросу после того, как обнаружил, что классы данных могут получать параметр декоратора, который позволяет полей. Это, безусловно, многообещающая разработка, хотя разработка этой функции, похоже, несколько застопорилась.
Прямо сейчас вы можете получить это поведение, а также некоторые другие тонкости, используя dataclassy , мою повторную реализацию классов данных, которая преодолевает подобные разочарования. Использование
from dataclassy
вместоfrom dataclasses
в исходном примере означает, что он работает без ошибок.Использование inspect для печати подписи
Child
проясняет происходящее; результат есть(name: str, age: int, school: str, ugly: bool = True)
. Поля всегда переупорядочиваются, так что поля со значениями по умолчанию идут после полей без них в параметрах инициализатора. Оба списка (поля без значений по умолчанию и поля с ними) по-прежнему упорядочены в порядке определения.Столкновение лицом к лицу с этой проблемой было одним из факторов, которые побудили меня написать замену для классов данных. Обходные пути, описанные здесь, хотя и полезны, требуют, чтобы код был искажен до такой степени, что они полностью сводят на нет наивный подход классов данных преимущества удобочитаемости (при котором упорядочение полей тривиально предсказуемо).
источник
Возможный обходной путь - использовать исправление обезьяны для добавления родительских полей
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)
Также можно добавить поля, отличные от значений по умолчанию, поставив галочку
if f.default is dc.MISSING
, но это, вероятно, слишком грязно.Несмотря на то, что в monkey-patching не хватает некоторых функций наследования, его все же можно использовать для добавления методов ко всем псевдо-дочерним классам.
Для более детального управления установите значения по умолчанию, используя
dc.field(compare=False, repr=True, ...)
источник
Вы можете использовать модифицированную версию классов данных, которая будет генерировать
__init__
метод только с ключевыми словами :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)
(также опубликовано как суть , протестировано с помощью бэкпорта Python 3.6)
Для этого потребуется определить дочерний класс как
@dataclass_keyword_only class Child(Parent): school: str ugly: bool = True
И будет генерировать
__init__(self, *, name:str, age:int, ugly:bool=True, school:str)
(что является допустимым питоном). Единственное предостережение здесь - не разрешать инициализировать объекты позиционными аргументами, но в остальном это совершенно обычныйdataclass
метод без уродливых хаков.источник