Путаница о __get__ и __call__ в python

См. простой пример ниже:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner): 
         return self.value 
    def __set__(self, instance, value):
         self.value = float(value)
    def __call__(self):
         print('__call__ called')

class Temperature(object):
    celsius = Celsius()
    def __init__(self):
       self.celsius1 = Celsius()


T = Temperature()
print('T.celsius:', T.celsius)
print('T.celsius1:', T.celsius1)

output
T.celsius: 0.0
T.celsius1: <__main__.Celsius object at 0x023544F0>

Интересно, почему у них разные результаты. Я знаю, что T.celsius вызовет вызовы __get__ и T.celsius1 __call__.

Ответ 1

Различия заключаются в том, что первым атрибутом является атрибут класса, а второй - атрибут экземпляра.

По документация, Если объект, реализующий хотя бы первый из методов Descriptor (__get__, __set__ и __delete__) сохраняется в атрибуте класса, при обращении к нему будет вызываться метод __get__. Это не относится к атрибуту экземпляра. Вы можете узнать больше из инструкции.

Метод объекта __call__ объекта вступает в игру только тогда, когда объект вызывается как функция:

>>> class Foo:
...    def __call__(self):
...        return "Hello there!"
...
>>> f = Foo()
>>> f() 
'Hello There!'

Ответ 2

Из документация:

Следующие методы применяются только тогда, когда экземпляр класса содержащий метод (так называемый класс дескриптора), появляется в класс владельца (дескриптор должен быть либо в классе владельцев словарь или в словаре классов для одного из его родителей).

Таким образом, дескрипторы (т.е. объекты, реализующие __get__, __set__ или __delete__) должны быть членами класса, а не экземпляром.

При следующих изменениях:

Temperature.celsius2 = Celsius()

T = Temperature()
print('T.celsius:', T.celsius)
print('T.celsius1:', T.celsius1)
print('T.celsius2:', T.celsius2)

Вывод:

T.celsius: 0.0
T.celsius1: <__main__.Celsius object at 0x7fab8c8d0fd0>
T.celsius2:, 0.0

Другие ссылки:

Ответ 3

T.celcius как атрибут класса Temperature, поэтому он возвращает результат метода __get__ T.celcius, как и ожидалось.

T.celsius1 является атрибутом экземпляра T, поэтому он возвращает переменную, так как дескрипторы вызываются только для новых объектов или классов стиля.

Метод __call__ будет использоваться, если вы должны сделать T.celsius().

Ответ 5

Как уже отмечалось, экземпляры дескрипторов предназначены для использования в качестве атрибутов класса.

class Temperature(object):
    celsius = Celsius()
    def __init__(self):
       self.celsius1 = Celsius()

Если вы хотите, чтобы self.celsius1 имел пользовательское поведение, переопределите __getattr__ метод:

class Temperature(object):
    celsius = Celsius()
    def __getattr__(self, name):
       if name == 'celsius1':
           return ...