【问题标题】:How can I fix the TypeError of my dataclass in Python?如何在 Python 中修复我的数据类的 TypeError?
【发布时间】:2019-12-12 22:21:20
【问题描述】:

我有一个具有 5 个属性的数据类。当我通过字典提供这些属性时,效果很好。但是当字典的属性多于类时,类会给出 TypeError。我试图做到这一点,当有额外的价值时,班级不会关心它们。我怎样才能做到这一点?

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'

我希望该类在这种情况下正常工作而不会出现任何错误。我不想在类中添加这些额外的属性。

【问题讨论】:

  • 为什么要添加__new__ 根本,数据类不需要显式的__new__ 方法。 int or None 不是描述age 是可选的有效方式,请使用Optional[age] 并分配默认值。可选值必须遵循必填字段。
  • age 是否应该是可选的,或者您只是想记录如果提供了age=0,它将被设置为None?跨度>
  • 我想知道为什么年龄可以是None?每个员工都应该有一个年龄,如果您的意思是年龄可能未初始化,那么使用age = -1 可能更有意义。
  • @ShaiAvr: 不,Noneage is missing 的绝佳选择。
  • @MartijnPieters 对于像年龄这样的东西,我更喜欢-1,因为它显然是一个无效的年龄,但我想这是个人喜好。

标签: python python-3.x python-dataclasses


【解决方案1】:

如果您希望数据类接受任意额外的关键字参数,那么您必须定义自己的__init__ 方法,或者在metaclass 上提供自定义__call__ 方法。如果你定义了一个自定义的__init__ 方法,dataclass 装饰器不会为你生成一个;此时无需再使用__post_init__,因为您已经在编写__init__ 方法。

旁注:

  • __new__ 无法更改传递给 __init__ 的参数。元类的__call__ 通常会首先调用cls.__new__(<arguments>),然后在instance__new__ 返回值上调用instance.__init__(<arguments>,参见datamodel documentation
  • 你不能使用int or None,这是一个只返回int的表达式,它不会让你省略age参数。改为为该字段提供默认值,或者如果 None 仅用于指示 age=0 或 int() 转换失败,则使用 Union 类型提示。
  • 定义了默认值的字段必须在未定义默认值的字段之后,因此请在末尾添加 age
  • 如果您还在数据类之外使用类型提示,并且age 是一个可选字段,则使用typing.Optionalage 字段的类型正确标记为可选。 Optional[int] 等价于Union[int, None];当没有设置默认值并且省略 age 是不可接受的时,我个人更喜欢在构造函数中使用后者。
  • 使用isinstance() 确定对象是否为字符串。或者干脆不测试,因为int(self.age) 只是返回不变的self.age,如果它已经设置为一个整数。
  • 如果可以将年龄设置为0 设置为None,则仅在__post_init__ 方法中使用or None
  • 如果仅当int(age) 失败时将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}' 

如果你想走元类路线,那么你可以通过自省__init____new__ 方法调用签名来创建一个忽略几乎任何类的所有额外参数的方法: p>

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 类,但如果您要以内置类型进行子类化,则 @​​987654372@ 或 __init__ 方法可能无法自省。这里不是这种情况,但是如果您要在其他情况下使用上述元类,您需要知道一个警告。

然后在你的数据类中使用上面的 metaclass 参数:

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__ 方法中的所有字段。

第一种方法的演示:

>>> 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:
        # ...

【讨论】:

  • 如果您提供的参数比预期的多,这仍然会失败。您应该将*args**kwargs 添加到__init__ 以使其像OP 想要的那样工作。附带说明:Optional[int] 优于 Union[int, None]
  • @ShaiAvr: Optional[int] 声明age 有一个默认值,如果你忽略它。我明确希望将此记录为不是可选的
  • @ShaiAvr:哎呀,我忘了在__init__ 中包含*args, **kwargs 参数。现已修复。
  • 根据docsOptional[int]等价于Union[int, None],与默认值无关。
  • @ShaiAvr: Optional[int] 确实是等价的,我只是在这里明确说明。这是个人喜好。
猜你喜欢
  • 2023-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-23
  • 2020-03-03
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
相关资源
最近更新 更多