Классы: введение в объектно-ориентированное программирование

Ссылки и объекты

Представление переменных в языке Python сильно отличается от традиционных языков программирования: Pascal, C, Java. Любая сущность, с которой работает Python-программа, является объектом. Числа, строки, списки, множества являются объектами. Названия функций также являются объектами и в некотором смысле ничем не отличаются от чисел, кроме того, что у функций есть операции, нетипичные для чисел (например, вызов функции при помощи операции ()), а другие операции, наоборот, отсутствуют.

У каждого объекта есть свой тип (это также называется классом): int, str, set, function и т.д.

Переменные в свою очередь являются ссылками на объекты: в любой момент времени переменная связана с каким-то объектом в памяти. Если мы выполняем присваивание:

a = 5

то имя переменной 5 связывается с объектом типа int, в котором хранится значение 5.

Если теперь выполнить другое присваивание:

a = 'hello'

то старая связь разрывается, создается новый объект типа str, в котором хранится значение 'hello', и переменная a связывается с этим объектом.

Если же теперь выполнить операцию:

def a():
    pass

то в памяти создается объект типа function, хранящий тело функции из одной инструкции pass, и переменная a теперь связывается с этой функцией.

Если же справа от оператора = поставить имя другого объекта:

b = a

то второй объект связывается с тем же объектом, с которым был связан первый объект: теперь b и a становятся ссылками на один и тот же объект и являются неразличимыми, пока для одного из них связь не будет разорвана при помощи оператора =

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

a is b

При этом переменные могут ссылаться на различные, но равные объекты, то есть оператов == для них вернет True. Пример различных, но равных объектов:

a = 'a' * 1000
b = 'aa' * 500

Но при этом в целях экономии памяти если переменным присвоить значение одного и того же небольшого целого числа или одной и той же небольшой строки, то переменные будут ссылаться на один и тот же объект, даже если значение этого объекта будет получено разными путями. Например:

a = 2 + 3
b = 1 + 4

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

Такое повторное использование применяется только для небольших чисел, строк и объектов типа bool, поскольку для больших объектов проверка того, что объект уже хранится в памяти требует много времени.

Создание новых типов данных

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

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

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

В этом случае придется поставать так:

Vasya = ('Василий', 4)
print('Имя:', Vasya[0])
print('Балл:', Vasya[1])

Но элементы кортежей не имеют собственных названий (то есть вовсе неочевидно, балл хранится именно в элементе кортежа с индексом 1). Удобней было бы обозначить атрибуты каждого объекта какими-то осмысленными идентификаторами и далее работать с этими идентификаторами. Пример:

class Person:
    pass

Vasya = Person()
Vasya.name = 'Василий'
Vasya.score = 4

В этом примере мы объявляем новый класс объектов: Person. Затем мы создаем новый объект класса, при помощи вызова конструктора Person(), который возвращает ссылку на новый объект, и присваиваем ссылку на этот объект переменной Vasya, которая также называется экземпляром класса или объектом. Далее объекту Vasya устанавливается два атрибута: name (типа str) и score (типа int).

Поля и методы

Таким образом, объе ты классов представляют собой новые типы данный, объединяющие несколько атрибутов (полей). Атрибуты могут быть произвольными типами данных: числами, строками, списками, множествами, словарями, другими классами. Обращение к атрибуту какого-либо объекта осуществляется при помощи dot-нотации: имя_класса.имя_атрибута.

Помимо полей у классов бывают методы: функции, которые можно применять к экземплярам класса. Например, у списков есть метод sort. Вызов метода также осуществляется при помощи dot-нотации, например: A.sort().

Можно рассматривать методы, как функции, у которых первым параметром является экземпляр класса. Методы так и объявляются: как функции внутри описания класса, первым параметром которой является экземпляр класса. По соглашению, эта ссылка должна называться self. Вот пример объявления класса Person и метода print, выводящего информацию о полях name и score:

