1

Consider this toy example where I use a descriptor to validate that a particular value doesn't exceed certain maximum value

class MaxValidator:
    def __init__(self, max=10):
        self.max = max
    def __set__(self, obj, value):
        if value > self.max:
            raise RuntimeError(f"value {value} must be smaller than {self.max}")
        obj._value = value
    def __get__(self, obj):
        return obj._value
class MyValue:
    value = MaxValidator(max=5)
    def __init__(self, value):
        self.value = value  # implicit validation takes place here

What happens now if I want a validator with a maximum value different than 5? The only solution I got was to create a class factory function:

def MyValueFactory(maximum):
    class _MyValue:
        value = MaxValidator(max=maximum)
        def __init__(self, value):
            self.value = value  # implicit validation takes place here
    return _MyValue
MyValue = MyValueFactory(5)  # this class has the same validator as the previous MyValue

I think a class factory function is a bit of overkill. Is there another pattern I can use when dealing with "parameterized" python descriptors?

Attempt to insert the descriptor in __init__

class MyValue:
    def __init__(self, value, maximum=5):
         self.value = MaxValidator(max=maximum)
         # but the following is not possible anymore
         self.value = value  #this is reassignment to self.value, the descriptor is lost 
jmborr
  • 1,215
  • 2
  • 13
  • 23
  • 1
    I might be missing something here, but why don't you initialize `value` inside `__init__`? This way you can pass an argument to it *and* it will not be shared among all instances. – DeepSpace Oct 20 '22 at 14:11
  • Note that I edited you code for 2 small mistakes: `MaxValidator.__init__` was missing `self`, and `max` should be `self.max` in the raised exception – DeepSpace Oct 20 '22 at 14:14
  • @DeepSpace Thanks for your corrections! I posted at the end of my question what I understood from your comment but this raises a problem with `self.value`. Did I misunderstood your comment? – jmborr Oct 21 '22 at 15:28

1 Answers1

1

Don´t forget that at execution time, the descriptors method for __get__ and __set__ have access to the instance and class where they live in.

So, all you need is a class attribute (and even an instance attribute) to configure the behavior of your descriptor class-wide. That can be done either with a fixed name, that will affect all descriptors of a certain kind, or better yet, a descriptor could check for its name - which can also be automatically attributed, and use that as a prefix for the maximum.

class MaxValidator:
    def __init__(self, default_max=10):
        self.default_max = default_max
    def __set_name__(self, owner, name):
        self.name = name
    def __set__(self, obj, value):
        # the line bellow retrieves <descriptor_name_maximum> attribute on the instance
        maximum = getattr(obj, self.name + "_maximum", self.default_max)
        if value > maximum:
            raise RuntimeError(f"value {value} must be smaller than {maximum}")
        obj._value = value
    def __get__(self, obj, owner):
        return obj._value
class MyValue:
    value = MaxValidator()
    def __init__(self, value, custom_max=5):
        self.value_maximum=custom_max
        self.value = value  # implicit validation takes place here

This is one other way of doing it. The factory function is not that terrible as well - but you seem to have forgotten the descriptor can check the class and instances themselves.

As for creating the descriptor itself inside __init__ - it is possible, but one have to keep in mind the descriptor must be a class attribute, and whenver you create a new instance of the class, the descriptor would be overriden with the new configurations:

class MyValue:
    # don't do this: each new instance will reset the configs
    # of previously created instances:
    def __init__(self, value, maximum=5):
         # The descriptor must be set in the class itself:
         self.__class__.value = MaxValidator(max=maximum)
         # the following will activate the descriptor defined above:
         self.value = value
  
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 1
    thanks a lot for the detailed example! I had decided to drop entirely the initialization of the descriptor, and store the maximum only in the instance (your `custom_max`), thus making it a required instance attribute. Your implementation is certainly more permissive thanks to the default value being passed to `getattr`. Actually, I didn't know `getattr` can receive a default value! – jmborr Oct 22 '22 at 17:24