Методы — Python: Введение в ООП
Имена атрибутов класса могут указывать на функции. Такие функции называются методами. Вы уже сталкивались с методами, когда работали со встроенными структурами данных. Давайте же объявим класс и метод в нем:
Здесь C.add — обычная функция, о чем нам и говорит интерпретатор. Но сами по себе обычные функции, помещенные в класс не так интересны: те же функции мы можем группировать и в модулях. Более того, обычные функции стоит объявлять именно в модулях.
Методы же нужны для того, чтобы работать с данными объектов класса. Но для этого методы должны быть связаны (bound).
Связанные методы
Вспомните, метод list.append ( list — это класс, помним и это) модифицирует именно объект списка. Посмотрим на этот метод поближе:
Я вызвал метод класса, как обычную функцию и передал первым аргументом объект списка. Метод изменил переданный список. Но ведь раньше мы вызывали этот метод в виде l.append(42) ! Так вот l.append это связанная версия метода list.append : метод связан с конкретным объектом списка и знает, что именно его он должен модифицировать!
Определим класс с атрибутом и методом, затем получим экземпляр класса и посмотрим на вывод REPL для разных версий метода (связанной и не связанной):
Foo.bar — это не связанный метод, то есть обычная функция. А вот x.bar уже связан ("bound") с объектом x : обратите внимание на одну и ту же строку " <__main__.Foo object at 0x7f7beca78668> " — так Python отображает объект x .
Но как метод получает доступ к связанному объекту, спросите вы? В Python методы получают ссылку на связанный объект в качестве первого аргумента. А называть этот аргумент принято именем "self". Вот пример метода, использующего данные объекта:
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
13. Методы¶
Не так просто дать определение, что же такое объектно-ориентированное программирование, но мы уже видели некоторые его свойства:
- Программы строятся из определений объектов и определений функций, и большинство вычислений выражается в терминах операций над объектами.
- Каждое определение объекта соответствует некоторому объекту или понятию реального мира, а функции, работающие с объектом, соответствуют тому, как взаимодействуют реальные объекты.
Например, класс Time , определенный в предыдущей главе, соответствует тому, как люди привыкли записывать время. А определенные нами функции соответствуют действиям, которые люди выполняют над временем. Подобным же образом, классы Point и Rectangle соответствуют математическим понятиям точки и прямоугольника.
До сих пор мы не пользовались средствами, которые Python предоставляет для объектно-ориентированного программирования. Строго говоря, эти средства не являются необходимыми для создания программ. По большей части, они предоставляют альтернативный синтаксис для тех вещей, которые мы уже делали. Однако, часто этот синтаксис оказывается более кратким и выразительным.
Например, в программе Time нет очевидной связи между определением класса и следующими за ним определениями функций. Если присмотреться, то оказывается, что каждая из функций принимает в качестве параметра, по крайней мере, один объект Time .
Это наблюдение мотивирует нас перейти к методам. Мы уже встречали некоторые методы, например, keys и values , которые вызываются на словарных объектах. Каждый метод связан с классом и предназначен для использования с объектами этого класса.
Методы похожи на функции, но есть два отличия:
- Методы определяются внутри определения класса, чтобы сделать отношения между классом и методом явными.
- Синтаксис для вызова метода отличается от синтаксиса для вызова функции.
В следующих разделах мы превратим функции из предыдущей главы в методы. Это преобразование почти механическое. Мы сделаем это, выполнив определенную последовательность шагов.
13.2. Метод increment ¶
Для начала давайте превратим в метод функцию increment .
Для этого достаточно поместить определение функции внутрь определения класса. Хорошим тоном будет также переименование параметра time в self (хоть делать это и не обязательно). Как вы, должно быть, помните, в сообществе программистов Python существует соглашение, согласно которому первому параметру метода дают имя self (англ.: сам).
Обратите внимание на сдвиг кода метода относительно заголовка класса:
Преобразование чисто механическое — мы переместили определение метода в определение класса, и изменили имя первого параметра.
Теперь можно вызвать increment как метод, используя точечную нотацию:
Объект, на котором вызывается метод, присваивается первому параметру метода. Таким образом, в данном случае my_time присваивается параметру self . Второй параметр, seconds , получает значение 600 .
В процедурном программировании предполагается, что функции выполняют необходимые действия. Синтаксис для вызова функции , increment(my_time, 600) , говорит: Эй, increment ! Вот тебе объект Time и 600 секунд, сделай с ними все необходимое.
Синтаксис объектно-ориентированном программировании предполагает, что необходимые действия выполняет объект. Вызов, подобный my_time.increment(600) говорит: Эй, my_time ! Пожалуйста, увеличь себя на 600 секунд!
Является ли полезным такое изменение взгляда на вещи? Дело в том, что иногда, передавая ответственность от функций объектам, мы можем писать более гибкий код. Также становится проще поддерживать и повторно использовать такой код.
13.3. Более сложный пример¶
Функция after немного более сложная, поскольку она имеет дело с двумя объектами Time . Первый из параметров переименуем в self , второй оставим без изменений:
Мы вызываем этот метод на одном объекте Time , и передаем второй объект Time в качестве аргумента:
Программа состоит из предложений на почти естественном английском языке: Если время готовности (done_time) позднее, чем текущее время (current_time), то.
13.4. Инициализирующий метод¶
Как мы уже знаем, инициализирующий метод — это специальный метод, который вызывается при создании объекта. Этот метод имеет имя __init__ (два символа подчеркивания, init , и еще два символа подчеркивания). Инициализирующий метод класса Time выглядит так:
Заметьте, что между атрибутом self.hours и параметром hours не возникает конфликта имен. Точечная нотация устраняет конфликт.
Метод, вызываемый при создании объекта и инициализирующий состояние объекта, также называют конструктором. В Python метод __init__ является конструктором.
Когда мы создаем объект Time , указанные нами аргументы передаются конструктору:
Поскольку параметры метода __init__ имеют значения по умолчанию, мы можем и не передавать аргументы при создании объекта:
Или передать только первый аргумент:
Или только первые два аргумента:
Мы также можем передать часть аргументов, явно поименовав их:
13.5. Метод __str__ ¶
Метод __str__ имеет специальное назначение в Python, он возвращает строковое представление объекта. Определим метод __str__ для класса Time , позаимствовав решение из функции print_time из предыдущей главы:
Если класс предоставляет метод с именем __str__ , то тем самым переопределяет поведение встроенной функции Python str .
При выводе объекта Time с помощью print неявно вызывается __str__ на этом объекте. Поэтому добавление метода __str__ также меняет поведение print :
Как видите, добавление метода __str__ к классу Time сделало ненужным написанную ранее функцию print_time .
Когда мы пишем новый класс, мы почти всегда начинаем с написания метода __init__ , который облегчает создание объектов, и метода __str__ , который часто полезен для отладки.
13.6. Снова Points ¶
Теперь, для закрепления изученного материала, давайте перепишем класс Point в стиле ООП:
Инициализирующий метод принимает x и y как опциональные параметры, значение по умолчанию для каждого из них 0.
Метод __str__ возвращает строковое представление объекта Point :
13.7. Перегрузка операторов¶
Некоторые языки программирования позволяют изменять определения встроенных операторов для использования этих операторов с типами, определенными пользователем. Это свойство называется перегрузкой операторов.
Например, для перегрузки оператора + , класс должен предоставить метод __add__ :
Как обычно, первый параметр метода представляет объект, на котором вызывается метод. Второй параметр удачно назван other (англ.: другой) чтобы противопоставить его первому, self . Для того, чтобы сложить два объекта Point , мы создаем и возвращаем новый объект Point , содержащий сумму координат x и сумму координат y двух объектов.
Теперь, когда мы применяем оператор + к объектам Point , Python вызывает метод __add__ :
Выражение p1 + p2 равнозначно выражению p1.__add__(p2) , только более изящно.
В качестве упражнения вам будет предложено самостоятельно написать метод __sub__(self, other) , который перегрузит оператор вычитания.
Перегрузить оператор умножения можно, определив метод __mul__ , или __rmul__ , или оба эти метода. Если левый операнд оператора * является объектом Point , то Python вызовет метод __mul__ , который ожидает, что второй операнд также является объектом Point . Этот метод рассчитает произведение точек согласно известной из математики формуле (сумма квадратов катетов равна квадрату гипотенузы):
Если левый операнд оператора * является примитивным числовым типом, а правый операнд — объект Point , то Python вызовет метод __rmul__ , который выполнит умножение объекта Point на число:
Результатом будет новый объект Point , чьи координаты кратны первоначальным координатам. Если other окажется типом, который нельзя умножить на число с плавающей точкой, то __rmul__ сгенерирует ошибку.
Следующий пример демонстрирует оба вида умножения:
А что случится, если мы попробуем вычислить p2 * 2 ? Так как первый аргумент является объектом Point , то Python вызовет __mul__ и передаст 2 в качестве второго аргумента. Внутри метода, __mul__ попытается получить атрибут x объекта other , что закончится неудачей, поскольку целое число не имеет атрибутов:
13.8. Полиморфизм¶
Большинство написанных нами методов работают только с определенными типами данных. Когда создается новый класс, то пишутся методы, которые работают с объектами этого класса.
Но есть некоторые операции, которые хотелось бы уметь выполнять с разными типами данных, например, арифметические операции из предыдущего раздела. Если разные типы поддерживают одни и те же операции, значит, можно писать функции, работающие с любым из этих типов.
Например, операция multadd (обычная в линейной алгебре) имеет три параметра; первые два перемножаются, и к полученному произведению прибавляется третий. Можем записать это на языке Python таким образом:
Этот метод будет работать с любыми значениями x и y , которые можно перемножить, и с любым значением z , которое можно прибавить к полученному произведению.
Можно вызвать этот метод с числовыми значениями:
Или с объектами Point :
В первом случае, Point умножается на число и складывается с другим Point . Во втором случае, произведение двух Point дает числовое значение, и третий аргумент также является числом.
Функции, подобные этой, которые могут принимать аргументы различных типов, называются полиморфными.
В качестве еще одного примера, рассмотрим метод front_and_back , который печатает список дважды, сначала — в прямом, а затем и в обратном порядке:
Поскольку метод reverse модифицирующий, мы делаем копию списка, прежде чем расставить его элементы в обратном порядке. Таким образом, метод reverse не изменяет список, который он получает в качестве параметра.
Вот пример использования метода front_and_back со списком:
Поскольку мы предназначили эту функцию для работы со списками, неудивительно, что она работает, как ожидалось. Было бы удивительно, если бы мы смогли применить эту функцию к объекту Point .
Для того, чтобы определить, может ли функция быть применена к новому типу, мы воспользуемся основным правилом полиморфизма: Если все операции внутри функции могут быть применены к данному типу, то вся функция может быть применена к данному типу.
Операции внутри функции включают copy , reverse и print .
copy работает с любым объектом. Мы уже написали метод __str__ для Point . Нам остается написать метод reverse для класса Point :
Теперь можно передать объект Point функции front_and_back :
Замечательное свойство языка Python — это непреднамеренный полиморфизм, когда вы обнаруживаете, что написанная вами функция может работать с типами, для которых вы ее не предназначали.
13.9. Глоссарий¶
13.10. Упражнения¶
Переделайте функцию convert_to_seconds в метод класса Time :
Добавьте в класс Point метод __sub__(self, other) , который перегрузит оператор вычитания, и попробуйте с ним поработать.
Перепишите класс Rectangle в объектно-ориентированном стиле, определив методы __init__ и __str__ .
Добавьте в класс Rectangle методы move_rect и grow_rect , созданные на основе одноименных функций из предыдущей главы.
Определите класс Pet (англ.: любимое животное) с атрибутами имя и возраст, инициализируемыми в методе __init__ значениями параметров метода. Метод __str__ должен возвращать строку с именем и возрастом животного. Поэкспериментируйте, создавая объекты класса Pet и выводя их на печать.
Классы, объекты и методы в Python
Наследование в Python основано на сходных идеях, используемых в других объектно-ориентированных языках, таких как Java, C ++ и т. Д. Новый класс может быть получен из существующего класса следующим образом.
BaseClass является уже существующий (родительский) класс, а DerivedClass это новый (дочерний) класс , который наследует (или подклассов) атрибуты BaseClass .Примечание: По состоянию на Python 2.2, все классы неявно наследуются от object класса , который является базовым классом для всех встроенных типов.
Определим родительский Rectangle класс в примере ниже, который неявно наследует от object :
Rectangle класс может быть использован в качестве базового класса для определения Square класса, как квадрат является частным случаем прямоугольника.
Square класс автоматически наследует все атрибуты Rectangle класса, а также класса объектов. super() используется для вызова __init__() метод Rectangle класса, по существу , вызовом любой перекрытый метод базового класса. Примечание: в Python 3, super() не требует аргументов.
Объекты производного класса могут получать доступ и изменять атрибуты своих базовых классов:
Встроенные функции, которые работают с наследованием
issubclass(DerivedClass, BaseClass) : возвращает True , если DerivedClass является подклассом BaseClass
isinstance(s, Class) : возвращает True , если ы является экземпляром Class или любой из производных классов Class
Переменные класса и экземпляра
Переменные экземпляра уникальны для каждого экземпляра, а переменные класса являются общими для всех экземпляров.
Переменные класса могут быть доступны в экземплярах этого класса, но присвоение атрибуту класса создаст переменную экземпляра, которая затеняет переменную класса
Обратите внимание , что мутирует переменный класс случаев может привести к неожиданным последствиям.
Связанные, несвязанные и статические методы
Идея связанных и несвязанных методов был удален в Python 3.В Python 3 при объявлении метода в классе, вы используете def ключевое слово, тем самым создавая объект функции. Это обычная функция, и окружающий класс работает как пространство имен. В следующем примере мы указываем метод f в пределах класса A , и это становится функцией A.f :
В Python 2 поведение отличается: объекты функций внутри класса были неявно заменены объектами типа instancemethod , которые назывались несвязанных метода , потому что они не были связаны с каким — либо конкретным экземпляром класса. Удалось получить доступ к основной функции с помощью .__func__ свойства.
Последнее поведение подтверждается проверкой — методы распознаются как функции в Python 3, в то время как различие поддерживается в Python 2.
В обеих версиях функции Python / метод A.f может быть вызван непосредственно, при условии , что вы передаете экземпляр класса A в качестве первого аргумента.
Теперь предположим , что a является экземпляром класса A , что a.f тогда? Ну, интуитивно это должно быть тем же самым методом f класса A , только он должен каким — то образом «знает» , что он был применен к объекту a — в Python это называется метод , связанный с. a
В суровых буднях детали следующим образом : запись af вызывает магический __getattribute__ метод , который сначала проверяет , является ли имеет атрибут с именем a a f (не), а затем проверяет , класс A , содержит ли это метод с таким именем (он делает), и создает новый объект m типа method , который имеет ссылку на исходные Af в m.__func__ и ссылку на объект a в m.__self__ .Когда этот объект вызывается как функция, он просто выполняет следующие действия : m(. ) => m.__func__(m.__self__, . ) .Таким образом , этот объект называется связанный метод потому , что при вызове он знает , чтобы поставить объект был привязан к качестве первого аргумента. (Эти вещи работают одинаково в Python 2 и 3).
Наконец, Python имеет методы класса и статические методы — специальные виды методов. Методы класса работают точно так же , как и обычные методы, за исключением того, что при вызове на объекте они связываются с классом объекта , а не к объекту. Таким образом , m.__self__ = type(a) .При вызове такого связанного метода, он проходит класс в качестве первого аргумента. a Статические методы еще проще: они вообще ничего не связывают и просто возвращают базовую функцию без каких-либо преобразований.
Обратите внимание, что методы класса привязаны к классу даже при обращении к экземпляру:
Стоит отметить , что на самом низком уровне, функции, методы, staticmethods и т.д., на самом деле дескрипторы , которые вызывают __get__ , __set и , возможно , `del__` специальные методы. Для более подробной информации о методах классов и статических методах:
Классы нового стиля против старого стиля
Новые классы в стиле были введены в Python 2.2 для объединения классов и типов. Они наследуют от верхнего уровня object типа. Класс нового типа является определенный пользователем тип, и очень похож на встроенных типов.
Классы старого типа не наследуют от object .Экземпляры старого типа всегда реализуется с помощью встроенного в instance типа.
В Python 3 классы старого стиля были удалены.
Новые классы стиля в Python 3 неявно наследуют от object , поэтому нет необходимости указывать MyClass(object) больше.
Значения по умолчанию для переменных экземпляра
Если переменная содержит значение неизменяемого типа (например, строку), тогда можно назначить значение по умолчанию, подобное этому.
Нужно быть осторожным при инициализации изменяемых объектов, таких как списки в конструкторе. Рассмотрим следующий пример:
Такое поведение вызвано тем, что в Python параметры по умолчанию связаны при выполнении функции, а не при ее объявлении. Чтобы получить переменную экземпляра по умолчанию, которая не разделяется между экземплярами, следует использовать такую конструкцию:
Смотрите также Мутабельные Аргументы по умолчанию и «изумление» Наималейшего и изменяемый по умолчанию аргумент .
Множественное наследование
Python использует C3 линеаризацию алгоритм для определения порядка , в котором для решения атрибутов класса, включая методы. Это известно как Порядок разрешения методов (MRO).
Вот простой пример:
Теперь, если мы создаем экземпляр FooBar , если мы ищем атрибут foo , мы видим, что атрибут Foo находится первым
Можно просто сказать, что алгоритм Python MRO
- Глубина первого (например , FooBar затем Foo ) , если
- общий родительский ( object ) блокируется ребенком ( Bar ) и
- круговые отношения не допускаются.
То есть, например, Bar не может наследовать от FooBar, а FooBar наследует от Bar.
Другая характерная особенность в наследстве является super .Супер может получить функции родительских классов.
Множественное наследование с помощью метода init класса, когда у каждого класса есть собственный метод init, тогда мы пытаемся получить множественное наследование, тогда вызывается только метод init класса, который наследуется первым.
для примера ниже Foo метод инициализировать класс вызывался класс Bar не INIT вызывался
Но это не значит, что Bar класс не наследуется. Instance конечного класса FooBar также экземпляр класса Bar и класса Foo.
Дескрипторы
Дескрипторы являются объектами , которые являются ( как правило) атрибутами классов и которые имеют какие — либо из __get__ , __set__ или __delete__ специальных методов.
Дескрипторы данных имеют какой — либо из __set__ или __delete__
Они могут контролировать пунктирный поиск на экземпляре, и используются для реализации функций, staticmethod , classmethod и property .
Методы класса: альтернативные инициализаторы
Методы класса представляют альтернативные способы создания экземпляров классов. Чтобы проиллюстрировать это, давайте посмотрим на пример.
Давайте предположим , что мы имеем относительно простой Person класс:
Возможно, было бы удобно иметь возможность создавать экземпляры этого класса, указав полное имя вместо имени и фамилии отдельно. Один из способов сделать это будет иметь last_name быть необязательным параметром, и при условии , что , если не дано, мы прошли полное имя:
Однако с этим битом кода связаны две основные проблемы:
- Параметры first_name и last_name теперь вводит в заблуждение, так как вы можете ввести полное имя для first_name .
- Кроме того, если есть больше падежей и / или больше параметров, которые обладают такой гибкостью, ветвление if / elif / else может быстро раздражать.
Введите методы класса. Вместо того , чтобы иметь один инициализатор, мы создадим отдельный инициализатору, называемый from_full_name , и украсить его с (встроенный) classmethod декоратора.
Обратите внимание на cls вместо self в качестве первого аргумента from_full_name .Методы класса применяется к общему классу, не является экземпляром данного класса (что self обычно обозначает). Так что , если cls является наш Person класс, то возвращается значение из from_full_name метода класса является Person(first_name, last_name, age) , который использует Person «s __init__ создать экземпляр Person класса.
В частности, если мы должны были сделать подкласс Employee из Person , то from_full_name будет работать в Employee классе , а также.
Для того, чтобы показать , что это работает , как и ожидалось, давайте создавать экземпляры Person в более чем одним способом , без разветвлений в __init__ :
- https://docs.python.org/2/library/functions.html#classmethod
- https://docs.python.org/3.5/library/functions.html#classmethod
Композиция классов
Композиция классов позволяет явные отношения между объектами. В этом примере люди живут в городах, которые принадлежат странам. Композиция позволяет людям получить доступ ко всем людям, живущим в их стране:
Monkey Patching (исправление обезьяны)
В этом случае «исправление обезьяны» означает добавление новой переменной или метода в класс после его определения. Например, скажем , мы определили класс A , как
Но теперь мы хотим добавить еще одну функцию позже в коде. Предположим, что эта функция выглядит следующим образом.
Но как же мы добавим это как метод в A ? Это просто мы просто по существу поместить эту функцию в с помощью оператора присваивания. A
Почему это работает? Потому что функции — это объекты, как и любой другой объект, а методы — это функции, принадлежащие классу.
Функция get_num должна быть доступна для всех существующих (уже создан) , а также к новым экземплярам A
Эти дополнения доступны для всех экземпляров этого класса (или его подклассов) автоматически. Например:
Обратите внимание, что, в отличие от некоторых других языков, этот метод не работает для определенных встроенных типов и не считается хорошим стилем.
Список всех членов класса
dir() функция может быть использована для получения списка членов класса, например:
Обычно ищут только «немагических» участников. Это можно сделать с помощью простого понимания , в котором перечислены члены, имена которых не начиная с __ :
Предостережения:
Классы можно определить __dir__() метод. Если этот метод существует вызова dir() будем называть __dir__() , в противном случае Python будет пытаться создать список членов класса. Это означает, что функция dir может иметь неожиданные результаты. Две цитаты важности из официальной документации питона :
Если объект не содержит каталог ( как правило ), функция пытается все возможное , чтобы собрать информацию из атрибута Dict объекта, если он определен, и от его типа объекта. Полученный список не обязательно является полным, и может быть неточным , если объект имеет собственный GetAttr ().
Примечание: Поскольку реж () поставляется в первую очередь для удобства использования в интерактивной командной строке, он пытается поставить интересный набор имен больше , чем он пытается поставить строго или последовательно определенный набор имен, и его детальное поведение может измениться по релизы. Например, атрибуты метакласса отсутствуют в списке результатов, когда аргумент является классом.
Введение в классы
Класс, функционирующий как шаблон, который определяет основные характеристики конкретного объекта. Вот пример:
Есть несколько вещей, на которые стоит обратить внимание при рассмотрении приведенного выше примера.
Теперь давайте сделаем несколько экземпляров нашего Person класса!
В настоящее время мы имеем три Person объектов, kelly , joseph и john_doe .
Мы можем получить доступ к атрибутам класса из каждого экземпляра с помощью оператора точки . Еще раз обратите внимание на разницу между атрибутами класса и экземпляра:
Мы можем выполнить методы класса с использованием того же оператора точки . :
Cвойства
Классы Python поддерживают свойства, которые выглядят как обычный переменный объект, но с возможностью прикрепления пользовательского поведения и документации.
Объекта , класса MyClass , будет иметь имеют свойство .string , однако его поведение теперь жестко контролируется:
Помимо полезного синтаксиса, описанного выше, синтаксис свойства позволяет проверять или добавлять другие дополнения к этим атрибутам. Это может быть особенно полезно с общедоступными API-интерфейсами, где пользователю должен быть предоставлен определенный уровень помощи.
Другое распространенное использование свойств — это предоставление классу возможности представлять «виртуальные атрибуты» — атрибуты, которые на самом деле не хранятся, но вычисляются только по запросу.
Синглтон класс
Синглтон — это шаблон, который ограничивает создание экземпляра класса одним экземпляром / объектом. Для получения дополнительной информации о питоных одноэлементных шаблонах проектирования, см здесь .
Другой способ — украсить свой класс. Следуя пример из этого ответа создать класс Singleton:
Что есть функция в Python?

