Понимание дескрипторов __get__ и __set__ и Python

Я пытаюсь понять, что такое дескрипторы 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)


class Temperature(object):
    celsius = Celsius()
  1. Зачем мне нужен класс дескриптора?

  2. Что такое instance и owner здесь? (в __get__). Какова цель этих параметров?

  3. Как бы я позвонил/использовал этот пример?

Ответ 1

Дескриптор - это то, как реализован тип property Python. Дескриптор просто реализует __get__, __set__ и т.д., А затем добавляется к другому классу в своем определении (как вы делали выше с классом Temperature). Например:

temp=Temperature()
temp.celsius #calls celsius.__get__

Доступ к свойству, которому вы присвоили дескриптор (в приведенном выше примере - по celsius), вызывает соответствующий метод дескриптора.

instance в __get__ является экземпляром класса (поэтому выше, __get__ получит temp, в то время как owner - это класс с дескриптором (так что это будет Temperature).

Вам нужно использовать класс дескриптора для инкапсуляции логики, которая его поддерживает. Таким образом, если дескриптор используется для кэширования какой-либо дорогостоящей операции (например), он может хранить значение в себе, а не в своем классе.

Статью о дескрипторах можно найти здесь.

РЕДАКТИРОВАТЬ: Как jchl указал в комментариях, если вы просто попробуйте Temperature.celsius, instance будет None.

Ответ 2

Зачем мне нужен класс дескриптора?

Это дает вам дополнительный контроль над тем, как работают атрибуты. Если вы, например, привыкли к геттерам и сеттерам в Java, то это Python способ сделать это. Одним из преимуществ является то, что он выглядит для пользователей как атрибут (нет изменений в синтаксисе). Таким образом, вы можете начать с обычного атрибута, а затем, когда вам нужно что-то сделать, переключиться на дескриптор.

Атрибут является просто изменяемым значением. Дескриптор позволяет вам выполнить произвольный код при чтении или установке (или удалении) значения. Таким образом, вы можете использовать его для сопоставления атрибута с полем в базе данных, например, своего рода ORM.

Другое использование может быть отказом принять новое значение, генерируя исключение в __set__ - фактически делая "атрибут" доступным только для чтения.

Что такое instance и owner здесь? (в __get__). Какова цель этих параметров?

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

Дескриптор определяется в классе, но обычно вызывается из экземпляра. Когда он вызывается из экземпляра, устанавливаются и instance и owner (и вы можете определить owner из instance так что это кажется бессмысленным). Но когда вызывается из класса, устанавливается только owner - вот почему он там.

Это необходимо только для __get__ потому что это единственный, который может быть вызван в классе. Если вы устанавливаете значение класса, вы устанавливаете сам дескриптор. Аналогично для удаления. Вот почему owner не нужен.

Как бы я позвонил/использовал этот пример?

Ну, вот классный трюк с использованием похожих классов:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(Я использую Python 3; для Python 2 вы должны убедиться, что эти подразделения /5.0 и /9.0). Это дает:

100.0
32.0

Теперь есть другие, возможно, лучшие способы достижения того же эффекта в python (например, если цельсий был свойством, которое является тем же базовым механизмом, но помещает весь источник внутри класса Temperature), но это показывает, что можно сделать...

Ответ 3

  Я пытаюсь понять, что такое дескрипторы Python и для чего они могут быть полезны.

Дескрипторы - это атрибуты класса (например, свойства или методы) с любым из следующих специальных методов:

  • __get__ (метод дескриптора без данных, например, для метода/функции)
  • __set__ (метод дескриптора данных, например, для экземпляра свойства)
  • __delete__ (метод дескриптора данных)

Эти объекты дескриптора могут использоваться в качестве атрибутов в других определениях классов объектов. (То есть они живут в __dict__ объекта класса.)

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

Функции/методы, связанные методы, property, classmethod и staticmethod используют эти специальные методы для управления доступом к ним через пунктирный поиск.

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

Другой дескриптор данных, member_descriptor, созданный __slots__, позволяет экономить память, позволяя классу хранить данные в изменяемой структуре данных типа кортежей вместо более гибкой, но занимающей много места __dict__.

Дескрипторы, не относящиеся к данным, обычно экземпляры, классы и статические методы, получают свои неявные первые аргументы (обычно называемые cls и self соответственно) из своего метода дескриптора, не являющегося данными, __get__.

Большинству пользователей Python необходимо изучить только простое использование, и им не нужно больше изучать или понимать реализацию дескрипторов.

В глубине: что такое дескрипторы?

Дескриптор - это объект с любым из следующих методов (__get__, __set__ или __delete__), предназначенный для использования с помощью точечного поиска, как если бы это был типичный атрибут экземпляра. Для объекта-владельца obj_instance с объектом descriptor:

  • obj_instance.descriptor вызывает
    descriptor.__get__(self, obj_instance, owner_class) возвращает value
    Вот как работают все методы и get в свойстве.

  • obj_instance.descriptor = value вызывает
    descriptor.__set__(self, obj_instance, value) возвращает None
    Вот как setter для свойства работает.

  • del obj_instance.descriptor вызывает
    descriptor.__delete__(self, obj_instance) возвращает None
    Так работает deleter для свойства.

obj_instance - это экземпляр, класс которого содержит экземпляр объекта дескриптора. self является экземпляром дескриптора (вероятно, только один для класса obj_instance)

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

def has_descriptor_attrs(obj):
    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))

