如何修复 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来处理ValueErrorTypeError 在这种情况下 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:
        # ...