Еще немного о дескрипторах в Python

Не так давно на Хабре уже был перевод статьи Раймонда Хеттингера Руководство к дескрипторам. В этой статье я постараюсь рассмотреть вопросы, которые возникли у меня после прочтения. Будет немного примеров кода, собственно вопросов и ответов к ним. Для понимания того, о чем речь, вам нужно знать, что такое дескрипторы и зачем они.

Когда вызываются дескрипторы?


Рассмотрим следующий код:

>>> class M(type):
...   def __new__(cls, name, bases, dct):
...       dct['test'] = lambda self, x: x*2
...       return type.__new__(cls, name, bases, dct)
...
>>> class A(object):
...   def __init__(self):
...       self.__dict__['test2'] = lambda self, x: x*4
...   __metaclass__ = M
...
>>> A().test(2)
4
>>> A().test2(2)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes exactly 2 arguments (1 given)


* This source code was highlighted with Source Code Highlighter.

Что не так? Вроде добавляем функцию одинаково, используя словарь объекта. Почему не вызывается дескриптор для функции «test2»? Дело в том, что функция «test» определяется для словаря класса, а функция «test2» — для словаря объекта:

>>> 'test' in A.__dict__
True
>>> 'test2' in A.__dict__
False
>>> 'test' in A().__dict__
False
>>> 'test2' in A().__dict__
True


* This source code was highlighted with Source Code Highlighter.

Отсюда — первый ответ: функция "__get__" вызывается только для дескрипторов, которые являются свойствами класса, а не свойствами объектов этого класса.

Что является дескриптором?


Предыдущий ответ сразу вызывает вопрос — что значит «только для дескрипторов»? Я ведь не создавал никаких дескрипторов, я создал только функцию!

Ответ ожидаемий — функции в Питоне являются дескрипторами(если быть точнее — дескрипторами не данных):

>>> def foo(x):
...   return x * 2
...
>>> '__get__' in dir(foo)
True


* This source code was highlighted with Source Code Highlighter.

Bound/Unbound методы


И напоследок самое вкусное. Задача:

Есть объект некоего класса, к которому необходимо динамически добавить метод, которому, конечно, должен передаваться «self» параметр. Не представляется возможным добавлять этот метод в словарь класса (мы не хотим повлиять на другие объекты).

Решить «в лоб» не выходит:

>>> class A(object):
...   def __init__(self):
...       self.x = 3
...
>>> def add_x(self, add):
...   self.x += add
...   print 'Modified value: %s' % (self.x,)
...
>>> a = A()
>>> a.add_x = add_x
>>> a.x
3
>>> a.add_x(3)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: add_x() takes exactly 2 arguments (1 given)


* This source code was highlighted with Source Code Highlighter.

Получаем ожидаемую ошибку, которая подтверждает первый ответ — при обращении к свойству-дескриптору объекта не используется "__get__". Что же делать?

Поиграем немного с методом "__get__" функции, которую мы только что создали:

>>> add_x.__get__
<method-wrapper '__get__' of function object at 0x800f2caa0>
>>> add_x.__get__(a)
<bound method ?.add_x of &#60;__main__.A object at 0x800f2dc50>>
>>> add_x.__get__(a, A)
<bound method A.add_x of &#60;__main__.A object at 0x800f2dc50>>
>>> add_x.__get__(None, A)
<unbound method A.add_x>


* This source code was highlighted with Source Code Highlighter.

О, кажется это то, что нам надо! В предпоследней строчке мы получили «bound» метод — именно так смотрелся бы вызов «A().add_x», если в классе «A» был бы определен метод «add_x». А в последней строчке видим «ожидаемый» результат вызова «A.add_x». Теперь мы знаем, как добавить метод к объекту:

>>> a.add_x = add_x.__get__(a, A)
>>> a.add_x(3)
Modified value: 6


* This source code was highlighted with Source Code Highlighter.

Бинго!
И так, именно метод "__get__" для функций-дескрипторов создает «bound/unbound» методы классов и объектов. И нет здесь никакой магии :)

Литература


Что еще почитать? На самом деле — немного. Есть несколько глав, в которых рассказывается о дескрипторах, в Python Data Model (implementing-descriptors), ну и можно снова вспомнить действительно хорошую статью Хеттингера (оригинал).

