Skip to content

Latest commit

 

History

History
337 lines (221 loc) · 28.4 KB

File metadata and controls

337 lines (221 loc) · 28.4 KB

Урок 3. Методы класса и параметр self

В прошлом уроке был разобран фундаментальный механизм: класс — это пространство имён, а объект получает доступ к атрибутам класса через определённый порядок поиска. Всё, что рассматривалось до сих пор, касалось данных — атрибутов, хранящих значения. Но класс описывает не только то, чем объект обладает, но и то, что объект умеет делать. Именно за это отвечают методы — функции, объявленные внутри тела класса.

На первый взгляд метод выглядит как обычная функция, слегка изменившая место прописки — теперь она находится не на верхнем уровне модуля, а внутри класса. Но при вызове метода через объект происходит нечто, что для многих начинающих становится источником путаницы: перед первым «настоящим» аргументом всегда обязан стоять параметр с именем 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: он превращает общий, разделяемый всеми код метода в код, привязанный к конкретному экземпляру в момент вызова.

Работа с данными объекта через 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, которое было разобрано в этом уроке.


Вопросы для проверки

  1. Чем метод класса отличается от обычной функции с точки зрения синтаксиса и с точки зрения места хранения?
  2. Что в действительности происходит, когда метод вызывается через объект, например pt.set_coords()?
  3. Что хранится в параметре self и почему он обязателен первым параметром метода?
  4. Почему вызов Point.set_coords() без аргументов приводит к ошибке о недостающем self?
  5. Почему методы не копируются в каждый создаваемый объект?
  6. Каким образом self обеспечивает независимость данных разных объектов одного класса?
  7. Чем вызов pt.get_coords() отличается от вызова getattr(pt, "get_coords")()?
  8. Что произойдёт, если объявить метод без параметра self, но внутри метода обращаться к self.x?

Задачи

Задача 1.

Объявите класс MediaPlayer с двумя методами:

  • open(self, file) — сохраняет имя файла в атрибуте объекта filename;
  • play(self) — выводит строку Воспроизведение <имя файла>.

Создайте два объекта, media1 и media2, откройте в них разные файлы и вызовите play() для каждого.

Ожидаемый результат:

Воспроизведение filemedia1
Воспроизведение filemedia2

Задача 2.

Объявите класс Lamp с методами turn_on(self) и turn_off(self), устанавливающими атрибут объекта state в значения "on" и "off" соответственно. Создайте объект lamp, включите и выключите его, выведите итоговое значение state.

Ожидаемый результат:

off

Задача 3.

Объявите класс 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

Задача 4.

Объявите класс Cart с методами:

  • add(self, product) — добавляет товар в список items;
  • remove(self, product) — удаляет товар из items, если он там присутствует;
  • show(self) — выводит все товары через пробел.

Товары должны храниться в атрибуте объекта items.

Ожидаемый результат после добавления "apple", "milk", "bread" и удаления "milk":

apple bread

Задача 5.

Объявите класс Translator с методами:

  • add(self, eng, rus) — добавляет перевод слова eng. Если слово уже существует, новый перевод добавляется как синоним, при этом повторно один и тот же перевод добавлять нельзя;
  • remove(self, eng) — удаляет слово из словаря переводов;
  • translate(self, eng) — возвращает список переводов слова.

Переводы должны храниться в атрибуте объекта — словаре, где ключ — английское слово, а значение — список переводов.

Добавьте переводы: tree — дерево, car — машина, car — автомобиль, leaf — лист, river — река, go — идти, go — ехать, go — ходить, milk — молоко. Удалите слово car. Выведите перевод слова go.

Ожидаемый результат:

идти ехать ходить

Задача 6.

Дан вспомогательный класс, который считывает строку с консоли и разбивает её на значения:

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'], выведите получившиеся атрибуты.


Предыдущий урок | Следующий урок