Атрибут python что это
Атрибуты и протокол дескриптора в Python
Рассмотрим такой код:
Сегодня мы разберём ответ на вопрос: «Что именно происходит, когда мы пишем foo.bar?»
Вы, возможно, уже знаете, что у большинства объектов есть внутренний словарь __dict__, содержащий все их аттрибуты. И что особенно радует, как легко можно изучать такие низкоуровневые детали в Питоне:
Давайте начнём с попытки сформулировать такую (неполную) гипотезу:
Пока звучит похоже на правду:
Теперь предположим, что вы уже в курсе, что в классах можно объявлять динамические аттрибуты:
Хм… ну ладно. Видно что __getattr__ может эмулировать доступ к «ненастоящим» атрибутам, но не будет работать, если уже есть объявленная переменная (такая, как foo.bar, возвращающая ‘hello!’, а не ‘goodbye!’). Похоже, всё немного сложнее, чем казалось вначале.
И действительно: существует магический метод, который вызывается всякий раз, когда мы пытаемся получить атрибут, но, как продемонстрировал пример выше, это не __getattr__. Вызываемый метод называется __getattribute__, и мы попробуем понять, как в точности он работает, наблюдая различные ситуации.
Пока что модифицируем нашу гипотезу так:
foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:
Проверим практикой, реализовав этот метод (под другим именем) и вызывая его напрямую:
Выглядит корректно, верно?
Отлично, осталось лишь проверить, что поддерживается присвоение переменных, после чего можно расходиться по дом… —
my_getattribute возвращает некий объект. Мы можем изменить его, если он мутабелен, но мы не можем заменить его на другой с помощью оператора присвоения. Что же делать? Ведь если foo.baz это эквивалент вызова функции, как мы можем присвоить новое значение атрибуту в принципе?
Когда мы смотрим на выражение типа foo.bar = 1, происходит что-то больше, чем просто вызов функции для получения значения foo.bar. Похоже, что присвоение значения атрибуту фундаментально отличается от получения значения атрибута. И правда: мы может реализовать __setattr__, чтобы убедиться в этом:
Пара вещей на заметку относительно этого кода:
А ведь у нас есть ещё и property (и его друзья). Декоратор, который позволяет методам выступать в роли атрибутов.
Давайте постараемся понять, как это происходит.
Просто ради интереса, а что у нас в f.__dict__?
В __dict__ нет ключа bar, но __getattr__ почему-то не вызывается. WAT?
bar — метод, да ещё и принимающий в качестве параметра self, вот только это метод находится в классе, а не в экземпляре класса. И в этом легко убедиться:
Ключ bar действительно находится в словаре атрибутов класса. Чтобы понять работу __getattribute__, нам нужно ответить на вопрос: чей __getattribute__ вызывается раньше — класса или экземпляра?
Видно, что первым делом проверка идёт в __dict__ класса, т.е. у него приоритет перед экземпляром.
Погодите-ка, а когда мы вызывали метод bar? Я имею в виду, что наш псевдокод для __getattribute__ никогда не вызывает объект. Что же происходит?
Вся суть тут. Реализуйте любой из этих трёх методов, чтобы объект стал дескриптором и мог менять дефолтное поведение, когда с ним работают как с атрибутом.
Если объект объявляет и __get__(), и __set__(), то его называют дескриптором данных («data descriptors»). Дескрипторы реализующие лишь __get__() называются дескрипторами без данных («non-data descriptors»).
Оба вида дескрипторов отличаются тем, как происходит перезапись элементов словаря атрибутов объекта. Если словарь содержит ключ с тем же именем, что и у дескриптора данных, то дескриптор данных имеет приоритет (т.е. вызывается __set__()). Если словарь содержит ключ с тем же именем, что у дескриптора без данных, то приоритет имеет словарь (т.е. перезаписывается элемент словаря).
Чтобы создать дескриптор данных доступный только для чтения, объявите и __get__(), и __set__(), где __set__() кидает AttributeError при вызове. Реализации такого __set__() достаточно для создания дескриптора данных.
Короче говоря, если вы объявили любой из этих методов — __get__, __set__ или __delete__, вы реализовали поддержку протокола дескриптора. А это именно то, чем занимается декоратор property: он объявляет доступный только для чтения дескриптор, который будет вызываться в __getattribute__.
Последнее изменение нашей реализации:
foo.bar эквивалентно foo.__getattribute__(‘bar’), что примерно работает так:
Попробуем продемонстрировать на практике:
Мы лишь немного поскребли поверхность реализации атрибутов в Python. Хотя наша последняя попытка эмулировать foo.bar в целом корректна, учтите, что всегда могут найтись небольшие детали, реализованные по-другому.
Надеюсь, что помимо знаний о том, как работают атрибуты, мне так же удалось передать красоту языка, который поощряет вас к экспериментам. Погасите часть долга знаний сегодня.
Пользовательские атрибуты в Python
__dict__
В примере описан класс StuffHolder с одним атрибутом stuff, который, наследуют оба его экземпляра. Добавление объекту b атрибута b_stuff, никак не отражается на a.
Посмотрим на __dict__ всех действующих лиц:
(У класса StuffHolder в __dict__ хранится объект класса dict_proxy с кучей разного барахла, на которое пока не нужно обращать внимание).
Ни у a ни у b в __dict__ нет атрибута stuff, не найдя его там, механизм поиска ищет его в __dict__ класса (StuffHolder), успешно находит и возвращает значение, присвоенное ему в классе. Ссылка на класс хранится в атрибуте __class__ объекта.
Поиск атрибута происходит во время выполнения, так что даже после создания экземпляров, все изменения в __dict__ класса отразятся в них:
В случае присваивания значения атрибуту экземпляра, изменяется только __dict__ экземпляра, то есть значение в __dict__ класса остаётся неизменным (в случае, если значением атрибута класса не является data descriptor):
Если имена атрибутов в классе и экземпляре совпадают, интерпретатор при поиске значения выдаст значение экземпляра (в случае, если значением атрибута класса не является data descriptor):
По большому счёту это всё, что можно сказать про __dict__. Это хранилище атрибутов, определённых пользователем. Поиск в нём производится во время выполнения и при поиске учитывается __dict__ класса объекта и базовых классов. Также важно знать, что есть несколько способов переопределить это поведение. Одним из них является великий и могучий Дескриптор!
Дескрипторы
С простыми типами в качестве значений атрибутов пока всё ясно. Посмотрим, как ведёт себя функция в тех же условиях:
WTF!? Спросите вы… возможно. Я бы спросил. Чем функция в этом случае отличается от того, что мы уже видели? Ответ прост: методом __get__.
Этот метод переопределяет механизм получения значения атрибута func экземпляра fh, а объект, который реализует этот метод непереводимо называется non-data descriptor.
Дескриптор — это объект, доступ к которому через атрибут переопределён методами в дескриптор протоколе:
Дескрипторы данных
Рассмотрим повнимательней дескриптор данных:
Стоит обратить внимание, что вызов DataHolder.data передаёт в метод __get__ None вместо экземпляра класса.
Проверим утверждение о том, что у дата дескрипторов преимущество перед записями в __dict__ экземпляра:
Так и есть, запись в __dict__ экземпляра игнорируется, если в __dict__ класса экземпляра (или его базового класса) существует запись с тем же именем и значением — дескриптором данных.
Ещё один важный момент. Если изменить значение атрибута с дескриптором через класс, никаких методов дескриптора вызвано не будет, значение изменится в __dict__ класса как если бы это был обычный атрибут:
Дескрипторы не данных
Пример дескриптора не данных:
Его поведение слегка отличается от того, что вытворял дата-дескриптор. При попытке присвоить значение атрибуту non_data, оно записалось в __dict__ экземпляра, скрыв таким образом дескриптор, который хранится в __dict__ класса.
Примеры использования
Дескрипторы это мощный инструмент, позволяющий контролировать доступ к атрибутам экземпляра класса. Один из примеров их использования — функции, при вызове через экземпляр они становятся методами (см. пример выше). Также распространённый способ применения дескрипторов — создание свойства (property). Под свойством я подразумеваю некое значение, характеризующее состояние объекта, доступ к которому управляется с помощью специальных методов (геттеров, сеттеров). Создать свойство просто с помощью дескриптора:
Или можно воспользоваться встроенным классом property, он представляет собой дескриптор данных. Код, представленный выше можно переписать следующим образом:
В обоих случаях мы получим одинаковое поведение:
Важно знать, что property всегда является дескриптором данных. Если в его конструктор не передать какую либо из функций (геттер, сеттер или делитер), при попытке выполнить над атрибутом соответствующее действие — выкинется AttributeError.
__getattr__(), __setattr__(), __delattr__() и __getattribute__()
Если нужно определить поведение какого-либо объекта как атрибута, следует использовать дескрипторы (например property). Тоже справедливо для семейства объектов (например функций). Ещё один способ повлиять на доступ к атрибутам: методы __getattr__(), __setattr__(), __delattr__() и __getattribute__(). В отличие от дескрипторов их следует определять для объекта, содержащего атрибуты и вызываются они при доступе к любому атрибуту этого объекта.
__getattr__(self, name) будет вызван в случае, если запрашиваемый атрибут не найден обычным механизмом (в __dict__ экземпляра, класса и т.д.):
__getattribute__(self, name) будет вызван при попытке получить значение атрибута. Если этот метод переопределён, стандартный механизм поиска значения атрибута не будет задействован. Следует иметь ввиду, что вызов специальных методов (например __len__(), __str__()) через встроенные функции или неявный вызов через синтаксис языка осуществляется в обход __getattribute__().
__setattr__(self, name, value) будет вызван при попытке установить значение атрибута экземпляра. Аналогично __getattribute__(), если этот метод переопределён, стандартный механизм установки значения не будет задействован:
__delattr__(self, name) — аналогичен __setattr__(), но используется при удалении атрибута.
При переопределении __getattribute__(), __setattr__() и __delattr__() следует иметь ввиду, что стандартный способ получения доступа к атрибутам можно вызвать через object:
__slots__
… Я боялся что изменения в системе классов плохо повлияют на производительность. В частности, чтобы дескрипторы данных работали корректно, все манипуляции атрибутами объекта начинались с проверки __dict__ класса на то, что этот атрибут является дескриптором данных…
На случай, если пользователи разочаруются ухудшением производительности, заботливые разработчики python придумали __slots__.
Наличие __slots__ ограничивает возможные имена атрибутов объекта теми, которые там указаны. Также, так как все имена атрибутов теперь заранее известны, снимает необходимость создавать __dict__ экземпляра.
Оказалось, что опасения Guido не оправдались, но к тому времени, как это стало ясно, было уже слишком поздно. К тому же, использование __slots__ действительно может увеличить производительность, особенно уменьшив количество используемой памяти при создании множества небольших объектов.
Заключение
Доступ к атрибутом в python можно контролировать огромным количеством способов. Каждый из них решает свою задачу, а вместе они подходят практически под любой мыслимый сценарий использования объекта. Эти механизмы — основа гибкости языка, наряду с множественным наследованием, метаклассами и прочими вкусностями. У меня ушло некоторое время на то, чтобы разобраться, понять и, главное, принять это множество вариантов работы атрибутов. На первый взгляд оно показалось слегка избыточным и не особенно логичным, но, учитывая, что в ежедневном программировании это редко пригодиться, приятно иметь в своём арсенале такие мощные инструменты.
Надеюсь, и вам эта статья прояснила парочку моментов, до которых руки не доходили разобраться. И теперь, с огнём в глазах и уверенностью в Точке, вы напишите огромное количество наичистейшего, читаемого и устойчивого к изменениям требований кода! Ну или комментарий.
Атрибут класса и экземпляра Python
Атрибуты — это ключевые игроки языка программирования. Они отвечают за хранение важных значений данных, а также помогают в манипулировании данными.
Понимание атрибута класса Python
Python Class Attribute — это атрибут или переменная, заключенная в класс. То есть его область действия находится в классе Python.
Атрибут Class создает только одну копию самого себя, и эта единственная копия используется всеми функциями и объектами в этом конкретном классе.
Давайте теперь разберемся с реализацией того же на примере ниже.
Реализация на примере
В этом примере мы создаем переменную класса val и инициализируем ее значением 1. Далее, мы получаем доступ к переменной val в функции product() и манипулируем значением, умножая его на 10.
Как ясно видно, одна и та же копия переменной val используется обоими созданными объектами. Таким образом, сначала val = 1.
Когда объект obj1 вызывает функцию, используется та же копия val (значение не сбрасывается) и, таким образом, становится val = 10. При вызове obj2 val становится val * 10, т.е. 10 * 10 = 100.
Понимание атрибута экземпляра Python
Python Instance attribute — это локальный атрибут или переменная, область действия которой находится в пределах конкретной функции, использующей атрибут. Таким образом, он заключен в конкретную функцию.
Атрибут Instance создает новую копию себя каждый раз, когда она вызывается функцией или объектом. То есть отдельная копия этой переменной используется каждый раз, когда объект или функция пытается получить к ней доступ.
Давайте теперь реализуем локальные атрибуты на примере.
Реализация атрибута экземпляра
В этом примере мы объявили и инициализировали атрибут экземпляра как val = 20.
Кроме того, когда obj1 пытается получить доступ к переменной через функцию, он создает свою собственную новую копию, поскольку сбрасывает значение по умолчанию на инициализированное значение, а затем предоставляет к ней доступ.
Тот же сценарий повторяется, когда obj2 пытается получить доступ к переменной экземпляра val.
Разбираемся с доступом к атрибутам в Python
Ну и что с того? Зачем думать о том, как Python за меньший синтаксис делает больше вызовов функций? На самом деле для этого есть две причины. Во-первых, полезно знать, как на самом деле работает Python, чтобы лучше понимать/отлаживать код, когда что-то идет не так как надо. Во-вторых, так можно выявить минимум, необходимый для реализации языка.
Именно поэтому, чтобы заняться самообразованием и заодно подумать, что может понадобиться для реализации Python под WebAssembly или API bare bones на C, я решил написать эту статью о том, как выглядит доступ к атрибутам и что скрывается за синтаксисом.
Теперь вы можете попытаться собрать воедино все, что относится к доступу к атрибутам, прочитав справочник по Python. Так вы можете прийти к выражениям ссылок на атрибуты и модели данных для настройки доступа к атрибутам, однако, все равно важно связать все вместе, чтобы понять, как работает доступ. Поэтому я предпочитаю идти от исходного кода на CPython и выяснять, что происходит в интерпретаторе (я специально использую тег репозитория CPython 3.8.3, поскольку у меня есть стабильные ссылки и я использую последнюю версию на момент написания статьи).
В начале статьи вам встретится немного кода на С, но я не жду, что вы досконально поймете, что там происходит. Я напишу о том, что нужно будет из него понять, поэтому если у вас нет ни малейших знаний в С, то ничего страшного, вы все равно поймете все то, о чем я говорю.
Смотрим в байткод
Итак, давайте разберемся со следующим выражением:
Наверное, самое простая отправная точка в изучении – это байткод. Посмотрим на эту строку и разберемся, что делает компилятор:
Самый важный код операции здесь — LOADATTR. Если интересно, он заменяет объект на вершине стека результатом доступа к именованному атрибуту, как указано в conames[i] .
Большая часть этого кода – это просто работа со стеком, его мы можем опустить. Ключевой бит – это вызов PyObject_GetAttr(), который и обеспечивает доступ к атрибутам.
Имя этой функции выглядит знакомо
Теперь это имя выглядит прямо как getattr(), только в соглашении об именовании функций в С, которое используется в CPython. Покопавшись в Python/bltinmodule.c, где лежат все встроенные модули Python, можем проверить, верна ли наша догадка. Поискав по «getattr» в файле, вы найдете строку, которая связывает имя «getattr» с функцией «builtin_getattr()»
Разбираемся с getattr()
Что мы уже знаем
Запись функции для getattr()
Поиск атрибутов с помощью специальных методов
Обработка типа объекта осуществляется специально, поскольку это позволяет ускорить поиск и доступ. В целом, это исключает дополнительный поиск, пропуская экземпляр каждый раз, когда мы что-то ищем. На уровне CPython это позволяет заводить специальные методы, которые находятся в поле struct для ускорения поиска. Поэтому несмотря на то, что кажется немного странным игнорировать объект, а вместо него использовать тип, это имеет определенный смысл.
Теперь во имя простоты я немного схитрю и заставлю getattr() обрабатывать методы getattribute() и getattr() явно, в то время как CPython производит некоторые манипуляции под капотом, чтобы заставить объект обрабатывать оба метода самостоятельно. В конечном счете, семантика наших целей получается одинаковой.
Псевдокод, реализующий getattr()
Разбираемся с object.getattribute()
В поисках дескриптора данных
Первая важная вещь, которую мы собираемся сделать в object.getattribute() – это поиск дескриптора данных для типа. Если вы никогда не слышали о дескрипторах, то расскажу – это способ программно управлять тем, как работает отдельный атрибут. Возможно, вы вообще никогда о них не слышали, но, если вы некоторое время уже используете Python, я подозреваю, что вы уже использовали дескрипторы: свойства, classmethod и staticmethod – все это дескрипторы.
Если же у самого объекта нет атрибута, то мы увидим, есть ли там дескриптор без данных. Поскольку мы уже искали дескриптор ранее, то можем предположить, что если он был найден, но еще не использовался, когда мы искали дескриптор данных, то это дескриптор без данных.
Наконец, мы нашли атрибут типа, и он не был дескриптором, теперь мы возвращаем его. В итоге, порядок поиска атрибутов выглядит следующим образом:
Дескриптор данных ищется по типам;
Дескриптор без данных ищется по типам;
Что угодно ищется по типам.
Вы заметите, что сначала мы ищем какой-то дескриптор, затем, если нам это не удалось, мы ищем обычный объект, который соответствует виду дескриптора, который мы искали. Сначала мы ищем данные, потом уже что-то другое. Все это имеет смысл, если думать о том как метод self.attr = val в init() хранит данные об объекте. Скорее всего, если вы столкнулись с этим, то хотите, чтобы это стояло перед методом или чем-то подобным. И вам в первую очередь нужны дескрипторы, поскольку, если вы программно определили атрибут, то вероятно, хотели бы, чтобы он использовался всегда.
Заключение
Как видите, во время поиска атрибутов в Python происходит много интересного. Несмотря на то, что я бы сказал, что ни одна из частей не является концептуально сложной, в сумме мы получаем множество операций. Именно поэтому некоторые программисты пытаются минимизировать доступ к атрибутам в Python, чтобы избегать всего этого механизма, если речь идет о важности производительности.
Так исторически сложилось, что почти вся эта семантика пришла в Python как часть классов нового стиля, а не «классических». Это различие исчезло в Python 3, когда классические классы остались в прошлом, так что если вы ничего о них не слышали, то это и хорошо, наверное.
Другие статьи из этой серии можно найти по тегу «syntactic sugar» в этом блоге. Код из этой статьи вы найдете здесь.
Класс и объект в Python
Объектно-ориентированное программирование в Python
Python — это процедурно-ориентированный и одновременно объектно-ориентированный язык программирования.
Процедурно-ориентированный
«Процедурно-ориентированный» подразумевает наличие функций. Программист может создавать функции, которые затем используются в сторонних скриптах.
Объектно-ориентированный
«Объектно-ориентированный» подразумевает наличие классов. Есть возможность создавать классы, представляющие собой прототипы для будущих объектов.
Создание класса в Python
Синтаксис для написания нового класса:
Атрибут:
Атрибут — это элемент класса. Например, у прямоугольника таких 2: ширина ( width ) и высота ( height ).
Метод:
Конструктор:
Создание объекта с помощью класса Rectangle:
Что происходит при создании объекта с помощью класса?
При создании объекта класса Rectangle запускается конструктор выбранного класса, и атрибутам нового объекта передаются значения аргументов. Как на этом изображении:
Конструктор с аргументами по умолчанию
В других языках программирования конструкторов может быть несколько. В Python — только один. Но этот язык разрешает задавать значение по умолчанию.
Все требуемые аргументы нужно указывать до аргументов со значениями по умолчанию.
Сравнение объектов
В Python объект, созданный с помощью конструктора, занимает реальное место в памяти. Это значит, что у него есть точный адрес.
Атрибуты
В Python есть два похожих понятия, которые на самом деле отличаются:
Стоит разобрать на практике:
Атрибут
Объекты, созданные одним и тем же классом, будут занимать разные места в памяти, а их атрибуты с «одинаковыми именами» — ссылаться на разные адреса. Например:
Атрибуты функции
Обычно получать доступ к атрибутам объекта можно с помощью оператора «точка» (например, player1.name ). Но Python умеет делать это и с помощью функции.
Функция | Описание |
---|---|
getattr (obj, name[,default]) | Возвращает значение атрибута или значение по умолчанию, если первое не было указано |
hasattr (obj, name) | Проверяет атрибут объекта — был ли он передан аргументом «name» |
setattr (obj, name, value) | Задает значение атрибута. Если атрибута не существует, создает его |
delattr (obj, name) | Удаляет атрибут |
Встроенные атрибуты класса
Объекты класса — дочерние элементы по отношению к атрибутам самого языка Python. Таким образом они заимствуют некоторые атрибуты:
Переменные класса
Переменные класса в Python — это то же самое, что Field в других языках, таких как Java или С#. Получить к ним доступ можно только с помощью имени класса или объекта.
Для получения доступа к переменной класса лучше все-таки использовать имя класса, а не объект. Это поможет не путать «переменную класса» и атрибуты.
У каждой переменной класса есть свой адрес в памяти. И он доступен всем объектам класса.