Понимание __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()
Зачем мне нужен класс descriptor?
Что здесь instance
и owner
? (в __get__
). Для чего нужны эти параметры?
Как бы я вызвал / использовал этот пример?
Переведено автоматически
Ответ 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
obj_instance
's __dict__
, тогдаСледствием такого порядка поиска является то, что дескрипторы, не относящиеся к данным, такие как функции / методы, могут быть переопределены экземплярами.
Мы узнали, что дескрипторы - это объекты с любым из __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()
- Зачем мне нужен класс 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)
- Что здесь означает экземпляр и владелец? (в get). Каково назначение этих параметров?
instance
является экземпляром владельца, вызывающего дескриптор. Владелец - это класс, в котором объект descriptor используется для управления доступом к точке данных. Смотрите описания специальных методов, определяющих дескрипторы, рядом с первым абзацем этого ответа для получения более описательных имен переменных.
- Как бы я вызвал / использовал этот пример?
Вот демонстрация:
>>> 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 в температурном классе... за исключением, может быть, чисто академического упражнения.