class Person:
    def print(self):
        print(self.name, self.score)

Теперь для вызова метода print для объекта Vasya нужно вызвать Vasya.print(). При этом не нужно задавать первый параметр self: в качестве этого параметра автоматически будет передан объект, для которого был вызван метод.

Методы могут принимать дополнительные параметры, как и обычные функции. Эти параметры описываются после параметра self.

Стандартные методы

Наш метод print предполагает, что у объекта есть поля name и score, иначе он завершится с ошибкой. Хочется быть уверенным, что у любого объекта класса Person есть эти поля. Для этого проще всего создать эти поля при создании объекта, т.е. при вызове функции Person. Для этого можнзо использовать конструктор: метод, который автоматически вызывается при создании объекта. Конструктором является метод с именем __init__:

class Person:
    def __init__(self):
        self.name = ''
        self.score = 0

При создании объекта функцией Person будет автоматически вызван конструктор __init__ (явно вызывать его не нужно), который полю name объекта, для которого он вызван, присвоит пустую строку, а полю score присвоит значение 0.

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

class Person:
    def __init__(self, name, score):
        self.name = name
        self.score = score

В данном случае мы используем одинаковые имена (name, score) для обозначения передаваемых параметров и полей класса. Это сделано для удобства — имена могут и различаться.

Теперь мы сможем создавать новый объект с заданными полями так: Person('Иванов', 5).

Но поскольку конструктор теперь обязательно принимает два дополнительных параметра мы лишились возможности вызывать конструктор без параметров, что также бывает удобно. Можно вернуть эту особенность, если установить для параметров, передаваемых конструктору, значения по умолчанию:

class Person:
    def __init__(self, name = '', score = 0):
        self.name = name
        self.score = score

Теперь мы можем вызывать конструктор как с параметрами (Person('Иванов', 5)), так и без параметров (Person()), в последнем случае параметрам будут переданы значения “по умолчанию”, указанные в описании конструктора.

Есть и другие стандартные методы, которые можно определить в описании класса.

Метод __repr__ должен возвращать текстовую строку, содержащую код (на языке Python), создающую объект, равный данному. Естественно, метод __repr__ должен содержать вызов конструктора, которому передаются в качестве параметров все строки исходного объекта, то есть он должен возвращать строку вида "Person('Иванов', 5)"

Пример метода __repr__ (для экономии места опустим описание конструктора __init__):

class Person:
    def __repr__(self):
        return "Person('" + self.name + "', " + self.score + ")"

Таким образом, метод __repr__ возвращает строку с описанием объекта, которое может быть воспринято итерпретатором языка Питон.

Метод __str__ возвращает строку, являющуюся описанием объекта в том виде, в котором его удобно будет воспринимать человеку. Здесь не нужно выводить имя конструктора, можно, например, просто вернуть строку с содержимым всех полей:

class Person:
    def __str__(self):
        return self.name + ' ' + str(self.score)

Метод __str__ будет вызываться, когда вызывается функция str от данного объекта, например, str(Vasya). То есть создавая метод __str__ вы даете указание Питону, как преобразовывать данный объект к типу str.

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

Переопределение стандартных операций

Рассмотрим класс Point (точка), используемый для представления точек (или радиус-векторов) на координатной плоскости. У точки два естественных поля-координаты: x и y. Если рассматривать точку как радиус-вектор, то хотелось бы определить для точек операцию +, чтобы точки можно было складывать столь же удобно, как и числа или строки. Например, чтобы можно было записать так:

A = Point(1, 2)
B = Point(3, 4)
C = A + B

Для этого необходимо перегрузить операцию +: определить функцию, которая будет использоваться, если операция + будет вызвана для объекта класса Point. Для этого нужно определить метод __add__ класса Point, у которого два параметра: неявная ссылка self на экземпляр класса, для которого она будет вызвана (это левый операнд операции +) и явная ссылка other на правый операнд:

class Point:
    def __init__(self, x = 0, y = 0):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