def is_descriptor(obj):
    """obj can be instance of descriptor or the descriptor class"""
    return bool(has_descriptor_attrs(obj))

Дескриптор данных имеет __set__ и/или __delete__.
Non-data- дескриптор не имеет ни __set__, ни __delete__.

def has_data_descriptor_attrs(obj):
    return set(['__set__', '__delete__']) & set(dir(obj))

def is_data_descriptor(obj):
    return bool(has_data_descriptor_attrs(obj))

Примеры объектов встроенного дескриптора:

  • classmethod
  • staticmethod
  • property
  • функции в целом

Дескрипторы без данных

Мы можем видеть, что classmethod и staticmethod не являются дескрипторами data-:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

Оба имеют только метод __get__:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))

Обратите внимание, что все функции также не являются дескрипторами data-:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)

Дескриптор данных, property

Однако property является дескриптором data-:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

Порядок поиска с точками

Это важные различия, поскольку они влияют на порядок поиска для точечного поиска.

obj_instance.attribute
  1. Сначала вышеприведенный пример проверяет, является ли атрибут дескриптором data- в классе экземпляра,
  2. Если нет, то он смотрит, находится ли атрибут в obj_instance __dict__, тогда
  3. в конце концов он возвращается к не-data- дескриптору.

Следствием этого порядка поиска является то, что дескрипторы, не являющиеся data-, такие как функции/методы, могут быть переопределены экземплярами.

Резюме и следующие шаги

Мы узнали, что дескрипторы - это объекты с любым из __get__, __set__ или __delete__. Эти объекты дескриптора могут использоваться в качестве атрибутов в других определениях классов объектов. Теперь посмотрим, как они используются, используя в качестве примера ваш код.


Анализ кода из вопроса

Вот ваш код, затем ваши вопросы и ответы на каждый из них:

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)

class Temperature(object):
    celsius = Celsius()
  1. Зачем мне нужен класс дескриптора?

Ваш дескриптор гарантирует, что у вас всегда есть число с плавающей запятой для этого атрибута класса Temperature, и что вы не можете использовать del для удаления атрибута:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

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

class Temperature(object):
    celsius = 0.0

Это приводит вас к тому же поведению, что и в вашем примере (см. ответ на вопрос 3 ниже), но использует встроенную функцию Pythons (property) и будет рассматриваться как более идиоматическая:

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)
  1. Что такое экземпляр и владелец здесь? (в получить). Какова цель этих параметров?

instance является экземпляром владельца, который вызывает дескриптор. Владелец - это класс, в котором объект дескриптора используется для управления доступом к точке данных. См. описания специальных методов, которые определяют дескрипторы рядом с первым абзацем этого ответа, для более наглядных имен переменных.

  1. Как мне позвонить/использовать этот пример?

Вот демонстрация:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0

Вы не можете удалить атрибут:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

И вы не можете назначить переменную, которая не может быть преобразована в число с плавающей точкой:

>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02

В противном случае у вас есть глобальное состояние для всех экземпляров, которое управляется назначением любому экземпляру.

Ожидаемый способ достижения этой цели большинством опытных программистов на Python - использование декоратора property, который использует те же дескрипторы под капотом, но переносит поведение в реализацию класса владельца (опять же, как определено выше). ):

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

Который имеет точно такое же ожидаемое поведение исходного фрагмента кода:

>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02

Заключение

Мы рассмотрели атрибуты, которые определяют дескрипторы, разницу между дескрипторами data- и не data-, встроенные объекты, которые их используют, и конкретные вопросы об их использовании.

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

Ответ 4

Прежде чем углубляться в детали дескрипторов, может быть важно узнать, как работает поиск атрибутов в Python. Это предполагает, что у класса нет метакласса и что он использует реализацию по умолчанию __getattribute__ (оба могут использоваться для "настройки" поведения).

