Mys*_*rio 35 python python-3.x python-3.7 python-dataclasses
我目前正在尝试使用Python 3.7中引入的新数据类结构.我目前坚持尝试做一些父类的继承.看起来参数的顺序是由我当前的方法拙劣的,这样子类中的bool参数在其他参数之前传递.这导致类型错误.
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()
Run Code Online (Sandbox Code Playgroud)
当我运行此代码时,我得到了这个TypeError:
TypeError: non-default argument 'school' follows default argument
Run Code Online (Sandbox Code Playgroud)
我该如何解决?
小智 121
请注意,使用Python 3.10,现在可以使用数据类本地完成此操作。
Dataclasses 3.10 添加了kw_only属性(类似于attrs)。它允许您指定哪些字段是keyword_only,因此将在init的末尾设置,不会导致继承问题。
直接摘自埃里克·史密斯关于该主题的博客文章:
人们[要求]此功能有两个原因:
- 当数据类具有许多字段时,按位置指定它们可能会变得不可读。它还要求为了向后兼容,所有新字段都添加到数据类的末尾。这并不总是令人满意的。
- 当一个数据类从另一个数据类继承时,并且基类具有带默认值的字段,则派生类中的所有字段也必须具有默认值。
下面是使用这个新参数的最简单方法,但是您可以通过多种方式使用它来在父类中使用带有默认值的继承:
from dataclasses import dataclass
@dataclass(kw_only=True)
class Parent:
name: str
age: int
ugly: bool = False
@dataclass(kw_only=True)
class Child(Parent):
school: str
ch = Child(name="Kevin", age=17, school="42")
print(ch.ugly)
Run Code Online (Sandbox Code Playgroud)
请查看上面链接的博文,了解 kw_only 的更全面解释。
干杯!
PS:由于它相当新,请注意您的 IDE 可能仍会引发错误,但它可以在运行时运行
Mar*_*ers 45
数据类组合属性的方式使您无法在基类中使用具有默认值的属性,然后在子类中使用没有默认(位置属性)的属性.
那是因为属性是从MRO的底部开始组合的,并按照首先看到的顺序构建属性的有序列表; 覆盖保留在原始位置.所以Parent从一开始['name', 'age', 'ugly'],ugly默认情况下,然后Child添加['school']到该列表的末尾(ugly已经在列表中).这意味着您最终得到['name', 'age', 'ugly', 'school']并且因为school没有默认值,这会导致无效的参数列表__init__.
这在PEP-557数据类中有记录,在继承下:
当
@dataclass装饰器创建数据类时,它会在反向MRO中查看所有类的基类(即,object从中开始),并且对于它找到的每个数据类,将该基类中的字段添加到有序类中字段映射.添加完所有基类字段后,它会将自己的字段添加到有序映射中.所有生成的方法都将使用这种组合的,计算的有序字段映射.由于字段是按插入顺序排列的,因此派生类会覆盖基类.
在规范下:
TypeError如果没有默认值的字段在具有默认值的字段后面,则会引发.当这发生在单个类中时,或者作为类继承的结果时,都是如此.
你有几个选项可以避免这个问题.
第一个选项是使用单独的基类将具有默认值的字段强制到MRO顺序中的稍后位置.不惜一切代价,避免直接在要用作基类的类上设置字段,例如Parent.
以下类层次结构有效:
# 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
Run Code Online (Sandbox Code Playgroud)
通过将字段拉出到单独的基类中,其中包含没有默认值的字段和具有默认值的字段,以及精心选择的继承顺序,您可以生成一个MRO,在没有默认值的情况下将所有字段放在默认值之前.反向MRO(忽略object)Child是:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Run Code Online (Sandbox Code Playgroud)
请注意,Parent这不会设置任何新字段,因此它在字段列表顺序中以"last"结尾并不重要.具有无默认值(_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
Run Code Online (Sandbox Code Playgroud)
所以你可以创建这两个类的实例:
>>> 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)
Run Code Online (Sandbox Code Playgroud)
另一种选择是仅使用具有默认值的字段; 你仍然可以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'")
Run Code Online (Sandbox Code Playgroud)
但这确实改变了野外秩序; school结束后ugly:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
Run Code Online (Sandbox Code Playgroud)
并且类型提示检查器会抱怨_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
Run Code Online (Sandbox Code Playgroud)
小智 25
如果从 init 函数中排除它们,则可以在父类中使用具有默认值的属性。如果您需要在初始化时覆盖默认值的可能性,请使用 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
Run Code Online (Sandbox Code Playgroud)
Pra*_*rni 10
下面的方法在使用纯 pythondataclasses并且没有太多样板代码的情况下处理这个问题。
该ugly_init: dataclasses.InitVar[bool]充当伪场正好可以帮助我们做初始化,一旦被创建的实例将丢失。Whileugly: 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()
Run Code Online (Sandbox Code Playgroud)
如果要使用ugly_init可选的模式,可以在 Parent 上定义一个类方法,该方法包含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()
Run Code Online (Sandbox Code Playgroud)
现在您可以使用create类方法作为工厂方法来创建具有默认值的父/子类ugly_init。请注意,您必须使用命名参数才能使用此方法。
您看到此错误,因为在具有默认值的参数之后添加了没有默认值的参数.继承字段到数据类的插入顺序与方法解析顺序相反,这意味着Parent字段首先出现,即使它们的子节点稍后写入.
Run Code Online (Sandbox Code Playgroud)@dataclass class Base: x: Any = 15.0 y: int = 0 @dataclass class C(Base): z: int = 10 x: int = 15最终的字段列表按顺序排列
x, y, z.最终类型x是int,如类中所指定C.
不幸的是,我认为没有办法解决这个问题.我的理解是,如果父类有一个默认参数,那么没有子类可以有非默认参数.
基于 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}'"
)
Run Code Online (Sandbox Code Playgroud)
2)然后在有继承问题的类中:
from src.utils import no_default, NoDefaultAttributesChild
@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
attr1: str = no_default
Run Code Online (Sandbox Code Playgroud)
编辑:
一段时间后,我也发现此解决方案与 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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
7684 次 |
| 最近记录: |