Pass descriptor by parameter to another descriptor

2

Let's say that in a class I have two descriptor properties. The first one ensures that the property in question is either negative the second must ensure that the property associated with it will not be greater than the previous property.

For example:

class AnyClass:

    prop1 = NaoMenorQueZero()
    prop2 = NaoMenorQue(#prop1)

    def __init__(self, prop1, prop2):
        self.prop1 = prop1
        self.prop2 = prop2

That is, prop2 must be less than prop1 . And in the intention of having a descriptor that can be used for other situations pessei in receiving the value to be compared by parameter (% with%). In the body of the descriptor, at the time an assignment is made ( NaoMenorQue(#prop1) ), in the prop2 = 10 method the comparison is made to see if __set__ is less than the value passed at boot time:

def __init__(self, dt):
    self.__dt = dt

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

def __set__(self, instance, value):
    if value <= self.__dt:
        instance.__dict__[self.__name] = value
    else:
        raise ValueError()

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

The problem with this logic is that the value attribute is receiving an object of type self.__dt , that is, the descriptor itself, and not just its value. And at the time of the comparison the NaoMenorQueZero is doing __set__ instead of int <= NaoMenorQuezero . To work around this I could overload the <, > operators, == and! = in the int <= int descriptor but this would be hard work since NaoMenorQueZero can be used on several occasions and I would have the obligation to overload the operators of all the descroptors that would be passed as a parameter to it.

Any other solution?

    
asked by anonymous 31.12.2018 / 15:14

1 answer

1

The instance of a descriptor is a property of the object class that uses the descriptors. This means that if you are creating a descriptor to store an individual value in each instance, that value has to be stored in the instance to which the descriptor is associated - so that the methods descriptor __get__ , __set__ and __del__ are given the "instance" parameter.

(You can have another "global" object for your descriptord that saves the data separate from the instances - but it is not worth it because you would have to create a whole logic aside to delete the data for instances that do not there are more).

In general, as it is in your own code, these values are saved in the instance itself - or with direct access to the attribute, with the . operator, or it can be using the __dict__ of the instance as you do.

but then the value stored by the first descriptor - the one you want to check - is retrieved from the instance, not from the descriptor. You can either "hardcode" access to that value, or rather simply use the descriptor itself to access the value - which is better.

Another cool thing you can use, since you are writing your descriptors, is to implement __set_name__ method, which exists from Python 3.6 onwards, and is called when the class that defined the descriptor is created, named that he will have. This allows you to internally store the descriptor name, and use a variant of that name (for example, with a prefix), to store the related values in __dict__ of the instance.

Before I put a concrete example, one last tip: do not use __ as a prefix of variable names in Python to think they are "private variables". This prefix does name mangling - it is something that was not meant to indicate that a variable is private - it is for a class to have variables independent of other classes that inherit from it. Python has no private variables - it has the convention that names started with _ (a single underline) are private - and this signals that users of their classes do not access those names directly. >

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

    def __get__(self, instance, owner):
        if instance is None: 
            return self
        return instance.__dict__["_" + self.name]


def NonNegative(MyDescriptorBase):

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("...")
        instance.__dict__["_" + self.name] = value


def NoLessThanDescriptor(MyDescriptorBase):
    def __init__(self, other_desc):
        self.other_desc = other_desc

    def __set__(self, instance, value):
        # Para recuperar o valor que o outro descriptor tem:
        instance_value = self.other_desc.__get__(instance, owner)
        if value < instance_value:
            raise ValueError("...")


# E então, você pode referenciar um descriptor 
# como parâmetro de outro no corpo de uma classe,
# respeitando a ordem (só é possível referenciar
# um descriptor que já foi definido)

class Exemplo:

     positivo = NonNegative()
     maior_numero = NoLessThanDescriptor(positivo)
     ...

The answer to your question is how to recover the value of the other descriptor: simply make the explicit call to the __get__ method, passing the instance and owner parameters.

(Of course you do not need to have a base class for descriptors, but in this example the __get__ and __set_name__ methods would be the same - no need to repeat code)

But then - perhaps you prefer this other approach, at the same time more generic and simpler: instead of passing another descriptor that will be the basis of comparison, simply pass the name of an attribute, like string itself. This way, you can use% builtin% of Python, and retrieve the value that is in the instance for the other attribute, which does not have to be a descriptor - can be any type of attribute:

def NoLessThanAttribute(MyDescriptorBase):

    def __init__(self, other_attr: str):
        self.other_attr = other_attr

    def __set__(self, instance, value):
        instance_value = getattr(instance, self.other_attr)
        if value < instance_value:
            raise ValueError("...")
    
31.12.2018 / 19:38