如何在Python中修复数据类的TypeError? [英] How can I fix the TypeError of my dataclass in Python?
问题描述
我有一个具有5个属性的数据类.当我通过字典提供这些属性时,它会很好地工作.但是,当字典的属性比类的属性多时,该类将给出TypeError.我试图使当有额外的值时,该类将不在乎它们.我该怎么做?
I have a dataclass with 5 attributes. When I give these attributes via a dictionary, it works well. But when the dictionary has more attributes than the class have, the class gives TypeError. I am trying to make that when there is extra values, the class wouldn't care them. How can I make that?
from dataclasses import dataclass
@dataclass
class Employee(object):
name: str
lastname: str
age: int or None
salary: int
department: str
def __new__(cls, name, lastname, age, salary, department):
return object.__new__(cls)
def __post_init__(self):
if type(self.age) == str:
self.age = int(self.age) or None
def __str__(self):
return f'{self.name}, {self.lastname}, {self.age}'
dic = {"name":"abdülmutallip",
"lastname":"uzunkavakağacıaltındauzanıroğlu",
"age":"24", "salary":2000, "department":"İK",
"city":"istanbul", "country":"tr", "adres":"yok", "phone":"0033333"}
a = Employee(**dic)
print(a)
错误是:
TypeError: __new__() got an unexpected keyword argument 'city'
我希望该类在这种情况下可以正常工作,而不会出现任何错误.我不想将这些额外的属性添加到类中.
I want the class works properly in this situation without any error. I don't want to add these extra attributes into the class.
推荐答案
如果希望数据类接受任意的额外关键字参数,则必须定义自己的__init__
方法,或者在以下位置提供自定义的__call__
方法: 元类 .如果定义自定义的__init__
方法,则dataclass
装饰器将不会为您生成一个.此时,由于您已经在编写__init__
方法,因此不再需要使用__post_init__
.
If you want the dataclass to accept arbitrary extra keyword arguments then you either have to define your own __init__
method, or provide a custom __call__
method on a metaclass. If you define a custom __init__
method, the dataclass
decorator won't generate one for you; at this point there is no need to use __post_init__
any more either since you already are writing an __init__
method.
旁注:
-
__new__
无法更改将哪些参数传递给__init__
.元类的__call__
通常将首先调用cls.__new__(<arguments>)
,然后在__new__
返回值的instance
上调用instance.__init__(<arguments>
,请参见typing.Optional
可以将age
字段的类型正确标记为可选.Optional[int]
等同于Union[int, None]
;就我个人而言,在没有默认值设置且省略age
是不可接受的情况下,我更喜欢在构造函数中使用后者. - 使用
isinstance()
确定对象是否为字符串.或者只是不进行测试,因为int(self.age)
仅返回self.age
不变(如果已经将其设置为整数). - 如果可以将设置为
0
的年龄设置为None
,可以在__post_init__
方法中仅使用or None
. - 如果仅当
int(age)
失败时将age
设置为None
,则必须使用try:...except
来处理ValueError
或TypeError
在这种情况下可能引发的异常,而不是or None
.
__new__
can't alter what arguments are passed to__init__
. The metaclass's__call__
will normally first callcls.__new__(<arguments>)
then callinstance.__init__(<arguments>
on theinstance
return value from__new__
, see the datamodel documentation.- You can't use
int or None
, that's an expression that just returnsint
, it won't let you omit theage
parameter. Give the field a default value instead, or use aUnion
type hint ifNone
is only used to indicate age=0 or a failedint()
conversion. - Fields that have a default defined must come after fields that do not have a default defined, so put
age
at the end. - If you also use type hinting beyond dataclasses, and
age
is meant to be an optional field, then usetyping.Optional
to properly mark the type of theage
field as optional.Optional[int]
is equivalent toUnion[int, None]
; personally I prefer the latter in constructors when there is no default value set and omittingage
is not acceptable. - Use
isinstance()
to determine if an object is a string. Or just don't test, sinceint(self.age)
just returnsself.age
unchanged if it already is set to an integer. - Only use
or None
in the__post_init__
method if it is okay for an age set to0
to be set toNone
. - If
age
is to be set toNone
only ifint(age)
fails, then you have to usetry:...except
to handle theValueError
orTypeError
exceptions thatint()
can raise in that case, notor None
.
假定仅在转换失败时才将age
设置为None
:
Assuming that you meant for age
to be set to None
only if conversion fails:
from dataclasses import dataclass
from typing import Union
@dataclass
class Employee(object):
name: str
lastname: str
age: Union[int, None] # set to None if conversion fails
salary: int
department: str
def __init__(
self,
name: str,
lastname: str,
age: Union[int, None],
salary: int,
department: str,
*args: Any,
**kwargs: Any,
) -> None:
self.name = name
self.lastname = lastname
try:
self.age = int(age)
except (ValueError, TypeError):
# could not convert age to an integer
self.age = None
self.salary = salary
self.department = department
def __str__(self):
return f'{self.name}, {self.lastname}, {self.age}'
如果您想走元类路线,那么可以通过内省__init__
或__new__
方法调用签名,创建一个忽略几乎所有任何类的所有额外参数的参数:>
If you want to go the metaclass route, then you can create one that ignores all extra arguments for almost any class, by introspecting the __init__
or __new__
method call signature:
from inspect import signature, Parameter
class _ArgTrimmer:
def __init__(self):
self.new_args, self.new_kw = [], {}
self.dispatch = {
Parameter.POSITIONAL_ONLY: self.pos_only,
Parameter.KEYWORD_ONLY: self.kw_only,
Parameter.POSITIONAL_OR_KEYWORD: self.pos_or_kw,
Parameter.VAR_POSITIONAL: self.starargs,
Parameter.VAR_KEYWORD: self.starstarkwargs,
}
def pos_only(self, p, i, args, kwargs):
if i < len(args):
self.new_args.append(args[i])
def kw_only(self, p, i, args, kwargs):
if p.name in kwargs:
self.new_kw[p.name] = kwargs.pop(p.name)
def pos_or_kw(self, p, i, args, kwargs):
if i < len(args):
self.new_args.append(args[i])
# drop if also in kwargs, otherwise parameters collide
# if there's a VAR_KEYWORD parameter to capture it
kwargs.pop(p.name, None)
elif p.name in kwargs:
self.new_kw[p.name] = kwargs[p.name]
def starargs(self, p, i, args, kwargs):
self.new_args.extend(args[i:])
def starstarkwargs(self, p, i, args, kwargs):
self.new_kw.update(kwargs)
def trim(self, params, args, kwargs):
for i, p in enumerate(params.values()):
if i: # skip first (self or cls) arg of unbound function
self.dispatch[p.kind](p, i - 1, args, kwargs)
return self.new_args, self.new_kw
class IgnoreExtraArgsMeta(type):
def __call__(cls, *args, **kwargs):
if cls.__new__ is not object.__new__:
func = cls.__new__
else:
func = getattr(cls, '__init__', None)
if func is not None:
sig = signature(func)
args, kwargs = _ArgTrimmer().trim(sig.parameters, args, kwargs)
return super().__call__(*args, **kwargs)
此元类适用于任何Python类,但是如果要以内置类型作为子类,则__new__
或__init__
方法可能不是自省的.这里不是这种情况,但是需要警告您是否需要在其他情况下使用上述元类.
This metaclass will work for any Python class, but if you were to subclass in a built-in type then the __new__
or __init__
methods may not be introspectable. Not the case here, but a caveat that you would need to know about if you were to use the above metaclass in other situations.
然后将以上内容用作数据类上的metaclass
参数:
Then use the above as a metaclass
parameter on your dataclass:
from dataclasses import dataclass
from typing import Union
@dataclass
class Employee(metaclass=IgnoreExtraArgsMeta):
name: str
lastname: str
age: Union[int, None]
salary: int
department: str
def __post_init__(self):
try:
self.age = int(self.age)
except (ValueError, TypeError):
# could not convert age to an integer
self.age = None
def __str__(self):
return f'{self.name}, {self.lastname}, {self.age}'
使用元类的优点在这里应该很清楚;无需重复__init__
方法中的所有字段.
The advantage of using a metaclass should be clear here; no need to repeat all the fields in the __init__
method.
第一种方法的演示:
>>> from dataclasses import dataclass
>>> from typing import Union
>>> @dataclass
... class Employee(object):
... name: str
... lastname: str
... age: Union[int, None] # set to None if conversion fails
... salary: int
... department: str
... def __init__(self,
... name: str,
... lastname: str,
... age: Union[int, None],
... salary: int,
... department: str,
... *args: Any,
... **kwargs: Any,
... ) -> None:
... self.name = name
... self.lastname = lastname
... try:
... self.age = int(age)
... except (ValueError, TypeError):
... # could not convert age to an integer
... self.age = None
... self.salary = salary
... self.department = department
... def __str__(self):
... return f'{self.name}, {self.lastname}, {self.age}'
...
>>> dic = {"name":"abdülmutallip",
... "lastname":"uzunkavakağacıaltındauzanıroğlu",
... "age":"24", "salary":2000, "department":"İK",
... "city":"istanbul", "country":"tr", "adres":"yok", "phone":"0033333"}
>>> a = Employee(**dic)
>>> a
Employee(name='abdülmutallip', lastname='uzunkavakağacıaltındauzanıroğlu', age=24, salary=2000, department='İK')
>>> print(a)
abdülmutallip, uzunkavakağacıaltındauzanıroğlu, 24
>>> a.age
24
>>> Employee(name="Eric", lastname="Idle", age="too old to tell", salary=123456, department="Silly Walks")
Employee(name='Eric', lastname='Idle', age=None, salary=123456, department='Silly Walks')
第二种方法:
>>> @dataclass
... class Employee(metaclass=IgnoreExtraArgsMeta):
... name: str
... lastname: str
... age: Union[int, None]
... salary: int
... department: str
... def __post_init__(self):
... try:
... self.age = int(self.age)
... except (ValueError, TypeError):
... # could not convert age to an integer
... self.age = None
... def __str__(self):
... return f'{self.name}, {self.lastname}, {self.age}'
...
>>> a = Employee(**dic)
>>> print(a)
abdülmutallip, uzunkavakağacıaltındauzanıroğlu, 24
>>> a
Employee(name='abdülmutallip', lastname='uzunkavakağacıaltındauzanıroğlu', age=24, salary=2000, department='İK')
>>> Employee("Michael", "Palin", "annoyed you asked", salary=42, department="Complaints", notes="Civil servants should never be asked for their salary, either")
Employee(name='Michael', lastname='Palin', age=None, salary=42, department='Complaints')
如果age
是可选的(因此具有默认值),则将其移至字段的末尾,将其指定为Optional[int]
,然后为其分配None
.您必须在指定自己的__init__
方法中执行相同的操作:
If age
is meant to be optional (so, have a default value), then move it to the end of the fields, give it Optional[int]
as the type, and assign None
to it. You'll have to do the same in the __init__
method you specify your own:
from typing import Optional
@dataclass
class Employee(object):
name: str
lastname: str
age: Optional[int] = None
salary: int
department: str
def __init__(
self,
name: str,
lastname: str,
salary: int,
department: str,
age: Optional[int] = None,
*args: Any,
**kwargs: Any,
) -> None:
# ...
这篇关于如何在Python中修复数据类的TypeError?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!