Как я могу исправить ошибку TypeError моего класса данных в Python?

У меня есть класс данных с 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'

Я хочу, чтобы в этой ситуации класс работал правильно, без ошибок. Я не хочу добавлять в класс эти дополнительные атрибуты.


person Emrah Tema    schedule 05.08.2019    source источник
comment
Зачем вообще добавлять __new__ , класс данных не требует явного __new__ метода. int or None не является допустимым способом описания того, что age является необязательным, используйте Optional[age] и назначьте значение по умолчанию. Необязательные значения должны следовать за обязательными полями.   -  person Martijn Pieters    schedule 05.08.2019
comment
age должен быть необязательным, или вы просто хотите записать, что он будет установлен в None, если age=0 предоставлен?   -  person Martijn Pieters    schedule 05.08.2019
comment
Я хотел бы знать, почему возраст может быть None? У каждого сотрудника должен быть возраст, и если вы имеете в виду, что возраст может быть неинициализированным, возможно, имеет смысл использовать age = -1.   -  person Shai Avr    schedule 05.08.2019
comment
@ShaiAvr: нет, None - отличный выбор для возраста не хватает.   -  person Martijn Pieters    schedule 05.08.2019
comment
@MartijnPieters Для чего-то вроде возраста я бы предпочел -1, потому что это явно недействительный возраст, но я полагаю, что это личное предпочтение.   -  person Shai Avr    schedule 05.08.2019
comment
Возможный дубликат Как игнорировать переданные лишние аргументы в класс данных?   -  person Arne    schedule 06.08.2019
comment
Дело не в возрастном типе   -  person Emrah Tema    schedule 06.08.2019


Ответы (1)


Если вы хотите, чтобы класс данных принимал произвольные дополнительные аргументы ключевого слова, вам необходимо либо определить свой собственный метод __init__, либо предоставить собственный метод __call__ для метакласс. Если вы определяете собственный __init__ метод, dataclass декоратор не сгенерирует его за вас; на этом этапе также нет необходимости использовать __post_init__, поскольку вы уже пишете метод __init__.

Боковые примечания:

  • __new__ не может изменить, какие аргументы передаются в __init__. Метакласс __call__ обычно сначала вызывает cls.__new__(<arguments>), а затем instance.__init__(<arguments> для instance возвращаемого значения из __new__, см. документация по модели данных.
  • Вы не можете использовать int or None, это выражение, которое просто возвращает int, оно не позволяет вам опустить параметр age. Вместо этого присвойте полю значение по умолчанию или используйте подсказку типа Union, если None используется только для указания возраста = 0 или неудачного int() преобразования.
  • Поля, для которых задано значение по умолчанию, должны располагаться после полей, для которых значение по умолчанию не определено, поэтому в конце укажите age.
  • Если вы также используете подсказку типа помимо классов данных и age должен быть необязательным полем, используйте _ 22_, чтобы правильно пометить тип поля age как необязательный. Optional[int] эквивалентно Union[int, None]; Лично я предпочитаю последнее в конструкторах, когда значение по умолчанию не задано и опускание age недопустимо.
  • Используйте isinstance(), чтобы определить, является ли объект строкой. Или просто не тестируйте, поскольку int(self.age) просто возвращает self.age без изменений, если он уже установлен в целое число.
  • Используйте or None в методе __post_init__ только в том случае, если для возраста, установленного на 0, можно установить значение None.
  • Если age должен быть установлен в None только в случае int(age) сбоя, тогда вы должны использовать 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}' 

Если вы хотите пойти по маршруту метакласса, вы можете создать тот, который игнорирует все дополнительные аргументы почти для любого класса, путем интроспекции сигнатуры вызова метода __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)

Этот метакласс будет работать для любого класса Python, но если вы должны создать подкласс во встроенном типе, тогда методы __new__ или __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:
        # ...
person Martijn Pieters    schedule 05.08.2019
comment
Это все равно не удастся, если вы предоставите больше аргументов, чем ожидалось. Вы должны добавить *args и **kwargs к __init__, чтобы он работал так, как хотел OP. В качестве примечания: Optional[int] лучше, чем Union[int, None] - person Shai Avr; 05.08.2019
comment
@ShaiAvr: Optional[int] указывает, что age имеет значение по умолчанию, если вы его опустите. Я прямо хочу задокументировать это как необязательный. - person Martijn Pieters; 05.08.2019
comment
@ShaiAvr: и упс, я забыл включить аргументы *args, **kwargs в __init__. Исправлено сейчас. - person Martijn Pieters; 05.08.2019
comment
Согласно docs, Optional[int] эквивалентно Union[int, None], и это не относится к значениям по умолчанию. - person Shai Avr; 05.08.2019
comment
@ShaiAvr: Optional[int] действительно эквивалент, я просто говорю об этом прямо здесь. Это личное предпочтение. - person Martijn Pieters; 05.08.2019
comment
Есть ли другой способ сделать это без использования обычного init? - person Emrah Tema; 05.08.2019
comment
@EmrahTema: Я не уверен, о чем вы там спрашиваете. Я показал вам два варианта, о других мне не известно. - person Martijn Pieters; 05.08.2019
comment
Примечание. Возможно, имеет смысл использовать operator.index вместо int для self.age преобразования / проверки. В аннотации типа указано, что должно быть int или None, но использование int для преобразования приведет к принуждению не-int-подобных вещей к int (например, strs, которые выглядят как числа, _8 _ / _ 9 _ / _ 10_, вызывая усечение). operator.index более консервативен; он принимает int (и его подклассы) и вещи, которые могут быть без потерь преобразованы только в int, возвращая int (или его подкласс), более точно соответствующий аннотации типа. - person ShadowRanger; 07.08.2019
comment
@ShadowRanger обратите внимание, что OP передает строку для возраста; похоже, что они и здесь приемлемы. В этот момент тип сигнатуры метода __init__ для age действительно должен отличаться от типа поля экземпляра. Однако это не то, что могут обслуживать классы данных. - person Martijn Pieters; 07.08.2019
comment
Поэтому, если подсказка типов должна поддерживаться должным образом, правильный способ справиться с этим - использовать отдельный фабричный метод для класса, который затем получает подсказку типа соответствующим образом. - person Martijn Pieters; 07.08.2019
comment
@MartijnPieters: Хороший вопрос. Фабричный метод, который выполняет более либеральные преобразования со строгими преобразованиями в самом классе, вероятно, является правильным решением; как есть, средства проверки статического типа должны помечать код OP, даже если он выполняется без ошибок. - person ShadowRanger; 07.08.2019