Я хочу создать несколько классов python, в которых есть много обязательных и необязательных полей. Во время инициализации я хочу проверить, прошли ли все необходимые атрибуты и проверили валидацию всех атрибутов.
Это решение, с которым я столкнулся. Но проблема с этим, IDE больше не предлагает автоматические подсказки атрибутов класса.
Есть ли лучшее решение для решения моей проблемы?
class Field(object):
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
class IntField(Field):
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError(f'expecting integer in {self.name} got {value}')
instance.__dict__[self.name] = value
class StrField(Field):
def __set__(self, instance, value):
if not isinstance(value, six.string_types):
raise ValueError(f'expecting string in {self.name} got {value}')
instance.__dict__[self.name] = value
class PropertiesCreator(type):
def __new__(mcs, classname, baseclasses, props):
c_props = props.get('_required_properties', ())
for name, cname in c_props:
props[name] = cname()
c_props = props.get('_optional_properties', ())
for name, cname in c_props:
props[name] = cname()
return super(PropertiesCreator, mcs).__new__(mcs, classname, baseclasses, props)
class Schema(object):
_required_properties = ()
_optional_properties = ()
def __init__(self, **kwargs):
for name, _ in self._required_properties:
try:
value = kwargs.pop(name)
except KeyError:
raise RequiredAttributeError("Required %s is missing" % name)
else:
setattr(self, name, value)
for name, _ in self._optional_properties:
try:
value = kwargs.pop(name)
except KeyError:
pass
else:
setattr(self, name, value)
if kwargs:
raise InvalidAttributeError("Invalid items ")
def required_attributes(self):
return self._required_properties
def optional_attributes(self):
return self._optional_properties
@six.add_metaclass(PropertiesCreator)
class CheckpointSchema(Schema):
_required_properties = (
("name", StrField),
("is_enabled", BoolField)
)
def __init__(self, **kwargs):
super(CheckpointSchema, self).__init__(**kwargs)
Способы использования метакласса для проверки свойств открыты, и только код, запущенный во время выполнения, может определить, будет ли аргумент или действителен. Это означает, что нет возможности для IDE, возможно, угадать вашу желаемую схему проверки и работать вместе с ней - вы могли бы обеспечить ее только путем написания расширения вашей среды разработки, которое будет сочетаться с вашим кодом.
ОДНАКО, есть хорошая вероятность, что если IDE импортирует модуль, содержащий ваш класс, используя сам Python (а не пытающийся статически проверять фактический введенный код), если ваши объектные классы имеют хорошо сформированный __init__
метод со всеми необходимыми параметры как обязательные, и все необязательные параметры со значением по умолчанию по умолчанию, это сработает.
Поэтому вы должны написать свой метакласс таким образом, чтобы он фактически генерировал функцию __init__
с соответствующей подписью. Хотя есть способы программно установить сигнатуру функции с types.CodeType
из inspect
модуля или создать функцию с использованием types.CodeType
+ types.FunctionType
, это займет много времени, чтобы получить работу и стать менее читаемым, чем просто составить строку с помощью требуемую подпись метода и использовать exec
чтобы заставить его прокатиться. Более того, это может использовать аннотации, поэтому вы можете получить дополнительную помощь от IDE.
Таким образом, ваш код может стать чем-то вроде (извините - Python 3.6), чтобы сделать его совместимым с Python 2.7, вам понадобится гораздо больше ввода текста):
from textwrap import dedent as D
...
class IntField(Field):
type_ = int
class StrField(Field):
type = str
...
_SENTINEL = object()
def _check_attrs(**kwargs):
self = kwargs.pop("self")
for name, _ in self._required_properties:
try:
value = kwargs.pop(name)
except KeyError:
raise RequiredAttributeError("Required %s is missing" % name)
else:
setattr(self, name, value)
for name, _ in self._optional_properties:
value = kwargs.pop(name)
if value is not _SENTINEL:
setattr(self, name, value)
if kwargs:
raise InvalidAttributeError("Invalid items ")
class PropertiesCreator(type):
def __new__(mcs, classname, baseclasses, props):
c_props = props.get('_required_properties', ())
for name, cname in c_props:
props[name] = cname()
required = ", ".join(f"{name}:{cname.type_}" for name, cname in c_props)
optional = []
c_props = props.get('_optional_properties', ())
for name, cname in c_props:
props[name] = cname()
optional.append((
optional = ", ".join(f"{name}:{cname.type_}={_SENTINEL}" for name, cname in c_props)
namespace = {"_check_attrs": _check_attrs}
exec(D(f"""\
def __init__(self, *, {required}, {optional}):
_check_attrs(locals())
"""))
props["__init__"] = namespace["__init__"]
return super(PropertiesCreator, mcs).__new__(mcs, classname, baseclasses, props)
class CheckpointSchema(Schema, metaclass=PropertiesCreator):
# Sorry - we are 18 months away from python 2 EOL .
# Do you really need Python 2 compatibility in a new project thic complex?
_required_properties = (
("name", StrField),
("is_enabled", BoolField)
)
# Leave __init__ undefined.
# if you need more code to run on '__init__', the metaclass will
# get a lot more complicated - maybe change th emetaclass to
# make use of '__new__' to init the properties, instead
# and leave '__init__' alone.
Вы можете указать необязательные и требуемые аргументы ключевых слов в функции __init__
, если это вам поможет.
def __init__(self, *, req_kwarg1, reqkwarg2, **opt_kwargs):
# check for types of required arguments
См. Это для получения дополнительной информации.