Pass Descriptor by parameter to another Descriptor

Asked

Viewed 48 times

1

Let’s imagine that in a class I have two properties of the Scriptor type. The first of them ensures that the property in question is negative the second must ensure that the property associated with it will not be greater than the previous property.

To illustrate:

class AnyClass:

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

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

That is to say, prop2 has to be smaller 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 (NaoMenorQue(#prop1)). There in the body of the Scriptor, at the time an assignment is made (prop2 = 10), in the method __set__ the comparison is made to see if value is less than the value passed at startup 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 attribute self.__dt is receiving an object of the type NaoMenorQueZero, that is, the Scriptor itself and not only its value. And at the time of comparison the __set__ is making int <= NaoMenorQuezero instead of int <= int. To solve this I could overload the operators <,>,== and != in Descriptor NaoMenorQueZero but that would be hard work because NaoMenorQue can be used on several occasions and I would have the obligation to overload the operators of all descroptors that would be passed as parameter to it.

Some other solution?

1 answer

1


The instance of a Descriptor is a property of the class of the object 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 saved in the instance that Descriptor is associated with - which is why the Descriptor methods __get__ , __set__ and __del__ receive the parameter "instance".

(You may 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 logical part to erase the data referring to instances that no longer exist).

In general, as it is in your own code, these values are stored in the instance itself - or with direct access to the attribute, with the operator ., or 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 verify - is retrieved from the instance, not from Descriptor. You can either "hardcodar" the access to that value, or rather simply use Scriptor itself to access the value - which is better.

Another cool thing you can use, since you’re writing your Scriptors, is to implement method __set_name__, which exists from Python 3.6 onwards, and is called when the class that defined the Descriptor is created, with the name it will have. With this, you can internally store the name of the Descriptor, and use a variant of that name (for example, with a prefix), to store the related values in the __dict__ of the instance.

Before I put a concrete example, one last tip: don’t use __ as a prefix of variable names in Python to find them to be "private variables". This prefix makes name mangling - something that has not been done to indicate that a variable is private - so that a class can have independent variables from other classes that it inherits from itself. Python has no private variables - has a convention of which names started with _ (a single underline) are private - and this signals your class users not to access these 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)
     ...

What answers your question is how to recover the value of the other Scriptor: simple call is made explicit to the method __get__ of it, passing the parameters of instance and Owner.

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

But then - maybe 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, as string itself. That way you can use the builtin getattr python, and retrieve the value that is in the instance for the other attribute, which does not need 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("...")
  • Congratulations John! As always killing on these more complex themes.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.