Вопрос-Ответ

Understanding __get__ and __set__ and Python descriptors

Понимание __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. Зачем мне нужен класс descriptor?


  2. Что здесь instance и owner? (в __get__). Для чего нужны эти параметры?


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


Переведено автоматически
Ответ 1

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

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

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

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

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

Официальная документация Python включает в себя статью о дескрипторах, в которой более подробно рассказывается об их работе, включая несколько примеров.

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

Ответ 2

Зачем мне нужен класс descriptor?


Это дает вам дополнительный контроль над тем, как работают атрибуты. Если вы привыкли к получателям и установщикам в 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 (например, если бы celsius было свойством, которое представляет собой тот же базовый механизм, но помещает весь исходный код внутри температурного класса), но это показывает, что можно сделать...

Ответ 3

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


Дескрипторы - это объекты в пространстве имен классов, которые управляют атрибутами экземпляра (такими как слоты, свойства или методы). Например:

class HasDescriptors:
__slots__ = 'a_slot' # creates a descriptor

def a_method(self): # creates a descriptor
"a regular method"

@staticmethod # creates a descriptor
def a_static_method():
"a static method"

@classmethod # creates a descriptor
def a_class_method(cls):
"a class method"

@property # creates a descriptor
def a_property(self):
"a property"

# even a regular function:
def a_function(some_obj_or_self): # creates a descriptor
"create a function suitable for monkey patching"

HasDescriptors.a_function = a_function # (but we usually don't do this)

Педантично, дескрипторы - это объекты с любым из следующих специальных методов, которые могут быть известны как "методы дескрипторов":


  • __get__: метод без дескриптора данных, например, для метода / функции

  • __set__: метод дескриптора данных, например, для экземпляра свойства или слота

  • __delete__: метод дескриптора данных, снова используемый свойствами или слотами

Эти объекты-дескрипторы являются атрибутами в других пространствах имен классов объектов. То есть они находятся в __dict__ класса object .

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

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

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

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

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

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

Но понимание того, как работают дескрипторы, может дать человеку большую уверенность в своем мастерстве владения 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__.

У дескриптора, не связанного с данными, нет ни __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 не являются дескрипторами данных:

>>> 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__']))

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

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

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

Однако, property это дескриптор данных:

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

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

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

obj_instance.attribute

  1. Сначала в приведенном выше примере проверяется, является ли атрибут дескриптором данных в классе экземпляра,

  2. Если нет, то нужно посмотреть, есть ли атрибут в obj_instance's __dict__ , тогда

  3. в конце концов, мы возвращаемся к дескриптору, не связанному с данными.

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

Краткое описание и следующие шаги

Мы узнали, что дескрипторы - это объекты с любым из __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. Зачем мне нужен класс descriptor?


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

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

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

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. Что здесь означает экземпляр и владелец? (в get). Каково назначение этих параметров?


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



  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 декоратора, который использует те же дескрипторы под капотом, но привносит поведение в реализацию класса owner (опять же, как определено выше):

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

Заключение

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

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

Ответ 4

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

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

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

введите описание изображения здесь

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


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

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

Вот тут-то и пригодятся дескрипторы:


  • Дескрипторы данных, которые содержат оба __get__ и __set__.

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

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

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

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


Зачем мне нужен класс descriptor?


В большинстве случаев вам не нужно писать классы дескрипторов! Однако вы, вероятно, самый обычный конечный пользователь. Например, функции. Функции являются дескрипторами, вот как функции могут использоваться в качестве методов с 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 дескриптором is (из руководства по дескриптору "Свойства"):

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

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


Что здесь instance и owner? (в __get__). Для чего нужны эти параметры?


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


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

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

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


  • второй аргумент 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) для хранения значения, чтобы избежать его совместного использования. Однако в некоторых случаях может потребоваться совместное использование значения между экземплярами (хотя на данный момент я не могу придумать сценарий). Однако это практически не имеет смысла для свойства celsius в температурном классе... за исключением, может быть, чисто академического упражнения.

python