Если вы хотите, чтобы класс данных принимал произвольные дополнительные аргументы ключевого слова, вам необходимо либо определить свой собственный метод __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
__new__
, класс данных не требует явного__new__
метода.int or None
не является допустимым способом описания того, чтоage
является необязательным, используйтеOptional[age]
и назначьте значение по умолчанию. Необязательные значения должны следовать за обязательными полями. - person Martijn Pieters   schedule 05.08.2019age
должен быть необязательным, или вы просто хотите записать, что он будет установлен вNone
, еслиage=0
предоставлен? - person Martijn Pieters   schedule 05.08.2019None
? У каждого сотрудника должен быть возраст, и если вы имеете в виду, что возраст может быть неинициализированным, возможно, имеет смысл использоватьage = -1
. - person Shai Avr   schedule 05.08.2019None
- отличный выбор для возраста не хватает. - person Martijn Pieters   schedule 05.08.2019-1
, потому что это явно недействительный возраст, но я полагаю, что это личное предпочтение. - person Shai Avr   schedule 05.08.2019