Создание класса Python с обязательными и необязательными атрибутами?

1

Я хочу создать несколько классов 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)

Теги:
metaclass

2 ответа

0

Способы использования метакласса для проверки свойств открыты, и только код, запущенный во время выполнения, может определить, будет ли аргумент или действителен. Это означает, что нет возможности для 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. 
0

Вы можете указать необязательные и требуемые аргументы ключевых слов в функции __init__, если это вам поможет.

def __init__(self, *, req_kwarg1, reqkwarg2, **opt_kwargs):
    # check for types of required arguments

См. Это для получения дополнительной информации.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню