Руководство к дескрипторам
Время на прочтение
10 мин
Количество просмотров 147K
Краткий обзор
В этой статье я расскажу о том, что такое дескрипторы, о протоколе дескрипторов, покажу как вызываются дескрипторы. Опишу создание собственных и исследую несколько встроенных дескрипторов, включая функции, свойства, статические методы и методы класса. С помощью простого приложения покажу, как работает каждый из них, приведу эквиваленты внутренней реализации работы дескрипторов кодом на чистом питоне.
Изучение того, как работают дескрипторы, откроет доступ к большему числу рабочих инструментов, поможет лучше понять как работает питон, и ощутить элегантность его дизайна.
Введение и определения
Если говорить в общем, то дескриптор — это атрибут объекта со связанным поведением (англ. binding behavior), т.е. такой, чьё поведение при доступе переопределяется методами протокола дескриптора. Эти методы: __get__
, __set__
и __delete__
. Если хотя бы один из этих методов определён для объекта, то он становится дескриптором.
Стандартное поведение при доступе к атрибутам — это получение, установка и удаление атрибута из словаря объекта. Например, a.x
имеет такую цепочку поиска атрибута: a.__dict__['x']
, затем в type(a).__dict__['x']
, и далее по базовым классам type(a)
не включая метаклассы. Если же искомое значение — это объект, в котором есть хотя бы один из методов, определяющих дескриптор, то питон может изменить стандартную цепочку поиска и вызвать один из методов дескриптора. Как и когда это произойдёт зависит от того, какие методы дескриптора определены для объекта. Дескрипторы вызываются только для объектов или классов нового стиля (класс является таким, если наследует от object
или type
).
Дескрипторы — это мощный протокол с широкой областью применения. Они являются тем механизмом, который стоит за свойствами, методами, статическими методами, методами класса и вызовом super()
. Внутри самого питона с их помощью реализуются классы нового стиля, которые были представлены в версии 2.2. Дескрипторы упрощают понимание нижележащего кода на C, а также представляют гибкий набор новых инструментов для любых программ на питоне.
Протокол дескрипторов
descr.__get__(self, obj, type=None) --> значение
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
Собственно это всё. Определите любой из этих методов и объект будет считаться дескриптором, и сможет переопределять стандартное поведение, если его будут искать как атрибут.
Если объект определяет сразу и __get__
, и __set__
, то он считается дескриптором данных (англ. data descriptor). Дескрипторы, которые определили только __get__
называются дескрипторами не данных (англ. non-data descriptors). Их называются так, потому что они используют для методов, но другие способы их применения также возможны.
Дескрипторы данных и не данных отличаются в том, как будет изменено поведение поиска, если в словаре объекта уже есть запись с таким же именем как у дескриптора. Если попадается дескриптор данных, то он вызывается раньше, чем запись из словаря объекта. Если в такой же ситуации окажется дескриптор не данных, то запись из словаря объекта имеет преимущество перед этим дескриптором.
Чтобы создать дескриптор данных только для чтения, определите и __get__
, и __set__
, и сделайте так, чтобы __set__
выбрасывал исключение AttributeError
. Определения метода __set__
и выбрасывания исключения достаточно, чтобы этот дескриптор считался дескриптором данных.
Вызов дескрипторов
Дескриптор можно вызвать напрямую через его метод. Например, d.__get__(obj)
.
Однако, наиболее частый вариант вызова дескриптора — это автоматический вызов во время доступа к атрибуту. Например, obj.d
ищет d
в словаре obj
. Если d
определяет метод __get__
, то будет вызван d.__get__(obj)
. Вызов будет сделан согласно правилам, описанным ниже.
Детали вызова различаются от того, чем является obj
— объектом или классом. В любом случае, дескрипторы работают только для объектов и классов нового стиля. Класс является классом нового стиля, если он является потомком object
.
Для объектов алгоритм реализуется с помощью object.__getattribute__
, который преобразует запись b.x
в type(b).__dict__['x'].__get__(b, type(b))
. Реализация работает через цепочку предшественников, в которой дескрипторы данных имеют приоритет перед переменными объекта, переменные объекта имеют приоритет перед дескрипторами не данных, и самый низкий приоритет у метода __getattr__
, если он определён. Полную реализацию на языке C можно найти в PyObject_GenericGetAttr()
в файле Objects/object.c
.
Для классов алгоритм реализуется с помощью type.__getattribute__
, который преобразует запись B.x
в B.__dict__['x'].__get__(None, B)
. На чистом питоне это выглядит так:
def __getattribute__(self, key):
"Эмуляция type_getattro() в Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
Важные части, которые следует запомнить:
- дескрипторы вызываются с помощью метода
__getattribute__
- переопределение
__getattribute__
прекратит автоматический вызов дескрипторов __getattribute__
доступен только внутри классов и объектов нового стиляobject.__getattribute__
иtype.__getattribute__
делают разные вызовы к__get__
- дескрипторы данных всегда имеют преимущество перед переменными объекта
- дескрипторы не данных могут потерять преимущество из-за переменных объекта
Объект, который возвращается после вызова super()
также имеет собственную реализацию метода __getattribute__
, с помощью которой вызывает дескрипторы. Вызов super(B, obj).m()
ищет в obj.__class__.__mro__
базовый класс A
, за которым сразу следует B
, и возвращает A.__dict__['m'].__get__(obj, A)
. Если это не дескриптор, то m
возвращается неизменённой. Если m
нет в словаре, то возвращаемся к поиску через object.__getattribute__
.
Примечание: в питоне 2.2, super(B, obj).m()
вызывал __get__
только если m
был дескриптором данных. В питоне 2.3, дескрипторы не данных тоже вызываются, за исключением тех случаев, когда используются классы старого стиля. Детали реализации можно найти в super_getattro()
в файле Objects/typeobject.c
, а эквивалент на чистом питоне можно найти в пособии от Guido.
Детали выше описывают, что алгоритм вызова дескрипторов реализуется с помощью метода __getattribute__()
для object
, type
и super
. Классы наследуют этот алгоритм, когда они наследуют от object
или если у них есть метакласс, реализующий подобную функциональность. Таким образом, классы могут отключить вызов дескрипторов, если переопределят __getattribute__()
.
Пример дескриптора
Следующий код создаёт класс, чьи объекты являются дескрипторам данных и всё, что они делают — это печатают сообщение на каждый вызов get
или set
. Переопределение __getattribute__
— это альтернативный подход, с помощью которого мы могли бы сделать это для каждого атрибута. Но если мы хотим наблюдать только за отдельными атрибутами, то это проще сделать с помощью дескриптора.
class RevealAccess(object):
"""Дескриптор данных, который устанавливает и возвращает значения,
и печатает сообщение о том, что к атрибуту был доступ.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Получаю', self.name
return self.val
def __set__(self, obj, val):
print 'Обновляю' , self.name
self.val = val
>>> class MyClass(object):
x = RevealAccess(10, 'var "x"')
y = 5
>>> m = MyClass()
>>> m.x
Получаю var "x"
10
>>> m.x = 20
Обновляю var "x"
>>> m.x
Получаю var "x"
20
>>> m.y
5
Этот простой протокол предоставляет просто увлекательные возможности. Некоторые из них настолько часто используются, что были объединены в отдельные функции. Свойства, связанные и несвязанные методы, статические методы и методы класса — все они основаны на этом протоколе.
Свойства
Вызова property()
достаточно, чтобы создать дескриптор данных, который вызывает нужные функции во время доступа к атрибуту. Вот его сигнатура:
property(fget=None, fset=None, fdel=None, doc=None) --> атрибут, реализующий свойства
В документации показано типичное использование property()
для создания управляемого атрибута x
:
class C(object):
def getx(self): return self.__x
def setx(self, value): self.__x = value
def delx(self): del self.__x
x = property(getx, setx, delx, "Я свойство 'x'.")
Вот эквивалент property
на чистом питоне, чтобы было понятно как реализовано property()
с помощью протокола дескрипторов:
class Property(object):
"Эмуляция PyProperty_Type() в Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError, "нечитаемый атрибут"
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError, "не могу установить атрибут"
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError, "не могу удалить атрибут"
self.fdel(obj)
Встроенная реализация property()
может помочь, когда существовал интерфейс доступа к атрибуту и произошли какие-то изменения, в результате которых понадобилось вмешательство метода.
Например, класс электронной таблицы может давать доступ к значению ячейки через Cell('b10').value
. В результате последующих изменений в программе, понадобилось сделать так, чтобы это значение пересчитывалось при каждом доступе к ячейке, однако программист не хочет менять клиентский код, который обращается к атрибуту напрямую. Эту проблему можно решить, если обернуть атрибут value
с помощью дескриптора данных, который будет создан с помощью property()
:
class Cell(object):
. . .
def getvalue(self, obj):
"Пересчитываем ячейку прежде чем вернуть значение"
self.recalc()
return obj._value
value = property(getvalue)
Функции и методы
В питоне все объектно-ориентированные возможности реализованы с помощью функционального подхода. Это сделано совсем незаметно с помощью дескрипторов не данных.
Словари классов хранят методы в виде функций. При определении класса, методы записываются с помощью def
и lambda
— стандартных инструментов для создания функций. Единственное отличие этих функций от обычных в том, что первый аргумент зарезервирован под экземпляр объекта. Этот аргумент обычно называется self
, но может называться this
или любым другим словом, которым можно называть переменные.
Для того, чтобы поддерживать вызов методов, функции включают в себя метод __get__
, который автоматически делает их дескрипторами не данных при поиске атрибутов. Функции возвращают связанные или не связанные методы, в зависимости от того, через что был вызван этот дескриптор.
class Function(object):
. . .
def __get__(self, obj, objtype=None):
"Симуляция func_descr_get() в Objects/funcobject.c"
return types.MethodType(self, obj, objtype)
С помощью интерпретатора мы можем увидеть как на самом деле работает дескриптор функции:
>>> class D(object):
def f(self, x):
return x
>>> d = D()
>>> D.__dict__['f'] # Внутренне хранится как функция
<function f at 0x00C45070>
>>> D.f # Доступ через класс возвращает несвязанный метод
<unbound method D.f>
>>> d.f # Доступ через экземпляр объекта возвращает связанный метод
<bound method D.f of <__main__.D object at 0x00B18C90>>
Вывод интерпретатора подсказывает нам, что связанные и несвязанные методы — это два разных типа. Даже если они могли бы быть реализованы таким образом, на самом деле, реализация PyMethod_Type
в файле Objects/classobject.c
содержит единственный объект с двумя различными отображениями, которые зависят только от того, есть ли в поле im_self
значение или там содержится NULL
(C эквивалент значения None
).
Таким образом, эффект вызова метода зависит от поля im_self
. Если оно установлено (т.е. метод связан), то оригинальная функция (хранится в поле im_func
) вызывается, как мы и ожидаем, с первым аргументом, установленным в значение экземпляра объекта. Если же она не связана, то все аргументы передаются без изменения оригинальной функции. Настоящая C реализация instancemethod_call()
чуть более сложная, потому что включает в себя некоторые проверки типов и тому подобное.
Статические методы и методы класса
Дескрипторы не данных предоставляют простой механизм для различных вариантов привязки функций к методам.
Повторим ещё раз. Функции имеют метод __get__
, с помощью которых они становятся методами, во время поиска атрибутов и автоматического вызова дескрипторов. Дескрипторы не данных преобразуют вызов obj.f(*args)
в вызов f(obj, *args)
, а вызов klass.f(*args)
становится f(*args)
.
В этой таблице показано связывание и два наиболее популярных варианта:
Преобразование | Вызвана через объект | Вызвана через класс | |
---|---|---|---|
Дескриптор | функция | f(obj, *args) | f(*args) |
staticmethod | f(*args) | f(*args) | |
classmethod | f(type(obj), *args) | f(klass, *args) |
Статические методы возвращают функцию без изменений. Вызовы c.f
или C.f
эквиваленты вызовам object.__getattribute__(c, "f")
или object.__getattribute__(C, "f")
. Как результат, функция одинаково доступна как из объекта, так и из класса.
Хорошими кандидатами для статических методов являются методы, которым не нужна ссылка на переменную self
.
Например, пакет для статистики может включать класс для экспериментальных данных. Класс предоставляет обычные методы для расчёта среднего, ожидания, медианы и другой статистики, которая зависит от данных. Однако, там могут быть и другие функции, которые концептуально связаны, но не зависят от данных. Например, erf(x)
это простая функция для преобразования, которая нужна в статистике, но не зависит от конкретного набора данных в этом классе. Она может быть вызвана и из объекта, и из класса: s.erf(1.5) --> 0.9332
или Sample.erf(1.5) --> 0.9332
.
Так как staticmethod()
возвращает функцию без изменений, то этот пример не удивляет:
>>> class E(object):
def f(x):
print x
f = staticmethod(f)
>>> print E.f(3)
3
>>> print E().f(3)
3
Если использовать протокол дескриптора не данных, то на чистом питоне staticmethod()
выглядел бы так:
class StaticMethod(object):
"Эмуляция PyStaticMethod_Type() в Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, objtype=None):
return self.f
В отличие от статических методов, методы класса подставляют в начало вызова функции ссылку на класс. Формат вызова всегда один и тот же, и не зависит от того, вызываем мы метод через объект или через класс.
>>> class E(object):
def f(klass, x):
return klass.__name__, x
f = classmethod(f)
>>> print E.f(3)
('E', 3)
>>> print E().f(3)
('E', 3)
Это поведение удобно, когда нашей функции всегда нужна ссылка на класс и ей не нужны данные. Один из способов использования classmethod()
— это создание альтернативных конструкторов класса. В питоне 2.3, метод класса dict.fromkeys()
создаёт новый словарь из списка ключей. Эквивалент на чистом питоне будет таким:
class Dict:
. . .
def fromkeys(klass, iterable, value=None):
"Эмуляция dict_fromkeys() в Objects/dictobject.c"
d = klass()
for key in iterable:
d[key] = value
return d
fromkeys = classmethod(fromkeys)
Теперь новый словарь уникальных ключей можно создать таким образом:
>>> Dict.fromkeys('abracadabra')
{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}
Если использовать протокол дескриптора не данных, то на чистом питоне classmethod()
выглядел бы так:
class ClassMethod(object):
"Эмуляция PyClassMethod_Type() в Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def newfunc(*args):
return self.f(klass, *args)
return newfunc
Продающий сайт — Краткое описание сайта (дескриптор)
В предыдущей статье, мы уже говорили, что каждый сайт должен содержать продающие элементы и должен быть заточен на одно конкретное целевое действие. И чтобы это целевое действие состоялось, необходимо чтобы на Вашем сайте были хотя бы базовые продающие элементы.
Одним из таких обязательных продающих элементов является дескриптор (краткое описание сайта). Вообще, базовые элементы, которые повышают конверсию Вашего сайта мы детально обсуждаем в нашей Мастер группе — сообществе предпринимателей.
Посмотрите видео: Типичные Ошибки в Instagram
Что же такое дескриптор?
Дескриптор – это краткое описание сайта иил основной смысл и идея ресурса. При попадании на любую страницу сайта, человек в первую очередь обращает свое внимание на левый верхний угол, где обычно расположен логотип.
Именно по этой причине, следует под логотипом, максимально четко описать посетителю, что это за сайт, и на тот ли ресурс посетитель попал.
Если ваш сайт является интернет-магазином, и Вы продаете на нем часы, Вы так и должны кратко описать примерно следующее: «интернет-магазин часов», если Вы продаете детские товары, значит «интернет-магазин детских товаров» и т.п.
Краткое описание сайта обязательно должно быть под логотипом, независимо от Вашего бизнеса и Вашей ниши, в которой Вы работаете. Если Вы обучаете людей английскому, так и пишите – «изучение английского языка онлайн». В любом случае четкое позиционирование и четкая формулировка обязательно должны быть на вашем сайте.
Следующим важным элементом в дескрипторе должен быть четкий и понятный Заголовок. Обычно его располагают в самом центре вверху сайта. Кроме того, что в Заголовке Вы предельно четко продолжаете описывать свой ресурс, Вы также можете разместить в Заголовке свою первую зацепку.
Заголовок – это не описание Вашего сайта, а элемент создающий заинтересованность клиента.
Например, для организации по строительству бань, можно разместить следующее предложение в заголовке: «Строительство домов под ключ в Санкт-Петербурге за 24 дня». Правда удобное и краткое описание сайта? Делая такую заявку уже в Заголовке, Вы создадите интерес посетителя и заставите его просмотреть основное содержание Вашего ресурса!
Также можете разместить подзаголовок, в котором Вы еще больше раскроете тему Вашего ресурса. В нем стоит разместить еще одну зацепку, интересную Вашей целевой аудитории. По статистике, 8 из 10 посетителей сайта читают заголовок и уже после этого принимают решение, стоит ли задержаться на этом ресурсе, или стоит перейти и искать новый ресурс, который будет полезен Вашему посетителю.
Основная задача дескриптора, заголовка и логотипа ответить посетителю на вопрос: «Где Я? Куда Я попал?»
С помощью дескриптора и заголовка Вашего сайта, Вы зацепите внимание посетителя в так называемом контенте первого экрана.
Посетитель принимает решение, насколько ему интересен и полезен сайт в течение первых 4-х секунд пребывания на ресурсе! Краткое описание сайта, Заголовок, логотип и дескриптор, при правильном составлении позволят Вам зацепить своего потенциального клиента и создать у него интерес к Вашему ресурсу.
В следующих статьях, я Вам расскажу об остальных элементах, которые обязательно должны быть на Ваших сайтах, и которые не только повлияют на продажи в Вашем бизнесе, но и значительно сэкономят средства на продвижение Вашего сайта в интернете.
С Вами,
— Игорь Зуевич.
Рекомендую Вам обратить внимание на следующие программы по созданию дополнительного источника дохода в интернете:
Закрытое Master Mind сообщество
Мастера партнерских программ
Партнерский Маркетинг для Новичков
Если следующие 5 минут вы можете инвестировать в самообразование, то переходите по ссылке и читайте следующую нашу статью: Инфобизнес. С чего начать создание инфобизнеса?
Понравилось? Жми «Мне Нравится«
Оставьте комментарий к этой статье ниже
10 секретов эффективных заголовков
1. В большинстве случаев, чем проще ваш заголовок, тем лучше конверсия
Не надо ничего специально усложнять, сыпать умными терминами и писать в заголовке предложения, уровня писательского конкурса в литературном институте. Скорее всего, аудитория все равно не оценит ваших стараний!Вместо этого, попробуйте просто описать суть своего предложения, уложившись в несколько слов:
- 5 секретов.
- 10 практических шагов.
- 7 критических ошибок.
- Эти заголовки уже стали «классикой». Но не смотря на кажущуюся простоту, они до сих пор отлично работают.
2. Цвет заголовка
Здесь также работает правило №1 – не усложняйте. Представьте, что у вас просто нет желтого, синего, оранжевого и других цветов. Вы ограниченны только черным и красным. И вам их хватит!
3. Короткие заголовки зачастую работают лучше, чем длинные
Чем дольше посетитель будет читать ваш заголовок, тем сильнее он ему надоест. Конечно, есть исключения. Например, легендарный продающий заголовок: «Они смеялись надо мной, когда я сказал что…(заработаю, научусь, смогу и т.д.), пока я не… (приехал на новом Мерседесе, сделал сальто назад и т.д.)».
4. Не «мудрите» с расположением заголовка
Наверху страницы, по центру – самое оптимальное место.
5. Усиливайте заголовок подзаголовком
Хорошим подзаголовком будет например описание формата вашего предложения (практический онлайн-тренинг, бесплатная книга и т.д.).
6. Надавите на «боль»
Мы устроены таким образом, что больше хотим избавиться от боли, чем получить удовольствие. Именно поэтому хорошо работают заголовки, «наступающие на самые больную мозоль».Шаблоны: «Достало сидеть в офисе?», «Замучили пробки?», «Надоела плохая погода?». И дальше вы сразу предлагаете решение этой проблемы – удаленная работа, отдых в Таиланде и т.д.
7. Задайте интригующий вопрос
Мы все любим загадки. А еще больше – отгадки на них. Поэтому вопросы в заголовках, работают отлично. Шаблоны: «В чем секрет…?», «Вы до сих пор…?», «Что бы вы выбрали?».
8. Многих из нас уже достало то, что кто-то говорит «что» нам делать, но не говорит «как»
Именно поэтому, в заголовке можно переходить сразу к сути. Шаблоны: «Как заработать 10 000$ за 3 месяца», «Как знакомиться с десятью красавицами каждый вечер».
9. Покупатели хотят добиться результатов «легко и быстро»
Отлично! – Используйте эти слова в заголовке.
10. И конечно все мы любим новинки
Вы можете так и написать: «Новинка!». А можете использовать однокоренные слова, или синонимы: «Новейшая разработка», «Революционная технология» и т.д.
И конечно главное правило – любой продающий заголовок нужно тестировать. Потому что вы никогда не сможете определить со 100% вероятностью, какой из них сработает лучше всего, но это можно сделать не всегда