简体   繁体   中英

Python descriptor for type checks and immutability

Read the Python Cookbook and saw descriptors, particularly the example for enforcing types when using class attributes. I am writing a few classes where that would be useful, but I would also like to enforce immutability. How to do it? Type checking descriptor adapted from the book:

class Descriptor(object):
    def __init__(self, name=None, **kwargs):
        self.name = name

        for key, value in kwargs.items():
            setattr(self, key, value)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = value


# by default allows None
class Typed(Descriptor):
    def __init__(self, expected_types=None, **kwargs):
        self.expected_types = expected_types

        super().__init__(**kwargs)

    def __set__(self, instance, value):
        if value is not None and not isinstance(value, self.expected_types):
            raise TypeError('Expected: {}'.format(str(self.expected_types)))

        super(Typed, self).__set__(instance, value)

class T(object):
    v = Typed(int)

    def __init__(self, v):
        self.v = v

Attempt #1: add a self.is_set attribute to Typed

# by default allows None
class ImmutableTyped(Descriptor):
    def __init__(self, expected_types=None, **kwargs):
        self.expected_types = expected_types
        self.is_set = False

        super().__init__(**kwargs)

    def __set__(self, instance, value):
        if self.is_set:
            raise ImmutableException(...)
        if value is not None and not isinstance(value, self.expected_types):
            raise TypeError('Expected: {}'.format(str(self.expected_types)))

        self.is_set = True

        super(Typed, self).__set__(instance, value)

Wrong, because when doing the following, ImmutableTyped is 'global' in the sense that it's a singleton throughout all instances of the class. When t2 is instantiated, is_set is already True from the previous object.

class T(object):
    v = ImmutableTyped(int)

    def __init__(self, v):
        self.v = v

t1 = T()
t2 = T()  # fail when instantiating

Attempt #2: Thought instance in __set__ refers to the class containing the attribute so tried to check if instance.__dict__[self.name] is still a Typed. That is also wrong.

Idea #3: Make Typed be used more similar to @property by accepting a ' fget ' method returning the __dict__ of T instances. This would require the definition of a function in T similar to:

@Typed
def v(self):
    return self.__dict__

which seems wrong.

How to implement immutability AND type checking as a descriptor?

Now this is my approach to the problem:

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

    def __init__(self, *, immutable=False, types=None)
        self.immutable == immutable is True
        self.types = types if types else []

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if self.immutable is True:
            raise TypeError('read-only attribute')
        elif not any(isinstance(value, cls)
                     for cls in self.types):
            raise TypeError('invalid argument type')
        else:
           instance.__dict__[self.name] = value

Side note: __set_name__ can be used to allow you to not specify the attribute name in initialisation. This means you can just do:

class Foo:
    bar = ImmutableTyped()

and the instance of ImmutableTyped will automatically have the name attribute bar since I typed for that to occur in the __set_name__ method.

Could not succeed in making such a descriptor. Perhaps it's also unnecessarily complicated. The following method + property use suffices.

# this also allows None to go through
def check_type(data, expected_types):
    if data is not None and not isinstance(data, expected_types):
        raise TypeError('Expected: {}'.format(str(expected_types)))

    return data

class A():
    def __init__(self, value=None):
        self._value = check_type(value, (str, bytes))

    @property
    def value(self):
        return self._value


foo = A()
print(foo.value)    # None
foo.value = 'bla'   # AttributeError
bar = A('goosfraba')
print(bar.value)    # goosfraba
bar.value = 'bla'   # AttributeError
class ImmutableTyped(object):

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

    def __init__(self, *, types=None):

        self.types = tuple(types or [])

        self.instances = {}

        return None

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):

        is_set = self.instances.setdefault(id(instance), False)

        if is_set:
            raise AttributeError("read-only attribute '%s'" % (self.name))

        if self.types:

            if not isinstance(value, self.types):
                raise TypeError("invalid argument type '%s' for '%s'" % (type(value), self.name))

        self.instances[id(instance)] = True

        instance.__dict__[self.name] = value

        return None

Examples:

class Something(object):

    prop1 = ImmutableTyped(types=[int])

something = Something()
something.prop1 = "1"

Will give:

TypeError: invalid argument type '<class 'str'>' for 'prop1'

And:

something = Something()
something.prop1 = 1
something.prop1 = 2

Will give:

TypeError: read-only attribute 'prop1'

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