Наилучшим примером поиска атрибутов (в Python 3.x или для классов нового стиля в Python 2.x) в этом случае является Понимание метаклассов Python (ionel codelog). Изображение использует : вместо "ненастраиваемого поиска атрибутов".

Это представляет поиск атрибута foobar в instance из Class:

enter image description here

Здесь важны два условия:

  • Если класс instance имеет запись для имени атрибута и имеет __get__ и __set__.
  • Если instance не имеет записи для имени атрибута, но класс имеет ее и имеет __get__.

Вот где в него входят дескрипторы:

  • Дескрипторы данных, которые имеют __get__ и __set__.
  • Дескрипторы без данных, которые имеют только __get__.

В обоих случаях возвращаемое значение проходит через __get__, вызываемый с экземпляром в качестве первого аргумента и классом в качестве второго аргумента.

Поиск еще более сложен для поиска атрибутов класса (см., Например, Поиск атрибутов класса (в вышеупомянутом блоге)).

Давайте перейдем к вашим конкретным вопросам:

Зачем мне нужен класс дескриптора?

В большинстве случаев вам не нужно писать дескрипторные классы! Однако вы, вероятно, очень обычный конечный пользователь. Например функции. Функции являются дескрипторами того, как функции могут использоваться в качестве методов, когда self неявно передается в качестве первого аргумента.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Если вы посмотрите test_method на экземпляр, вы получите "связанный метод":

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

Аналогичным образом, вы также можете привязать функцию, вызвав ее метод __get__ вручную (не очень рекомендуется, только для иллюстративных целей):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

Вы даже можете вызвать этот "самосвязанный метод":

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

Обратите внимание, что я не предоставил никаких аргументов, и функция вернула экземпляр, который я связал!

Функции - это дескрипторы без данных!

Некоторыми встроенными примерами дескриптора данных могут быть property. Пренебрегая getter, setter и deleter дескриптором property (из Руководства по дескриптору "Свойства"):

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

Так как это дескриптор данных, он вызывается всякий раз, когда вы ищите "имя" property, и он просто делегирует функции, украшенные @property, @name.setter и @name.deleter (если присутствует).

В стандартной библиотеке есть несколько других дескрипторов, например staticmethod, classmethod.

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

Другим примером может быть свойство класса.

Одним забавным примером (с использованием __set_name__ из Python 3.6) также может быть свойство, которое допускает только определенный тип:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

Затем вы можете использовать дескриптор в классе:

class Test(object):
    int_prop = TypedProperty(int)

И немного поиграем с этим:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

Или "ленивая собственность":

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

Это случаи, когда перемещение логики в общий дескриптор может иметь смысл, однако можно также решить их (но, возможно, повторить некоторый код) другими способами.

What is instance и owner here? (in __get__). What is the purpose of these parameters?

Это зависит от того, как вы смотрите атрибут. Если вы посмотрите на атрибут в экземпляре, то:

  • вторым аргументом является экземпляр, в котором вы ищите атрибут
  • третий аргумент - это класс экземпляра

Если вы ищите атрибут в классе (при условии, что дескриптор определен в классе):

  • Второй аргумент - None
  • третий аргумент - это класс, в котором вы ищите атрибут

Так что в основном третий аргумент необходим, если вы хотите настроить поведение при поиске на уровне класса (потому что instance - это None).

Как бы я позвонил/использовал этот пример?

Ваш пример - это в основном свойство, которое допускает только значения, которые могут быть преобразованы в float и которые совместно используются всеми экземплярами класса (и класса), хотя можно использовать только "чтение" доступа к классу, в противном случае вы бы заменили экземпляр дескриптора):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

Именно поэтому дескрипторы обычно используют второй аргумент (instance) для хранения значения, чтобы избежать его совместного использования. Однако в некоторых случаях может быть желательным разделение значения между экземплярами (хотя я не могу думать о сценарии в данный момент). Однако для градуса Цельсия практически нет смысла в температурном классе... за исключением, может быть, чисто академического упражнения.

Ответ 5

Зачем мне нужен класс дескриптора?

Вдохновленный Свободным Питоном Buciano Ramalho

Воображение у вас есть такой класс

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

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

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

затем определите класс LineItem следующим образом:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

и мы можем расширить класс количества, чтобы сделать более общую проверку

Ответ 6

Я попробовал (с небольшими изменениями, как предложил) код от Andrew Cooke. (Я запускаю python 2.7).

Код:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

Результат:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

С Python до 3 убедитесь, что подкласс из объекта, который сделает дескриптор корректным, поскольку магия get не работает для классов старого стиля.

Ответ 7

Вы увидите https://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)