Теперь при вызове оператора A + B Питон вызовет метод A.__add__(B), то есть вызовет указанный метод, где self = A, other = B.

Аналогично можно определить и оставшиеся операции. Полезной для переопределения является операция <. Она должна возвращать логическое значение True, если левый операнд меньше правого или False в противном случае (также в том случае, если объекты равны). Для переопределения этого операнда нужно определить метод __lt__ (less tthan):

class Point:
    def __lt__(self, other):
        return self.x < other.x or self.x == other.x and self.y < other.y

В этом примере оператор вернет True, если у левого операнда поле x меньше, чем у правого операнда, а также если поля x у них равны, а поле y меньше у левого операнда.

После определения оператора <, появляется возможность упорядочивать объекты, используя этот оператор. Теперь можно сортировать списки объектов при помощи метода sort() или функции sorted, при этом будет использоваться именно определенный оператор сравнения <.

Функции type и isinstance

Полезно, чтобы конструктор __init__ мог воспринимать параметры различных типов. Например, удобно инициализировать точку не только двумя числами, но и строкой, в которой через пробел записаны два числа (такая строка может быть считана со стандартного ввода), списком или кортежем. То есть передаваемые конструктору аргументы могут быть разного типа (int, float, str, list, tuple). Конструктор должен выполнять различные действия для параметров различного типа, для этого нужно уметь проверять принадлежность объекту какому-либо классу.

Эту задачу можно решить при помощи функций type и isinstance. Функция type возвращает класс, к которому принадлежит объект. Например:

if type(a) == int:
    print('a -  целое число')
elif type(a) == str:
    print('a - строка')

Для этого можно использовать функцию isinstance, у которой два параметра: объект и класс. Функция возращает True, если объект принадлежит классу или False в противном случае. Пример:

if isinstance(a, int):
    print('a -  целое число')
elif isinstance(a, str):
    print('a - строка')

Список возможных перегружаемых операторов

Полная документация на английском: http://docs.python.org/py3k/reference/datamodel.html.

Следующая таблица взята из книги Саммерфильда (стр. 283 и далее).

Метод Использование
Операторы сравнения
__lt__(self, other) x < y
__le__(self, other) x <= y
__eq__(self, other) x == y
__ne__(self, other) x != y
__gt__(self, other) x > y
__ge__(self, other) x >= y
Арифметические операторы
Сложение
__add__(self, other) x + y
__radd__(self, other) y + x
__iadd__(self, other) x += y
Вычитание
__sub__(self, other) x - y
__rsub__(self, other) y - x
__isub__(self, other) x -= y
Умножение
__mul__(self, other) x * y
__rmul__(self, other) y * x
__imul__(self, other) x *= y
Деление
__truediv__(self, other) x / y
__rtruediv__(self, other) y / x
__itruediv__(self, other) x /= y
Целочисленное деление
__floordiv__(self, other) x // y
__rfloordiv__(self, other) y // x
__ifloordiv__(self, other) x //= y
__divmod__(self, other) divmod(x, y)
Остаток
__mod__(self, other) x % y
__rmod__(self, other) y % x
__imod__(self, other) x %= y
Возведение в степень
__pow__(self, other) x ** y
__rpow__(self, other) y ** x
__ipow__(self, other) x **= y
Отрицание, модуль
__pos__(self) +x
__neg__(self) -x
__abs__(self) abs(x)
Преобразование к стандартным типам
__int__(self) int(x)
__float__(self) float(x)
__str__(self) str(x)
__round__(self, digits = 0) round(x, digits)

Упражнения

Класс “Точка” (Point)

Создайте класс Point, определите для него конструктор, метод __str__, необходимые арифметические операции. У класса Point два поля: x и y.

A: Самая дальняя точка

Программа получает на вход число N, далее координаты N точек. Выведите координаты точки, наиболее удаленной от начала координат.

