13. Методы

13.1. Функции для объектов

Не так просто дать определение, что же такое объектно-ориентированное программирование, но мы уже видели некоторые его свойства:

  1. Программы строятся из определений объектов и определений функций, и большинство вычислений выражается в терминах операций над объектами.
  2. Каждое определение объекта соответствует некоторому объекту или понятию реального мира, а функции, работающие с объектом, соответствуют тому, как взаимодействуют реальные объекты.

Например, класс Time, определенный в предыдущей главе, соответствует тому, как люди привыкли записывать время. А определенные нами функции соответствуют действиям, которые люди выполняют над временем. Подобным же образом, классы Point и Rectangle соответствуют математическим понятиям точки и прямоугольника.

До сих пор мы не пользовались средствами, которые Python предоставляет для объектно-ориентированного программирования. Строго говоря, эти средства не являются необходимыми для создания программ. По большей части, они предоставляют альтернативный синтаксис для тех вещей, которые мы уже делали. Однако, часто этот синтаксис оказывается более кратким и выразительным.

Например, в программе Time нет очевидной связи между определением класса и следующими за ним определениями функций. Если присмотреться, то оказывается, что каждая из функций принимает в качестве параметра, по крайней мере, один объект Time.

Это наблюдение мотивирует нас перейти к методам. Мы уже встречали некоторые методы, например, keys и values, которые вызываются на словарных объектах. Каждый метод связан с классом и предназначен для использования с объектами этого класса.

Методы похожи на функции, но есть два отличия:

  1. Методы определяются внутри определения класса, чтобы сделать отношения между классом и методом явными.
  2. Синтаксис для вызова метода отличается от синтаксиса для вызова функции.

В следующих разделах мы превратим функции из предыдущей главы в методы. Это преобразование почти механическое. Мы сделаем это, выполнив определенную последовательность шагов.

13.2. Метод increment

Для начала давайте превратим в метод функцию increment.

Для этого достаточно поместить определение функции внутрь определения класса. Хорошим тоном будет также переименование параметра time в self (хоть делать это и не обязательно). Как вы, должно быть, помните, в сообществе программистов Python существует соглашение, согласно которому первому параметру метода дают имя self (англ.: сам).

Обратите внимание на сдвиг кода метода относительно заголовка класса:

class Time:

    def increment(self, seconds):
        self.seconds += seconds

        while self.seconds >= 60:
            self.seconds -= 60
            self.minutes += 1

        while self.minutes >= 60:
            self.minutes -= 60
            self.hours += 1

Преобразование чисто механическое — мы переместили определение метода в определение класса, и изменили имя первого параметра.

Теперь можно вызвать increment как метод, используя точечную нотацию:

>>> my_time.increment(600)

Объект, на котором вызывается метод, присваивается первому параметру метода. Таким образом, в данном случае my_time присваивается параметру self. Второй параметр, seconds, получает значение 600.

В процедурном программировании предполагается, что функции выполняют необходимые действия. Синтаксис для вызова функции , increment(my_time, 600), говорит: Эй, increment! Вот тебе объект Time и 600 секунд, сделай с ними все необходимое.

Синтаксис объектно-ориентированном программировании предполагает, что необходимые действия выполняет объект. Вызов, подобный my_time.increment(600) говорит: Эй, my_time! Пожалуйста, увеличь себя на 600 секунд!

Является ли полезным такое изменение взгляда на вещи? Дело в том, что иногда, передавая ответственность от функций объектам, мы можем писать более гибкий код. Также становится проще поддерживать и повторно использовать такой код.

13.3. Более сложный пример

Функция after немного более сложная, поскольку она имеет дело с двумя объектами Time. Первый из параметров переименуем в self, второй оставим без изменений:

class Time:
    #previous method definitions here...

    def after(self, time2):
        if self.hour > time2.hour:
            return True
        if self.hour < time2.hour:
            return False

        if self.minute > time2.minute:
            return True
        if self.minute < time2.minute:
            return False

        if self.second > time2.second:
            return True
        return False

Мы вызываем этот метод на одном объекте Time, и передаем второй объект Time в качестве аргумента:

if doneTime.after(current_time):
    print "The bread will be done after it starts."

Программа состоит из предложений на почти естественном английском языке: Если время готовности (done_time) позднее, чем текущее время (current_time), то...

13.4. Инициализирующий метод

Как мы уже знаем, инициализирующий метод — это специальный метод, который вызывается при создании объекта. Этот метод имеет имя __init__ (два символа подчеркивания, init, и еще два символа подчеркивания). Инициализирующий метод класса Time выглядит так:

class Time:
    def __init__(self, hours=0, minutes=0, seconds=0):
        self.hours = hours
        self.minutes = minutes
        self.seconds = seconds

Заметьте, что между атрибутом self.hours и параметром hours не возникает конфликта имен. Точечная нотация устраняет конфликт.

