简体   繁体   中英

How can I extend Python's built-in property class by adding an argument to it without breaking it?

I am trying to write a specialized subclass of Python's built-in property which takes an input argument when decorating a function like this:

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

I've been using the examples on https://realpython.com/primer-on-python-decorators/ as a reference to try to accomplish this as follows:

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

This setup allows me to retrieve the explicit type specified for the property in a class that uses my special_property like this:

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')

The reason I'm doing this, is to make it possible to create a JSON serializable class by decorating properties that should be stored/loaded in a JSON file. In this example, there would be no need to explicitly ensure the type of my_number is an int , because the json module can handle that automatically. But in my actual case, there are more complex objects that I mark as JSON serializable with a decorator and implement custom serialization/deserialization methods. In order for this to work, however, the code does need to know what type is expected for the properties.

This allows me, for example, to create hieriarchies of JSON serializable classes. My current implementation allows for the entire data structure to be stored and loaded from JSON without losing any information.

Now I want to go one step further and make it possible to validate the format of data as well when attempting to set the value of a specialized_property . Hence, I want to be able to do this:

@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

This would allow me to, for example, to ensure that a list of numbers loaded from a JSON file has the right size.

However, due to the code making the addition of the property_type argument work, it is now impossible to use @my_number.setter . If I try to run the code, I get:

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

This makes sense to me, because of overriding the __call__ method and returning the function object. But how do I get around this and accomplish what I want?

Here's my implementation. It uses the Python implementation of property outlined in the Descriptor HOWTO . I've added a wrapper around this that accepts a function or type which will be called when setting or getting a value. In the wrapper's closure, I defined the special_property_descriptor class which has a .type . This is the function/type given to the wrapper outside. Finally, this property descriptor class is returned by the wrapper having had it's .type attribute set.

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

Obviously, you can amend the functionality here. In my example, the descriptor will attempt to cast the value to the desired type before setting/getting it. If you want to, you could do an isinstance(value, self.type) to only enforce type and not attempt to convert invalid values.

Don't mess with property. Keep track of the types separately in your own class variable.

See the prop_type class variable below for an illustration.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM