Не понимаю реализацию по умолчанию не переопределяющих дескрипторов в Python

#python

#python

Вопрос:

Я не понимаю проектного решения, позволяющего сделать не переопределяющие дескрипторы неэффективными, когда существует атрибут экземпляра, например

 >>> class Descriptor:
...     def __get__(self, obj, objtype=None):
...             return 4
...
>>> class Class:
...     attr = Descriptor()
...     def __init__(self):
...             self.attr = 'instance attr'
...
>>> Class().attr # why doesn't this return 4?
'instance attr'
 

Для меня переопределение дескрипторов имеет смысл в том смысле, что если у нас есть дескриптор with __set__ , то это __set__ в значительной степени всегда используется для чего-то вроде obj.attr = <new value> .

Почему не переопределяющие дескрипторы не являются такими простыми в языке, т. Е. Почему они не __get__ всегда используются, например, при доступе к атрибутам obj.attr ?

Комментарии:

1. __get__ должно быть определено для объекта, свойство которого вы пытаетесь получить ( Class в данном случае), а не значение свойства, которое вы пытаетесь получить.

2. Непонятно, почему вы ожидаете 4 , а 'instance attr' не в этом примере; какой эффект self.attr = 'instance attr' , по вашему мнению, должен быть в противном случае, или вы думаете, что это не должно иметь никакого эффекта (т. Е. Сбой без сбоев)?

3. @Aplet123 Это неправда. Если вы удалите self.attr = 'instance attr' строку, код будет работать так, как ожидалось. См. Это руководство по дескрипторам в python .

4. @kaya3, я думаю self.attr = 'instance attr' , следует выполнить традиционное присвоение атрибуту экземпляра, но любое извлечение все равно должно основываться на существующем дескрипторе __get__ .

5. Почему это когда-либо было желаемым поведением? Если у дескриптора нет __set__ метода, то он не переопределяет то, что должно произойти при назначении атрибута, поэтому вы должны ожидать, что присвоение атрибута будет иметь свое обычное поведение, т. Е. Если вы назначаете значение, а затем получаете к нему доступ, вы получаете только что присвоенное значение. Если это не то поведение, которое вы хотите, чтобы присваивание имело, тогда дескриптор должен определять поведение.

Ответ №1:

Вот как я получаю выгоду от этого:

 class Lazy():
    def __init__(self, function):
        self.name = function.__name__
        self.function = function
    
    def __get__(self, obj, type=None):
        print("get value by heavier __get__")
        obj.__dict__[self.name] = self.function(obj)
        return obj.__dict__[self.name]
        
class C:
    
    @Lazy
    def attr(self):
        # doing heavy calculation
        return "heavy calculation result"
 
 >>> c = C()
>>> c.attr
get value by heavier __get__
heavy calculation result

>>> c.attr # get value by __dict__
heavy calculation result
 

В логике поиска атрибутов объекта __dict__ имеет более высокий приоритет, чем дескриптор, не связанный с данными (возможно, не переопределяющий дескриптор в вашем вопросе). При первой попытке доступа к атрибуту, поскольку атрибут не существует в __dict__ , атрибут может быть вычислен в __get__ и кэширован в традиционном location( __dict__ ) . При более низком __get__ приоритете более поздний поиск будет возвращен __dict__ напрямую.

__get__ разместите сложную, тяжелую логику поиска внутри. Итак, если у вас есть два подхода для получения одинакового значения, более тяжелый должен иметь более низкий приоритет.

Ответ №2:

TLDR: определение переменной экземпляра, т.Е. self.attr = 'instance attr' переопределяет определение переменной класса, т.е. attr = Descriptor() Всякий раз, когда происходит столкновение имен. Либо измените имена, либо используйте classname для доступа к переменным класса следующим образом

 Class.attr # instead of Class().attr
 

Длинный ответ

Есть переменные класса и переменные экземпляра. Посмотрите это видео для быстрого понимания. Доступ к переменным класса можно получить, используя classname или используя экземпляр (если он еще не был переопределен), но к переменным экземпляра можно получить доступ только с помощью экземпляров.

В вашем примере следующая строка устанавливает переменную класса attr , которая при обращении к которой будет возвращена 4

 attr = Descriptor() # returns 4 because of __get__() definition
 

Однако с помощью этой следующей строки внутри конструктора Class т.Е. __init__() Вы настраиваете переменную экземпляра с тем же именем attr , переопределяя таким образом переменную класса.

 self.attr = 'instance attr'
 

Всякий раз, когда вы обращаетесь к свойствам экземпляра, сначала проверяются переменные экземпляра, и только если они не найдены, тогда проверяются переменные класса. Class() создает экземпляр, и so Class().attr будет искать в attr качестве переменной экземпляра, и поскольку она определена, 'instance attr' будет возвращено значение . Если бы вы не определили его в __init__() , то он не был бы найден, и поэтому, поскольку следующий шаг attr будет ищется как переменная класса, и поскольку это будет определено со значением 4 , оно будет возвращено.