Метод, вызываемый при создании объекта и инициализирующий состояние объекта, также называют конструктором. В Python метод __init__ является конструктором.

Когда мы создаем объект Time, указанные нами аргументы передаются конструктору:

>>> current_time = Time(9, 14, 30)
>>> print_time(current_time)
>>> 09:14:30

Поскольку параметры метода __init__ имеют значения по умолчанию, мы можем и не передавать аргументы при создании объекта:

>>> current_time = Time()
>>> print_time(current_time)
>>> 00:00:00

Или передать только первый аргумент:

>>> current_time = Time(9)
>>> print_time(current_time)
>>> 09:00:00

Или только первые два аргумента:

>>> current_time = Time(9, 14)
>>> print_time(current_time)
>>> 09:14:00

Мы также можем передать часть аргументов, явно поименовав их:

>>> current_time = Time(seconds = 30, hours = 9)
>>> print_time(current_time)
>>> 09:0:30

13.5. Метод __str__

Метод __str__ имеет специальное назначение в Python, он возвращает строковое представление объекта. Определим метод __str__ для класса Time, позаимствовав решение из функции print_time из предыдущей главы:

class Time:
    #previous method definitions here...

    def __str__(self):
        return "%02i:%02i:%02i" % (self.hours, self.minutes, self.seconds)

Если класс предоставляет метод с именем __str__, то тем самым переопределяет поведение встроенной функции Python str.

>>> t = Time()
>>> str(t)
00:00:00

При выводе объекта Time с помощью print неявно вызывается __str__ на этом объекте. Поэтому добавление метода __str__ также меняет поведение print:

>>> t = Time(9)
>>> print t
09:00:00

Как видите, добавление метода __str__ к классу Time сделало ненужным написанную ранее функцию print_time.

Когда мы пишем новый класс, мы почти всегда начинаем с написания метода __init__, который облегчает создание объектов, и метода __str__, который часто полезен для отладки.

13.6. Снова Points

Теперь, для закрепления изученного материала, давайте перепишем класс Point в стиле ООП:

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __str__(self):
        return '(' + str(self.x) + ', ' + str(self.y) + ')'

Инициализирующий метод принимает x и y как опциональные параметры, значение по умолчанию для каждого из них 0.

Метод __str__ возвращает строковое представление объекта Point:

>>> p = Point(3, 4)
>>> str(p)
'(3, 4)'

13.7. Перегрузка операторов

Некоторые языки программирования позволяют изменять определения встроенных операторов для использования этих операторов с типами, определенными пользователем. Это свойство называется перегрузкой операторов.

Например, для перегрузки оператора +, класс должен предоставить метод __add__:

class Point:
    # previously defined methods here...

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

Как обычно, первый параметр метода представляет объект, на котором вызывается метод. Второй параметр удачно назван other (англ.: другой) чтобы противопоставить его первому, self. Для того, чтобы сложить два объекта Point, мы создаем и возвращаем новый объект Point, содержащий сумму координат x и сумму координат y двух объектов.

Теперь, когда мы применяем оператор + к объектам Point, Python вызывает метод __add__:

>>>  p1 = Point(3, 4)
>>>  p2 = Point(5, 7)
>>>  p3 = p1 + p2
>>>  print p3
(8, 11)

Выражение p1 + p2 равнозначно выражению p1.__add__(p2), только более изящно.

В качестве упражнения вам будет предложено самостоятельно написать метод __sub__(self, other), который перегрузит оператор вычитания.

Перегрузить оператор умножения можно, определив метод __mul__, или __rmul__, или оба эти метода. Если левый операнд оператора * является объектом Point, то Python вызовет метод __mul__, который ожидает, что второй операнд также является объектом Point. Этот метод рассчитает произведение точек согласно известной из математики формуле (сумма квадратов катетов равна квадрату гипотенузы):

def __mul__(self, other):
    return self.x * other.x + self.y * other.y

Если левый операнд оператора * является примитивным числовым типом, а правый операнд — объект Point, то Python вызовет метод __rmul__, который выполнит умножение объекта Point на число:

def __rmul__(self, other):
    return Point(other * self.x,  other * self.y)

Результатом будет новый объект Point, чьи координаты кратны первоначальным координатам. Если other окажется типом, который нельзя умножить на число с плавающей точкой, то __rmul__ сгенерирует ошибку.

Следующий пример демонстрирует оба вида умножения:

>>> p1 = Point(3, 4)
>>> p2 = Point(5, 7)
>>> print p1 * p2
43
>>> print 2 * p2
(10, 14)

А что случится, если мы попробуем вычислить p2 * 2? Так как первый аргумент является объектом Point, то Python вызовет __mul__ и передаст 2 в качестве второго аргумента. Внутри метода, __mul__ попытается получить атрибут x объекта other, что закончится неудачей, поскольку целое число не имеет атрибутов:

