Как я могу расширить встроенный класс свойств Python, добавив в него аргумент, не нарушая его? - PullRequest
1 голос
/ 21 мая 2019

Я пытаюсь написать специализированный подкласс встроенного в Python property, который принимает входной аргумент при декорировании функции, подобной этой:

@special_property(int)
def my_number(self):
    return self._number

Я использовал примеры на https://realpython.com/primer-on-python-decorators/ в качестве ссылки, чтобы попытаться выполнить это следующим образом:

class special_property(property):
    def __init__(self, property_type):
        super().__init__()
        self.type = property_type

    def __call__(self, fn):
        fn.type = self.type
        return fn

Эта настройка позволяет мне получить явное type, указанное для свойства в классе, который использует мой special_property, например:

class Object(object):
    def __init__(self):
        super().__init__()
        self._number = 0

    @special_property(int)
    def my_number(self):
        return self._number

    def load_from_json(self, json_file):
        with open(json_file, 'r') as f:
            state = json.load(f)

        for name, value in state.items():
            if hasattr(self, name):
                klass = self.__class__.__dict__[name].type
                try:
                    self.__setattr__(name, klass(value))
                except:
                    ValueError('Error loading from JSON')

Причина, по которой я это делаю, состоит в том, чтобы сделать возможным создание сериализуемого класса JSON путем декорирования свойств, которые должны храниться / загружаться в файл JSON. В этом примере не нужно было бы явно гарантировать, что тип my_number является int, потому что модуль json может обрабатывать это автоматически. Но в моем случае есть более сложные объекты, которые я отмечаю как сериализуемые JSON с помощью декоратора и реализующие собственные методы сериализации / десериализации. Однако, чтобы это работало, код должен знать, какой тип ожидается для свойств.

Это позволяет мне, например, создавать иерархии сериализуемых классов JSON. Моя текущая реализация позволяет хранить и загружать всю структуру данных из JSON без потери какой-либо информации.

Теперь я хочу сделать еще один шаг и сделать возможным проверку формата данных также при попытке установить значение specialized_property. Следовательно, я хочу быть в состоянии сделать это:

@specialized_property(int)
def my_number(self):
    return self._number

@my_number.setter
def my_number(self, value):
    if value < 0:
        raise ValueError('Value of `my_number` should be >= 0')
    self._number = value

Это позволило бы мне, например, убедиться, что список чисел, загруженных из файла JSON, имеет правильный размер.

Однако из-за кода, добавляющего аргумент property_type, теперь невозможно использовать @my_number.setter. Если я пытаюсь запустить код, я получаю:

AttributeError: 'function' object has no attribute 'setter'

Это имеет смысл для меня, потому что переопределяет метод __call__ и возвращает объект function. Но как мне обойти это и выполнить то, что я хочу?

Ответы [ 2 ]

2 голосов
/ 21 мая 2019

Вот моя реализация.Он использует реализацию Python property, описанную в дескрипторе HOWTO .Я добавил обертку для этого, которая принимает функцию или тип, который будет вызываться при установке или получении значения.В закрытии оболочки я определил класс special_property_descriptor, который имеет .type.Это функция / тип, данная оболочке снаружи.Наконец, этот класс дескриптора свойства возвращается оболочкой, для которой установлен атрибут .type.

def special_property(cls):
    class special_property_descriptor(object):
        type = cls
        def __init__(self, fget=None, fset=None, fdel=None, doc=None):
            self.fget = fget
            self.fset = fset
            self.fdel = fdel
            if doc is None and fget is not None:
                doc = fget.__doc__
            self.__doc__ = doc

        def __set_name__(self, owner, name):
            self.name = name

        def __get__(self, obj, objtype=None):
            if obj is None:
                return self
            if self.fget is None:
                raise AttributeError('unreadable attribute')
            r = self.fget(obj)
            try:
                return self.type(r)
            except Exception:
                raise TypeError(f'attribute {self.name} must '
                                f'of type {self.type.__name__}') 

        def __set__(self, obj, value):
            try:
                value = self.type(value)
            except Exception:
                raise TypeError(f'attribute {self.name} must '
                                f'of type {self.type.__name__}')
            if self.fset is None:
                raise AttributeError('can\'t set attribute')
            self.fset(obj, value)

        def __delete__(self, obj):
            if self.fdel is None:
                raise AttributeError('can\'t delete attribute')
            self.fdel(obj)

        def getter(self, fget):
            return type(self)(fget, self.fset, self.fdel, self.__doc__)

        def setter(self, fset):
            return type(self)(self.fget, fset, self.fdel, self.__doc__)

        def deleter(self, fdel):
            return type(self)(self.fget, self.fset, fdel, self.__doc__)
    return special_property_descriptor

Очевидно, вы можете изменить функциональность здесь.В моем примере дескриптор будет пытаться привести значение к желаемому типу до его установки / получения.Если вы хотите, вы можете сделать isinstance(value, self.type) только для принудительного применения типа и не пытаться преобразовать недопустимые значения.

0 голосов
/ 21 мая 2019

Не связывайтесь с собственностью.Отслеживайте типы отдельно в своей собственной переменной класса.

См. Ниже переменную класса prop_type для иллюстрации.

import json

class Object(object):
    prop_type = {}
    def __init__(self):
        super().__init__()
        self._number = 0

    @property
    def my_number(self):
        return self._number
    prop_type['my_number'] = int


    @my_number.setter
    def my_number(self, value):
        if self.prop_type['my_number'] != int:
            raise ValueError("Not an int")
        if value < 0:
            raise ValueError('Value of `my_number` should be >= 0')
        self._number = value
...