首页 > 解决方案 > 如何通过向 Python 的内置属性类添加参数而不破坏它来扩展它?

问题描述

我正在尝试编写一个 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_numberint因为json模块可以自动处理。但在我的实际情况中,有更复杂的对象,我用装饰器将其标记为 JSON 可序列化并实现自定义序列化/反序列化方法。然而,为了使其工作,代码确实需要知道属性的预期类型。

例如,这允许我创建 JSON 可序列化类的层次结构。我当前的实现允许从 JSON 存储和加载整个数据结构而不会丢失任何信息。

现在我想更进一步,在尝试设置 a 的值时也可以验证数据的格式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对象。但是我该如何解决这个问题并完成我想要的呢?

标签: pythonpython-decorators

解决方案


这是我的实现。它使用Descriptor HOWTOproperty中概述的 Python 实现。我为此添加了一个包装器,它接受在设置或获取值时将调用的函数或类型。在包装器的闭包中,我定义了具有. 这是给外部包装器的功能/类型。最后,这个属性描述符类由设置了属性的包装器返回。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)只强制类型而不尝试转换无效值。


推荐阅读