>>> print p2 * 2
AttributeError: 'int' object has no attribute 'x'

13.8. Полиморфизм

Большинство написанных нами методов работают только с определенными типами данных. Когда создается новый класс, то пишутся методы, которые работают с объектами этого класса.

Но есть некоторые операции, которые хотелось бы уметь выполнять с разными типами данных, например, арифметические операции из предыдущего раздела. Если разные типы поддерживают одни и те же операции, значит, можно писать функции, работающие с любым из этих типов.

Например, операция multadd (обычная в линейной алгебре) имеет три параметра; первые два перемножаются, и к полученному произведению прибавляется третий. Можем записать это на языке Python таким образом:

def multadd (x, y, z):
    return x * y + z

Этот метод будет работать с любыми значениями x и y, которые можно перемножить, и с любым значением z, которое можно прибавить к полученному произведению.

Можно вызвать этот метод с числовыми значениями:

>>> multadd (3, 2, 1)
7

Или с объектами Point:

>>> p1 = Point(3, 4)
>>> p2 = Point(5, 7)
>>> print multadd (2, p1, p2)
(11, 15)
>>> print multadd (p1, p2, 1)
44

В первом случае, Point умножается на число и складывается с другим Point. Во втором случае, произведение двух Point дает числовое значение, и третий аргумент также является числом.

Функции, подобные этой, которые могут принимать аргументы различных типов, называются полиморфными.

В качестве еще одного примера, рассмотрим метод front_and_back, который печатает список дважды, сначала — в прямом, а затем и в обратном порядке:

def front_and_back(front):
    import copy
    back = copy.copy(front)
    back.reverse()
    print str(front) + str(back)

Поскольку метод reverse модифицирующий, мы делаем копию списка, прежде чем расставить его элементы в обратном порядке. Таким образом, метод reverse не изменяет список, который он получает в качестве параметра.

Вот пример использования метода front_and_back со списком:

>>>   myList = [1, 2, 3, 4]
>>>   front_and_back(myList)
[1, 2, 3, 4][4, 3, 2, 1]

Поскольку мы предназначили эту функцию для работы со списками, неудивительно, что она работает, как ожидалось. Было бы удивительно, если бы мы смогли применить эту функцию к объекту Point.

Для того, чтобы определить, может ли функция быть применена к новому типу, мы воспользуемся основным правилом полиморфизма: Если все операции внутри функции могут быть применены к данному типу, то вся функция может быть применена к данному типу.

Операции внутри функции включают copy, reverse и print.

copy работает с любым объектом. Мы уже написали метод __str__ для Point. Нам остается написать метод reverse для класса Point:

def reverse(self):
    self.x , self.y = self.y, self.x

Теперь можно передать объект Point функции front_and_back:

>>>   p = Point(3, 4)
>>>   front_and_back(p)
(3, 4)(4, 3)

Замечательное свойство языка Python — это непреднамеренный полиморфизм, когда вы обнаруживаете, что написанная вами функция может работать с типами, для которых вы ее не предназначали.

13.9. Глоссарий

инициализирующий метод
Специальный метод, который автоматически вызывается при создании нового объекта и инициализирует данные этого объекта.
конструктор
То же, что инициализирующий метод.
метод
Функция, определенная внутри определения класса и вызываемая при помощи точечной нотации.
объектно-ориентированный язык программирования
Язык, предоставляющий средства объектно-ориентированного программирования, такие, как определяемые пользователем классы и наследование, что облегчает объектно-ориентированное программирование.
объектно-ориентированное программирование
Стиль программирования, предполагающий организацию данных и операций для работы с этими данными в виде классов.
перегрузка операторов
Распространение использования встроенных операторов ( +, -, *, >, <, etc.) на типы, определенные пользователем.
полиморфная функция
Функция, которая может работать с данными более чем одного типа. Если каждая из операций внутри функции может быть выполнена для данного типа, то и вся функция может работать с данным типом.

13.10. Упражнения

  1. Переделайте функцию convert_to_seconds в метод класса Time:

    def convert_to_seconds(t):
        minutes = t.hours * 60 + t.minutes
        seconds = minutes * 60 + t.seconds
        return seconds
    
  2. Добавьте в класс Point метод __sub__(self, other), который перегрузит оператор вычитания, и попробуйте с ним поработать.

  3. Перепишите класс Rectangle в объектно-ориентированном стиле, определив методы __init__ и __str__.

  4. Добавьте в класс Rectangle методы move_rect и grow_rect, созданные на основе одноименных функций из предыдущей главы.

  5. Определите класс Pet (англ.: любимое животное) с атрибутами имя и возраст, инициализируемыми в методе __init__ значениями параметров метода. Метод __str__ должен возвращать строку с именем и возрастом животного. Поэкспериментируйте, создавая объекты класса Pet и выводя их на печать.