如何在Python中修复数据类的TypeError? [英] How can I fix the TypeError of my dataclass in Python?

查看:193
本文介绍了如何在Python中修复数据类的TypeError?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个具有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>,请参见
  • __new__ can't alter what arguments are passed to __init__. The metaclass's __call__ will normally first call cls.__new__(<arguments>) then call instance.__init__(<arguments> on the instance return value from __new__, see the datamodel documentation.
  • You can't use int or None, that's an expression that just returns int, it won't let you omit the age parameter. Give the field a default value instead, or use a Union type hint if None is only used to indicate age=0 or a failed int() 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 use typing.Optional to properly mark the type of the age field as optional. Optional[int] is equivalent to Union[int, None]; personally I prefer the latter in constructors when there is no default value set and omitting age is not acceptable.
  • Use isinstance() to determine if an object is a string. Or just don't test, since int(self.age) just returns self.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 to 0 to be set to None.
  • If age is to be set to None only if int(age) fails, then you have to use try:...except to handle the ValueError or TypeError exceptions that int() can raise in that case, not or 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