Эта статья предназначена только для тех людей, которым хочется узнать, чем на самом деле является функция в python. НО! Предупреждаю, я не буду лезть в сурсы питона. Эта статья была создана только для обычных вроде меня программистов.
Статья будет состоять из 4 частей:
Как осуществляется поиск атрибутов в классах.
Что есть метод и как он вызывается.
Что есть функция и как она вызывается.
1. Как осуществляется поиск атрибутов в классах
Мы знаем, что при обращении к атрибуту вызывается дандер-метод __getattribute__, который в свою очередь пытается возвратить наш атрибут, но! Если он его не находит атрибут, то он вызывает исключение AttributeError, если __getattr__ не определен в нашем классе. Это понятно, но мы не знаем, что происходит под капотом. И поэтому я решил создать свою интерпретацию поиска атрибутов.

Как видите, тут не так все просто как нам казалось) Срабатывается куча проверок, чтобы наконец-то либо возвратить наш атрибут или вызвать исключение.
Давайте с помощью этой блок-схемы, реализуем свой прототип __getattribute__ и посмотрим, будет ли он работать так же как и оригинал.
Вот что вышло:
И давайте наконец его испробуем!
Создадим класс Pet, который наследуется от класса Animal, где первый атрибут будет объектом дескриптора данных и 2 обычных переменных экземпляра класса это animal и age:
И как вы видите, у нас все сработало!) Наш прототип __getattribute__ отлично возвращает методы, дескрипторы данных, дескрипторы не-данных, локальные атрибуты (если так можно назвать), атрибуты класса.) (Небольшое уточнение: под возвращением атрибутов, я имею ввиду возвращения их через экземпляр класса, т.к через класс все атрибуты возвращаются с помощью вызова __getattribute__ у метакласса (type))
Давайте на этом примере я объясню как все происходит. К примеру, второй вызов нашего прототипа для возвращения локального атрибута animal. Тут все очень просто. Происходит 3 итерации нашего цикла, где мы проверяем, является ли animal чьим-то атрибутом класса. Конечно же нет, потому у нас выполняется конструкция в else, где мы проверяем есть ли animal в пространстве имен экземпляра класса. Есть. И мы его возвращаем.
А теперь давайте перейдем к дескриптору данных, то есть к атрибуту name. Происходит 1 итерация, где мы проверяем, есть ли он в пространстве имен класса Pet. Да, есть. Дальше, является ли он дескриптором данных. Является. Потому мы с помощью дандер метода __get__ возвращаем его.
Так же не забываем, что наш прототип проходит по всему MRO класса, как у оригинала, он может возвращать все методы род.класса и так же его атрибуты (как показано на примере). Но некоторые из вас спросят, как вызвался атрибут eyes класса Animal к примеру? Для начала узнаем как выглядит MRO у класса Pet.
Вот так:
[<class ‘main.Pet’>, <class ‘main.A’>, <class ‘object’>]
В первую очередь происходит первая итерация нашего цикла в прототипе __getattribute__, где cls это класс Pet. Он не проходит проверку if item in cls.__dict__, потому что у Pet нет такого атрибута. Потому, происходит вторая итерация, уже Animal и при той проверке, она возвращает True поэтому мы возвращаем атрибут класса Animal.
Теперь, вы уже знаете как примерно происходит поиск атрибутов, как под капотом вызываются дескриптора данных и дескрипторы не-данных, локальные атрибуты, атрибуты класса и т.д. Вы даже можете создать свой прототип __getattribute__. Поэтому, давайте перейдем ко второй части статьи.
2. Что такое метод и как он вызывается
Вы уже наверно подумали, почему же я не объяснил про вызов метода meow с помощью нашего прототипа, точнее. как он вызвался с помощью него?
Давайте по порядку и издалека. Какие свойства имеют методы? Ну, они возвращают объект bound method при обращении через экземпляр класса, а так же первым аргументом всегда у них является опять-таки экземпляр класса. Отлично! С этим мы разобрались.
Теперь. Давайте, чтобы доказать вам, что метод является дескриптором не-данных я вам объясню почему метод не вызывается как обычный атрибут класса (Class.__dict__[item]) или как обычный локальный атрибут объекта класса (obj.__dict__[item])
Во первых, если бы методы вызывались как обычный атрибут класса, мы бы точно не смогли бы возвращать объект bound method через экземпляр класса при обращений к ним, потому что метод возвращал бы объект функций (как и класс). И тут вы зададите вопрос, а почему? Ну потому что в любом случае он бы под капотом вызывался бы как атрибут класса (Class.__dict__[item]), если бы мы даже обращались к нему с помощью объекта класса. Вот небольшой пример:
А теперь, давайте попробуем второй вариант. Запустим такой код:
И к сожалению мы получаем исключение KeyError.
Тут мне кажется и так было понятно что вариант с объектом класса не сработает, только из-за того что методы находятся в пространстве имен класса, а не в экземпляре естественно. И прежде чем я отвечу на вопрос что вообще делает тут такое:
Давайте вернемся к результатам:
Метод не вызывается как обычный атрибут класса, т.к происходит иное поведение (возвращение объекта функций, а не bound method даже при обращении к методу с помощью объекта класса), во вторых обращение экземпляра класса к методам такое же,как и у класса, т.к под капотом он все равно вызывался бы как через класс. Потому, такой вариант не является истинным.
Метод не вызывается как обычный локальный атрибут объекта класса, т.к методы находятся в пространстве имен класса, а не в объекте класса.
И что мы получаем тогда? То что метод является — дескриптором. А точнее: Метод — это дескриптор не-данных.
Если вы мне не поверили, то:
Кстати, если уж мы вспомнили о классе Pet и его методе meow, то давайте заодно попробуем вызвать его как дескриптор:
Как видите, все сработало на ура!
Теперь, давайте вернемся к той части кода в классе Email:
Почему тут присутствует проверка item на значение __dict__ и почему я вызываю у него метод __get__? Ну-у, скорее всего потому что он является — дескриптором :). И кстати дескриптором данных, он имеет и __get__ и __set__ и __delete__.
И вы наверно в ответ скажите, а когда мы вообще к нему обращаемся? Вот здесь self.__dict__ опять вызывается __getattribute__,в котором параметр item имеет значение __dict__.
И узнав что __dict__ является дескриптором, вы одновременно узнали как он возвращается у объектов.
И кстати, у классов же атрибут __dict__ не является дескриптором.
3. Что такое функция и как она вызывается
Теперь, мы можем приступить к самой главной части статье.
Мы знаем что функция и методы (здесь, под методами я подразумеваю именно функцию класса, а не объект класса method) являются объектами класса function.
Потому не сложно понять, что функция так же является дескриптором не-данных и еще не забываем, что метод по сути является функцией класса, грубо говоря. И тут задается вопрос, а может на самом деле функции вызывается как метод? Не совсем. Но мы можем ее так вызывать:
Тут удивляться нечему. Потому что вместо того, чтобы обращаться к методу через пространство имен класса (потому что по другому мы не смогли бы), мы попросту прямо обращаемся к нашей обычной функции и возвращаем ее как связанный метод.
Но все равно, на самом деле функции так не вызываются. Давайте опять вспомним, что функция является объектом класса function, где тело функций содержится в дандер-методе __call__ который принадлежит естественно к классу функций. И чтобы вызвать этот метод. Что нужно сделать?) Правильно! Обратиться к дандер-методу __get__ где экземпляром класса будет сама наша функция, а класс — класс функций. Давайте же это реализуем!
И вот так, мы узнали как на самом деле «вызываются» функции) Но у некоторых может возникнуть еще один вопрос, «а почему у функций есть метод __get__, если мы его по сути не используем для вызова?» Ответ очень прост: на случае если мы обратимся к функции как к атрибуту.
4. Вывод
Мы узнали как примерно происходит поиск атрибутов в классах, что функция и метод являются дескрипторами не-данных. Мы узнали как на самом деле вызываются методы:
Как на самом деле вызываются функции:
И. Все! Моя статья уже подходит к концу! Потому. напоследок я хочу дать вам домашнее задания:
Добавить одну важную, но одновременно маленькую деталь, которую я не успел вписать в мою блок-схему и в прототип. И конечно же, скинуть улучшенный прототип в комментариях!
И так же, всем огромное спасибо за прочтение моей первой статьи! Огромная благодарность Павлу за помощь в ее написании и спасибо Бензу! Я очень надеюсь что я хоть как-то дал вам ответ!