如何修复 Python 中数据类的 TypeError?
How can I fix the TypeError of my dataclass in Python?
我有一个包含 5 个属性的数据class。当我通过字典提供这些属性时,效果很好。但是当字典的属性多于 class 的属性时,class 会给出 TypeError。当有额外的值时,我试图做到这一点,class 不会关心它们。我该怎么做?
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'
我希望 class 在这种情况下能正常工作,没有任何错误。我不想将这些额外的属性添加到 class.
如果你想让数据class接受任意额外的关键字参数,那么你要么必须定义自己的__init__
方法,要么在metaclass。如果您定义自定义 __init__
方法,dataclass
装饰器将不会为您生成一个;此时也不再需要使用 __post_init__
,因为您已经在编写 __init__
方法。
旁注:
__new__
无法更改传递给 __init__
的参数。 metaclass 的 __call__
通常会先调用 cls.__new__(<arguments>)
,然后在 __new__
的 instance
return 值上调用 instance.__init__(<arguments>
,看到 datamodel documentation.
- 你不能用
int or None
,那是一个只returns int
的表达式,它不会让你省略age
参数。为该字段提供默认值,或者如果 None
仅用于指示 age=0 或失败的 int()
转换,则使用 Union
类型提示。
- 具有默认定义的字段 必须 位于未定义默认的字段之后,因此将
age
放在末尾。
- 如果您还使用数据 classes 之外的类型提示,并且
age
是一个可选字段,则使用 typing.Optional
来正确标记 age
字段为可选。 Optional[int]
等同于Union[int, None]
;就我个人而言,当没有设置默认值并且 省略 age
是不可接受的时,我个人更喜欢后者。
- 使用
isinstance()
判断对象是否为字符串。或者 就不要测试 ,因为 int(self.age)
只是 returns self.age
如果它已经被设置为一个整数则保持不变。
- 仅在
__post_init__
方法中使用 or None
如果可以将年龄设置为 0
设置为 None
。
- 如果
age
仅在int(age)
失败时设置为None
,那么您必须使用try:...except
来处理ValueError
或TypeError
在这种情况下 int()
可以引发的异常,而不是 or None
.
假设您打算仅在转换失败时将 age
设置为 None
:
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}'
如果你想走 metaclass 路线,那么你可以创建一个忽略所有额外参数的几乎 any class,通过内省__init__
或 __new__
方法调用签名:
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)
这个 metaclass 适用于任何 Python class,但是如果你要在内置类型中使用 subclass 那么 __new__
或 __init__
方法可能无法自省。这里不是这种情况,但是如果你在其他情况下使用上面的元class,你需要知道一个警告。
然后将以上内容用作数据的 metaclass
参数class:
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}'
使用元class的优势在这里应该很清楚;无需重复 __init__
方法中的所有字段。
第一种方法的演示:
>>> 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__
方法中执行相同的操作:
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:
# ...
我有一个包含 5 个属性的数据class。当我通过字典提供这些属性时,效果很好。但是当字典的属性多于 class 的属性时,class 会给出 TypeError。当有额外的值时,我试图做到这一点,class 不会关心它们。我该怎么做?
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'
我希望 class 在这种情况下能正常工作,没有任何错误。我不想将这些额外的属性添加到 class.
如果你想让数据class接受任意额外的关键字参数,那么你要么必须定义自己的__init__
方法,要么在metaclass。如果您定义自定义 __init__
方法,dataclass
装饰器将不会为您生成一个;此时也不再需要使用 __post_init__
,因为您已经在编写 __init__
方法。
旁注:
__new__
无法更改传递给__init__
的参数。 metaclass 的__call__
通常会先调用cls.__new__(<arguments>)
,然后在__new__
的instance
return 值上调用instance.__init__(<arguments>
,看到 datamodel documentation.- 你不能用
int or None
,那是一个只returnsint
的表达式,它不会让你省略age
参数。为该字段提供默认值,或者如果None
仅用于指示 age=0 或失败的int()
转换,则使用Union
类型提示。 - 具有默认定义的字段 必须 位于未定义默认的字段之后,因此将
age
放在末尾。 - 如果您还使用数据 classes 之外的类型提示,并且
age
是一个可选字段,则使用typing.Optional
来正确标记age
字段为可选。Optional[int]
等同于Union[int, None]
;就我个人而言,当没有设置默认值并且 省略age
是不可接受的时,我个人更喜欢后者。 - 使用
isinstance()
判断对象是否为字符串。或者 就不要测试 ,因为int(self.age)
只是 returnsself.age
如果它已经被设置为一个整数则保持不变。 - 仅在
__post_init__
方法中使用or None
如果可以将年龄设置为0
设置为None
。 - 如果
age
仅在int(age)
失败时设置为None
,那么您必须使用try:...except
来处理ValueError
或TypeError
在这种情况下int()
可以引发的异常,而不是or None
.
假设您打算仅在转换失败时将 age
设置为 None
:
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}'
如果你想走 metaclass 路线,那么你可以创建一个忽略所有额外参数的几乎 any class,通过内省__init__
或 __new__
方法调用签名:
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)
这个 metaclass 适用于任何 Python class,但是如果你要在内置类型中使用 subclass 那么 __new__
或 __init__
方法可能无法自省。这里不是这种情况,但是如果你在其他情况下使用上面的元class,你需要知道一个警告。
然后将以上内容用作数据的 metaclass
参数class:
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}'
使用元class的优势在这里应该很清楚;无需重复 __init__
方法中的所有字段。
第一种方法的演示:
>>> 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__
方法中执行相同的操作:
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:
# ...