Понимание __get__ и __set__ и дескрипторов Python - PullRequest
279 голосов
/ 26 сентября 2010

Я пытаюсь понять, что дескрипторы 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. Как бы я назвал / использовал этот пример?

Ответы [ 7 ]

126 голосов
/ 26 сентября 2010

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

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

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

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

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

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

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

96 голосов
/ 04 августа 2013

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

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

56 голосов
/ 01 января 2016

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

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

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

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

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

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

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

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

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

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

In Depth: What IsДескрипторы?

Дескриптор - это объект с любым из следующих методов (__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))

A Дескриптор данных имеет __set__ и / или __delete__.
A Дескриптор без данных не имеет ни __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 __dict__, затем
  3. в конечном итоге возвращается к Non-Data-Descriptor.

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

Повтор и следующие шаги

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

Ваш дескриптор гарантирует, что у вас всегда есть float для этого атрибута класса 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

Заключение

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

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

8 голосов
/ 25 марта 2019

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

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

Это представляет поиск атрибута 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

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

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

6 голосов
/ 31 июля 2017

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

Вдохновлен Свободный Питон Бучано Рамалхо

Представьте, что у вас есть такой класс

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's 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

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

0 голосов
/ 16 марта 2019

Вы бы увидели 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__)
0 голосов
/ 08 марта 2014

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...