Реализация класса должна содержать конструктор, метод __str__ для вывода точки, метод dist, который возвращает расстояние от начала координат до точки.

Ввод Вывод
2
1 2
2 3
2 3

B: Центр масс

Выведите координаты центра масс данного множества точек (учтите, что это —два действительных числа).

Для создания точки определите конструктор, который мог бы принимать на вход как два числа, так и строку. Таким образом, точку можно будет создавать как вызовом Point(x, y), так и Point(input()).

Определите операции сложения точек, умножения точки на число, деления точки на число.

Ввод Вывод
2
1 2
2 3
1.5 2.5

C: Диаметр множества

Выведите диаметр данного множества – максимальное расстояние между двумя данными точками.

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

Ввод Вывод
3
1 1
1 0
0 0
1.4142135623731

D: Сортировка

Определите для точек операцию сравнения __lt__, сравнивающую точки по значению расстояния от начала координат. Отсортируйте данные точки в порядке возрастания расстояния от начала координат.

Ввод Вывод
3
1 0
-1 -1
0 0
0 0
1 0
-1 -1

E: Максимальный периметр

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

Для нахождения периметра треугольника напишите отдельную функцию Perimeter(A, B, C).

Ввод Вывод
4
0 0
0 1
1 0
1 1
3.41421356237309

F: Максимальная площадь

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

Для нахождения площади треугольника напишите отдельную функцию Area(A, B, C).

Ввод Вывод
4
0 0
0 1
1 0
1 1
0.5

Класс “Дробь” (Fraction)

Класс Fraction должен иметь два поля: числитель a и знаменатель b. Оба поля должны быть типа int.

Для класса Fraction определите конструктор, который может принимать следующие виды параметров:

У каждой дроби существует единственное каноническое представление. Каноническое представление: это такое представление \(\frac{a}{b}\), что \(b>0\), \( (a, b) = 1\). Класс должен иметь метод reduce, который приводит дробь к каноническому представлению. После каждой операции с дробями (в том числе и в конструкторе) необходимо вызывать метод reduce для сокращения дроби.

Необходимо определить метод __str__, который выводит a/b в каноническом представлении, если же дробь является целым числом, то просто значение этого числа.

G: Сократите дробь

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

Ввод Вывод
1 2
30/-9
8/4
3
1/2
-10/3
2
3

H: Конструкторы

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

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

exec(sys.stdin.read())
Ввод Вывод
a = Fraction()
b = Fraction(1)
c = Fraction(1, 3)
d = Fraction('14 35')
e = Fraction('-6/2')
f = Fraction(e)
print(a, b, c, d, e, f, sep='\n')
0
1
1/3
2/5
-3
-3

I: Сравнения

Определите методы __lt__, __le__, __gt__, __ge__, __eq__, __ne__. В качестве параметра other может выступать объект одного из трех типов: int, float, Fraction.

Тестирование проводится, как в предыдущей задаче.

J: Умножение

Определите методы умножения __mul__, __rmul__, __imul__ так, чтобы можно было:

Методы типа __imul__, то есть переопределяющие операторы присваивания (*=) должны возвращать self.

K: Деление

Определите методы деления __truediv__, __rtruediv__, __itruediv__.

L: Возведение в степень

Определите операции возведения в степень __pow__, __rpow__ так, чтобы можно было возводить дроби в степень типа int, float, Fraction, числа типа int и float в степень Fraction. Операция возведения Fraction ** int возвращает Fraction, во всех остальных случаях возвращается float.

Определите операцию __ipow__ для возведения дроби в целочисленную степень.

M: Сложение

Определите операции __add__, __iadd__, __radd__.

N: Вычитание

Определите операции __sub__, __isub__, __rsub__.

O: Знаки

Определите операции __pos__, __neg__, __abs__.

P: Преобразование типов

Определите операции __int__ (должна округлять вниз до ближайшего целого), __float__, __round__ (должна возвращать значение типа float, можно использовать функцию round для величины типа float).