P.S. Русский — не родной язык, замечания прошу в личку.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 29
  • 0
    >С предыдущего ответа сразу возникает вопрос — что значит для дескрипторов?

    Завис.
    • +2
      Пофиксав. Возможно теперь немного понятней.
      • 0
        В конце статьи: >>> P.S. Русский — не родной язык, замечания прошу в личку.

        Статья полезная, а потому граждане! Будьте добры!
      • 0
        Тут можно было и без дескрипторов обойтись.

        >>> import types
        >>> a.add_x = types.MethodType(add_x, a)
        • +1
          Да, можно. Идея в том, что б показать, как это работает.
        • 0
          Я так и не понял что такое дескриптор, может имело смысл хотябы простое определение дать?
          • 0
            В самом начале этой статьи есть ссылка на статью-перевод. Попробуйте заглянуть туда.
            • 0
              Простое определение на самом деле ничего не обьяснит.

              Если хотите — дескриптор это обьект, для которого определен хотя б один из методов "__get__", "__set__", "__del__".
              • +3
                Дескриптор — это интерфейс атрибута объекта или класса. Т.е. инстанс дескриптора — это атрибут, для которого можно определить операции:

                1) getattr и тогда вызывается метод __get__,
                2) установки атрибут (setattr)
                3) и удаления атрбиута(delattr)

                class Descriptor(object):
                def __get__(*args, **kw):

                def __set__(*args, **kw):


                class Foo(object):
                x = Descriptor()
                y = 1

                foo = Foo()
                Что будет происходить при вызове foo.x определяется в __get__ методе дескриптора, ну и так далее.
                • 0
                  Да понял. Спасибо. именно то что мне нужно для текущего проекта: я уж думал кучу ифов городить, а так прекрасно на стадии сборки класса нужную стратегию подключить.

                  Автору: 1 абзац, а на понимание статьи очень сильно влияет.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  > тест на понимание работы @property

                  Этот тест скорее на понимание того, что Bar является объектом Foo и поэтому в Bar доступны все классовые переменные из Foo, а не только property.
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +2
                      >>> Bar().baz

                      Я полагаю будет эксепшн, поскольку атрибуты метакласса доступны только классу, но не инстансу класса :)
                • 0
                  Кстати, есть забавная задачка, связанная с дескрипторами:

                  >>> x = object()
                  >>> x.foo = 1
                  Traceback (most recent call last):
                  File "", line 1, in AttributeError: 'object' object has no attribute 'foo'
                  >>> class Object(object): pass

                  >>> y = Object()
                  >>> y.foo = 1
                  >>> y.foo
                  1
                  >>>

                  почему так, ведь мы только наследовали класс, но при этом, ничего не добавляли и меняли. Ну и почему в x=object() нельзя добавлять новые атрибуты.
                  • 0
                    >>> type(Object())
                    <class '__main__.Object'>
                    >>> hasattr(Object(), '__dict__')
                    True
                    >>> type(object())
                    <type 'object'>
                    >>> hasattr(object(), '__dict__')
                    False

                    Где-то так…
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        object() == object.__call__()

                        Вы видимо имеете ввиде vars(object)?
                      • 0
                        Встроенные типы даних реализованы не на Пайтоне, поэтому нет возможности динамически добавлять атрибуты.

                        Интересно, сработает ли «object().foo = 1» в PyPy?
                        • +1
                          > Встроенные типы даних реализованы не на Пайтоне, поэтому нет возможности динамически добавлять атрибуты.

                          Возможность теоретическая есть, но этого не стали делать из соображений производительности. Встроенные классы — это полноценные классы в питоне. Т.е. я тоже могу создать класс без __dict__ и который будет себя вести также, как object.
                          • НЛО прилетело и опубликовало эту надпись здесь
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                >>> class A(object):
                                … __slots__ = []

                                >>> a = A()
                                >>> a.x = 1
                                Traceback (most recent call last):
                                File "", line 1, in AttributeError: 'A' object has no attribute 'x'
                                >>> hasattr(A(), '__dict__')
                                False
                                >>>
                          • 0
                            ага, а откуда он появился? Мы ведь в Object ничего не писали и не добавляли, просто наследовали.
                          • НЛО прилетело и опубликовало эту надпись здесь
                          • 0
                            Про low-level python можете еще что-нибудь для расширения кругозора порекомендовать читать? Кроме docs.python.org, там конечно все есть, но это справочник и надо знать, что ищешь.
                            • 0
                              На Хабре есть хорошый топик — как раз ответ на Ваш вопрос: habrahabr.ru/blogs/python/84235/
                              • 0
                                > Кроме docs.python.org, там конечно все есть, но это справочник и надо знать, что ищешь.

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

                                docs.python.org/reference/datamodel.html#
                                • НЛО прилетело и опубликовало эту надпись здесь

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.