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