В прошлом уроке был разобран фундаментальный механизм: класс — это пространство имён, а объект получает доступ к атрибутам класса через определённый порядок поиска. Всё, что рассматривалось до сих пор, касалось данных — атрибутов, хранящих значения. Но класс описывает не только то, чем объект обладает, но и то, что объект умеет делать. Именно за это отвечают методы — функции, объявленные внутри тела класса.
На первый взгляд метод выглядит как обычная функция, слегка изменившая место прописки — теперь она находится не на верхнем уровне модуля, а внутри класса. Но при вызове метода через объект происходит нечто, что для многих начинающих становится источником путаницы: перед первым «настоящим» аргументом всегда обязан стоять параметр с именем self. В этом уроке разбирается, что это за параметр, откуда он берётся и почему без понимания его механики объектно-ориентированный код в Python остаётся набором заклинаний, а не осмысленным инструментом.
Определим класс с одним методом:
class Point:
color = "red"
circle = 2
def set_coords():
print("Вызов метода set_coords")По внешнему виду метод ничем не отличается от обычной функции: то же ключевое слово def, то же тело, тот же синтаксис. Разница лишь в месте объявления — метод находится внутри тела класса, а значит, становится частью его пространства имён, точно так же, как атрибуты color и circle. Убедиться в этом можно, заглянув в Point.__dict__ — среди прочего там обнаружится и set_coords, представленный как обычный объект функции.
Раз метод — это атрибут класса, его можно вызвать напрямую через имя класса, точно так же, как обращаются к любому другому атрибуту:
Point.set_coords()Вызов метода set_coords
Здесь всё работает без каких-либо неожиданностей: Point.set_coords находит функцию в пространстве имён класса, а скобки после неё вызывают эту функцию, как и любую другую. До этого момента метод не сделал ничего такого, чего не сделала бы обычная функция вне класса.
Теперь создадим объект класса и попробуем вызвать тот же метод, но уже через него:
pt = Point()
pt.set_coords()TypeError: set_coords() takes 0 positional arguments but 1 was given
Ошибка сообщает, что функция, принимающая ноль аргументов, неожиданно получила один. Это указывает на важный факт: вызов метода через объект — это не то же самое, что вызов метода через класс. Когда метод вызывается через экземпляр, Python автоматически подставляет в качестве первого аргумента сам этот экземпляр. Строка pt.set_coords(), которую написал программист, фактически выполняется интерпретатором как:
Point.set_coords(pt)Иными словами, объект, через который был вызван метод, всегда передаётся первым позиционным аргументом — независимо от того, сколько аргументов программист указал в скобках сам. Именно поэтому метод обязан быть готов принять этот дополнительный, неявно передаваемый аргумент — а для этого в его определении должен присутствовать соответствующий параметр. По общепринятому соглашению этот параметр называют self, хотя формально это обычное имя, которое можно было бы заменить любым другим — просто вся экосистема Python, все библиотеки и весь код, который вы когда-либо будете читать, единогласно используют именно self, и отступать от этого соглашения не имеет смысла.
Исправим метод:
class Point:
color = "red"
circle = 2
def set_coords(self):
print("Вызов метода set_coords", self)pt = Point()
pt.set_coords()Вызов метода set_coords <__main__.Point object at 0x000001>
Вывод показывает, что self внутри метода — это в точности тот же объект, что и pt снаружи. Ссылка на объект, вызвавший метод, автоматически оказывается в параметре self, и с этого момента метод «знает», с каким объектом он работает в данном конкретном вызове.
Полезно убедиться, что ничего скрытого в этом механизме нет — весь эффект можно воспроизвести вручную. Если вызвать метод через класс, не передав объект, Python выдаст осмысленную ошибку:
Point.set_coords()TypeError: set_coords() missing 1 required positional argument: 'self'
Ошибка прямо называет недостающий аргумент — self. Дело в том, что при обращении Point.set_coords Python извлекает из класса обычную функцию, ничего о конкретном объекте не зная — брать этот аргумент попросту неоткуда, если объект не передан явно. Стоит передать его вручную — и вызов снова становится корректным:
Point.set_coords(pt)Вызов метода set_coords <__main__.Point object at 0x000001>
Это ровно то же самое, что Python делает автоматически при вызове pt.set_coords(). Разница между pt.set_coords() и Point.set_coords(pt) — чисто синтаксическая: они выполняют один и тот же вызов, но первый вариант короче, потому что Python сам подставляет pt в качестве self, когда метод вызывается через точечную нотацию у объекта. Технически это происходит потому, что при обращении pt.set_coords Python возвращает не саму функцию из класса, а так называемый связанный метод (bound method) — специальную обёртку, которая уже «помнит» объект pt и подставит его первым аргументом, как только метод будет вызван.
Стоит остановиться и задать вопрос, который заслуживает отдельного, честного ответа: зачем Python устроен именно так, а не проще?
Ответ связан с тем, что было разобрано в предыдущем уроке о пространствах имён. Метод, объявленный в классе, не копируется в каждый создаваемый объект — он существует в единственном экземпляре, в пространстве имён класса, и используется всеми объектами совместно. Сколько бы точек ни было создано на основе класса Point, метод set_coords в памяти будет только один. Но если метод один на всех, а данные, с которыми он должен работать, у каждого объекта свои — методу необходим способ узнать, с каким именно объектом он сейчас имеет дело. Именно эту роль и выполняет self: он превращает общий, разделяемый всеми код метода в код, привязанный к конкретному экземпляру в момент вызова.
До сих пор метод ничего полезного не делал — он лишь выводил информацию о себе. Реальная задача методов — читать и изменять данные объекта, ради которого метод и вызывается. Перепишем set_coords так, чтобы он действительно сохранял координаты:
class Point:
color = "red"
circle = 2
def set_coords(self, x, y):
self.x = x
self.y = yОбратите внимание на порядок параметров: self идёт первым, а следом — x и y, те аргументы, которые программист передаёт осознанно. При вызове:
pt = Point()
pt.set_coords(1, 2)Python снова подставляет pt в self автоматически, а 1 и 2 попадают в x и y уже как обычные аргументы. Внутри метода строка self.x = x выполняет присваивание через объект — тот самый механизм, что был разобран в прошлом уроке: создаётся (или обновляется) атрибут именно у объекта, на который ссылается self, то есть у pt. Убедиться в этом можно, заглянув в его пространство имён:
print(pt.__dict__) # {'x': 1, 'y': 2}Строка self.x = x внутри метода set_coords и строка pt.x = 1, написанная напрямую вне класса, делают абсолютно одно и то же — разница лишь в том, что метод выполняет эту операцию не с конкретным жёстко прописанным именем pt, а с тем объектом, который был передан в self при вызове. Это и позволяет одному и тому же методу одинаково корректно работать с любым количеством разных объектов.
Убедимся в этом, создав второй объект:
pt2 = Point()
pt2.set_coords(10, 20)
print(pt.__dict__) # {'x': 1, 2': 2}
print(pt2.__dict__) # {'x': 10, 'y': 20}Оба объекта хранят собственные, независимые значения координат, хотя вызывали для их установки один и тот же метод класса. Именно self обеспечивает эту независимость: в каждом отдельном вызове он указывает ровно на тот объект, через который метод был вызван, и все операции присваивания внутри метода адресуются именно этому объекту, а не какому-то другому.
Методы не обязаны только записывать данные — они точно так же могут их читать и возвращать. Добавим второй метод:
class Point:
def set_coords(self, x, y):
self.x = x
self.y = y
def get_coords(self):
return (self.x, self.y)pt = Point()
pt.set_coords(3, 5)
print(pt.get_coords()) # (3, 5)Метод get_coords принимает единственный параметр — self, и внутри обращается к self.x и self.y. Здесь снова работает обычный порядок поиска атрибута: Python ищет x и y сначала в самом объекте, на который указывает self, — и находит их там, поскольку ранее set_coords создал их именно как атрибуты объекта. Если бы get_coords был вызван для объекта, у которого координаты ещё не были установлены, возникло бы исключение AttributeError — метод не может прочитать то, чего не существует ни в объекте, ни в классе.
Раз метод — это атрибут класса, хранящий функцию, к нему применимы те же инструменты, что были разобраны для обычных атрибутов в прошлом уроке — в том числе getattr:
pt = Point()
pt.set_coords(3, 5)
method = getattr(pt, "get_coords")
print(method) # <bound method Point.get_coords of <...>>
print(method()) # (3, 5)getattr(pt, "get_coords") возвращает тот же самый связанный метод, что и обычное обращение pt.get_coords — просто имя атрибута в этом случае передаётся строкой, что бывает полезно, если оно вычисляется динамически. На практике почти всегда используется прямой синтаксис pt.get_coords() — он короче и однозначно понятнее при чтении кода, а getattr для методов применяют только тогда, когда заранее неизвестно, метод с каким именно именем нужно вызвать.
Самая частая ошибка начинающих — забыть указать self первым параметром метода:
class Point:
def set_coords(x, y): # self пропущен
self.x = xПри попытке вызвать pt.set_coords(1, 2) возникнет ошибка — либо TypeError из-за несовпадения количества аргументов (потому что Python всё равно попытается подставить pt первым параметром, и на его место попадёт x, а 1 останется без места), либо NameError: name 'self' is not defined, если внутри тела метода self используется, а среди параметров его нет. Важно закрепить: Python не создаёт self автоматически из воздуха — это обычный параметр функции, который необходимо объявить явно, как и любой другой. Название self — вопрос соглашения, а не синтаксического требования языка, но нарушать это соглашение не стоит: любой другой программист, открыв ваш код, ожидает увидеть на этом месте именно self.
Вторая ловушка связана с путаницей между вызовом через класс и вызовом через объект. Начинающие иногда пишут Point.set_coords(1, 2), ожидая, что это установит координаты какому-то объекту, — но в этом вызове 1 окажется на месте self, а 2 — на месте x, и метод попытается выполнить self.y = y, где self равен числу 1, что немедленно приведёт к ошибке, поскольку у числа нельзя создавать произвольные атрибуты. Вызов метода через класс осмыслен только тогда, когда первым аргументом сознательно передаётся конкретный объект: Point.set_coords(pt, 1, 2).
Третий момент, который стоит держать в голове: self — это не служебное слово Python, а лишь общепринятое имя параметра. Формально код будет работать, даже если этот параметр назвать иначе, например this или obj. Но такой код будет вводить в заблуждение любого, кто его читает, включая вас самих через несколько месяцев, — и по этой причине единственно правильная практика заключается в том, чтобы всегда называть первый параметр метода именно self, без исключений.
Метод — это функция, объявленная в теле класса и хранящаяся в его пространстве имён вместе с обычными атрибутами. Ключевое отличие метода от произвольной функции проявляется в момент вызова через объект: Python автоматически подставляет в качестве первого аргумента сам вызвавший объект, и для приёма этого аргумента метод обязан объявить параметр — по соглашению называемый self.
Существование self напрямую вытекает из того, что метод, в отличие от атрибутов данных, не копируется в каждый объект — он существует единственным экземпляром в классе и используется совместно всеми объектами. self — это тот механизм, который позволяет единственному экземпляру метода в каждый конкретный момент вызова работать именно с тем объектом, через который он был вызван, читая и изменяя атрибуты именно этого объекта, а не какого-то другого.
Вызов объект.метод(аргументы) и вызов Класс.метод(объект, аргументы) полностью эквивалентны — разница только в удобстве записи. Понимание этой эквивалентности снимает ощущение, что self — что-то загадочное: это обычный параметр, который просто заполняется автоматически при обращении через точку у объекта.
В следующем уроке мы разберём метод __init__ — специальный метод, который автоматически вызывается сразу после создания объекта и избавляет от необходимости вручную вызывать set_coords или подобные методы для того, чтобы объект оказался в корректном, полностью готовом к работе состоянии. Заодно станет понятно, что и __init__ подчиняется точно тому же правилу с self, которое было разобрано в этом уроке.
- Чем метод класса отличается от обычной функции с точки зрения синтаксиса и с точки зрения места хранения?
- Что в действительности происходит, когда метод вызывается через объект, например
pt.set_coords()? - Что хранится в параметре
selfи почему он обязателен первым параметром метода? - Почему вызов
Point.set_coords()без аргументов приводит к ошибке о недостающемself? - Почему методы не копируются в каждый создаваемый объект?
- Каким образом
selfобеспечивает независимость данных разных объектов одного класса? - Чем вызов
pt.get_coords()отличается от вызоваgetattr(pt, "get_coords")()? - Что произойдёт, если объявить метод без параметра
self, но внутри метода обращаться кself.x?
Объявите класс MediaPlayer с двумя методами:
open(self, file)— сохраняет имя файла в атрибуте объектаfilename;play(self)— выводит строкуВоспроизведение <имя файла>.
Создайте два объекта, media1 и media2, откройте в них разные файлы и вызовите play() для каждого.
Ожидаемый результат:
Воспроизведение filemedia1
Воспроизведение filemedia2
Объявите класс Lamp с методами turn_on(self) и turn_off(self), устанавливающими атрибут объекта state в значения "on" и "off" соответственно. Создайте объект lamp, включите и выключите его, выведите итоговое значение state.
Ожидаемый результат:
off
Объявите класс Graph с атрибутом класса LIMIT_Y = [0, 10] и методами:
set_data(self, data)— сохраняет переданный список чисел в атрибуте объектаdata;draw(self)— выводит через пробел те числа изdata, которые попадают в диапазонLIMIT_Yвключительно.
Создайте объект graph_1, передайте ему список [10, -5, 100, 20, 0, 80, 45, 2, 5, 7] и вызовите draw().
Ожидаемый результат:
10 0 2 5 7
Объявите класс Cart с методами:
add(self, product)— добавляет товар в списокitems;remove(self, product)— удаляет товар изitems, если он там присутствует;show(self)— выводит все товары через пробел.
Товары должны храниться в атрибуте объекта items.
Ожидаемый результат после добавления "apple", "milk", "bread" и удаления "milk":
apple bread
Объявите класс Translator с методами:
add(self, eng, rus)— добавляет перевод словаeng. Если слово уже существует, новый перевод добавляется как синоним, при этом повторно один и тот же перевод добавлять нельзя;remove(self, eng)— удаляет слово из словаря переводов;translate(self, eng)— возвращает список переводов слова.
Переводы должны храниться в атрибуте объекта — словаре, где ключ — английское слово, а значение — список переводов.
Добавьте переводы: tree — дерево, car — машина, car — автомобиль, leaf — лист, river — река, go — идти, go — ехать, go — ходить, milk — молоко. Удалите слово car. Выведите перевод слова go.
Ожидаемый результат:
идти ехать ходить
Дан вспомогательный класс, который считывает строку с консоли и разбивает её на значения:
class StreamReader:
FIELDS = ('id', 'title', 'pages')
def readlines(self):
lst_in = input().split()
sd = StreamData()
res = sd.create(self.FIELDS, lst_in)
return sd, resОбъявите класс StreamData с методом create(self, fields, lst_values), который:
- принимает кортеж названий полей
fieldsи список значенийlst_values; - создаёт в объекте атрибуты с именами из
fields, присваивая им соответствующие значения изlst_valuesпо порядку; - если количество полей и количество значений не совпадает, метод не должен создавать никаких атрибутов и обязан вернуть
False; - если всё прошло успешно, метод возвращает
True.
Продемонстрируйте работу класса напрямую, без чтения с консоли: создайте объект StreamData, вызовите create с fields = ('id', 'title', 'pages') и lst_values = ['1', 'Python', '500'], выведите получившиеся атрибуты.