Практика: Pygame, шарики и ООП
Общая информация
При процедурном программировании программа разбивается на части в соответствии с алгоритмом: каждая функция является составной частью алгоритма. При объектно-ориентированном программировании программа строится как совокупность взаимодействующих объектов. Что же такое объект? С точки зрения объектно-ориентированного подхода, объект - это нечто, обладающее состоянием и поведением. По сути, состояние - это данные, соответствующие объекту, например рост, вес и громкость гавканья собаки. Поведением называется набор операций, которые можно производить с объектом. Например, в случае с собакой: покормить, погладить, погулять. Эти операции, которые можно выполнять над объектом обычно называют методами (по крайней мере в контексте языка Python).
Часто приходится работать с объектами одной природы. Например, если у нас несколько собак, то у них у всех одинаковые наборы данных (хотя значения могут различаться) и одинаковые методы. Для определения такой "общей природы" вводятся классы. Класс, по сути, есть шаблон объектов - базовое состояние и общее поведение для всех объектов этого класса. Объекты одного класса называют экземплярами этого класса. Также, класс является типом данных для объектов.
В языке Python для определения класса используется оператор class. Рассмотрим следующий пример:
class Dog:
def say_gaw(self): # имя self для первого аргумента метода это общепринятое но не обязательное правило
print('Gaw-gaw')
my_dog = Dog()
another_dog = Dog()
my_dog.say_gaw() # вызовется функция Dog.say_gaw с параметром self = my_dog
another_dog.say_gaw()
Здесь мы описали класс Dog, который задает один метод. При описании методов класса первый аргумент есть ссылка на экземепляр, для которого этот метод вызывается. Далее, мы создали пару собак и позвали для каждой метод say_gaw. Для создания объектов используется имя класса со скобками. Методы вызываются через точку после имени объекта. Заметьте, что первый аргумент метода - self - при вызове указывать не нужно, т.к. им становится сам объект (тот для которого зовем метод, его имя перед точкой).
Для хранения данных в объектах используются атрибуты. Это те самые "свойства" объекта - рост, вес и т.п. Атрибуты могут иметь любой тип данных. Так же как и с обычными переменными в Python, объявлять атрибуты неким специальным образом не нужно, они появляются автоматически, при первом присваивании, следующим образом:
class Dog:
def say_gaw(self):
if self.angry:
print('GAW-GAW')
else:
print('Gaw-gaw')
def ping(self):
self.angry = True
def feed(self, food_count):
if food_count > 10:
self.angry = False
my_dog = Dog()
my_dog.feed(20)
my_dog.say_gaw() # напечатает Gaw-gaw
my_dog.ping()
my_dog.say_gaw() # напечатает GAW-GAW
Часто для атрибутов хочется иметь некоторое начальное значение. В предыдущем примере есть проблема - если собака попытается гавкнуть до того как ее пнули или покормили, она навернется с ошибкой "AttributeError: 'Dog' object has no attribute 'angry'". Для решения этой проблемы используется метод со специальным именем - __init__, который вызывается автоматически при создании объекта:
class Dog:
def __init__(self):
self.angry = False
def say_gaw(self):
if self.angry:
print('GAW-GAW')
else:
print('Gaw-gaw')
my_dog = Dog()
my_dog.say_gaw() # ошибки нет, напечатает Gaw-gaw
Метод __init__
называется конструктором. Собственно, конструктор зовется при выполнении конструкции вида ИмяКласса()
, в нашем случае - Dog()
. Аргументом self
для конструктора становится вновь созданный объект. Конструктор, также как и обычные методы, может иметь дополнительные аргументы кроме self
. Эти аргументы передаются при создании объекта, следующим образом:
class Dog:
def __init__(self, angry, count):
self.angry = angry
self.count = count
def say_gaw(self):
if self.angry:
print('GAW-' * self.count)
else:
print('gaw-' * self.count)
my_dog = Dog(True, 3)
my_dog.say_gaw() # ошибки нет, напечатает Gaw-gaw
Класс в Python также является объектом. Объект этот создается с помощью ключевого слова class
, как в примерах выше. Таким образом, в предыдущем примере вызов my_dog.say_gaw()
эквивалентен вызову Dog.say_gaw(my_dog)
. Разобраться, какой объект какому классу принадлежит помогут встроенные функции type
и isinstance
:
>>> class A:
... pass
...
>>> a = A()
>>> type(a)
<class '__main__.A'>
>>> type(A)
<class 'type'>
>>> type(type)
<class 'type'>
>>> type(1)
<class 'int'>
>>> type(int)
<class 'type'>
>>>
>>> isinstance(1, int)
True
>>> isinstance(1, A)
False
>>> isinstance(a, A)
True
>>> isinstance(type, type)
True
>>> isinstance(A, type)
True
Примечание: здесь был объявлен, в тестовых целях, пустой класс A - в нем нет никаких методов.
Нужно заметить также, что методы, которые класс определяет, не будут методами для него (как для объекта), а будут просто атрибутами типа function
. Действительно, при вызове Dog.say_gaw(my_dog)
никакой дополнительный self
уже не передается, функция запускается в том виде, в котором мы ее написали. Это также можно показать следующим образом:
>>> class A:
... def f(self):
... print('hello')
...
>>>
>>> type(A.f)
<class 'function'>
>>> a = A()
>>> type(a.f)
<class 'method'>
То есть, A.f
- это функция, а a.f
- метод. Метод здесь это объект, который содержит в себе ссылку на объект, за которым этот метод закреплен (в нашем случае это объект a
) и ссылку на функцию, которую надо вызывать. Соответственно при вызове метод зовет эту функцию, передавая ссылку на свой объект как первый аргумент и прокидывая остальные аргументы.
>>> m = a.f
>>> m is A.f
False
>>> m.__func__ is A.f
True
>>> m.__self__ is a
True
>>> m.__func__(m.__self__)
hello
>>> m()
hello
>>> a.f()
hello
>>> A.f(a)
hello
Стандартные методы
Кроме __init__
есть и другие стандартные методы, которые можно определить в описании класса.
Метод __str__ возвращает строку, являющуюся описанием объекта в том виде, в котором его удобно будет воспринимать человеку. Здесь не нужно выводить имя конструктора, можно, например, просто вернуть строку с содержимым всех полей:
class Dog
def __str__(self):
return self.name + ' ' + str(self.score)
Метод __str__
будет вызываться, когда вызывается функция str
от данного объекта, например, str(Vasya)
. То есть создавая метод __str__
вы даете указание Питону, как преобразовывать данный объект к типу str
.
Поскольку функция print
использует именно функцию str
для вывода объекта на экран, то определение метода __str__
позволит выводить объекты на экран удобным способом: при помощи print
.
Переопределение стандартных операций
Реализуем класс Vector, используемый для представления радиус-векторов на координатной плоскости, и определим в нем поля-координаты: x и y. Также очень хотелось бы определить для векторов операцию +, чтобы их можно было складывать столь же удобно, как и числа или строки. Например, чтобы можно было записать так:
a = Vector(1, 2)
b = Vector(3, 4)
c = a + b
Для этого необходимо перегрузить операцию +: определить функцию, которая будет использоваться, если операция + будет вызвана для объекта класса Vector. Для этого нужно определить метод __add__ класса Vector, у которого два параметра: неявная ссылка self на экземпляр класса, для которого она будет вызвана (это левый операнд операции +) и явная ссылка other на правый операнд:
class Vector:
def __init__(self, x = 0, y = 0):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
Теперь при вызове оператора a + b Питон вызовет метод a.__add__(b), то есть вызовет указанный метод, где self = a, other = b.
Аналогично можно определить и оставшиеся операции. Полезной для переопределения является операция <. Она должна возвращать логическое значение True, если левый операнд меньше правого или False в противном случае (также в том случае, если объекты равны). Для переопределения этого операнда нужно определить метод __lt__ (less than):
class Vector:
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, при этом будет использоваться именно определенный оператор сравнения <.
Упражнения - класс Vector
Упражнение №1
Создайте класс Vector с полями x и y, определите для него конструктор, метод __str__, необходимые арифметические операции:
- сложение (__add__)
- вычитание (__sub__)
- умножение на число справа (__mul__) и слева (__rmul__)
- отрицание (унарный минус __neg__)
Упражнение №2
Используя класс Vector выведите координаты центра масс данного множества точек.
Упражнение №3
Используя информацию из предыдущей лабораторной, напишите добавление шарика по нажатию кнопки мыши. Все шарики должны отскакивать от стенок. Соударение шаров - не нужно.
Что понадобится для этого (подсказки):
- класс Ball, с методами update(self, dt) (для обновления координат), render(self, canvas) (для рисования, canvas - это наш объект screen из Pygame).
- кончно, все координаты и скорости представим объектами класса Vector
- в классе Vector нам пригодится дополнительный метод intpair(self), который вернет координаты округленные и в виде кортежа (т.е. в виде, в котором удобно скормить их Pygame-у)
Упражнение №4
При клике мышью по шарику меняем цвет этого шарика.
Упражнение №5
При клике мышью по шарику, этот шарик выбирается для ручного управления с клавиатуры (как в предыдущей лабораторной)
Упражнение №6
Включаем соударение шаров. Подсказка: изменение импульса шара при упругом столкновении с другим шаром выглядит так:
где:
\(p_1\) - импульс нашего шара
\(m_1\) - масса нашего шара
\(p_2\) - импульс второго шара
\(m_2\) - масса второго шара
\(n\) - единичный вектор нормали контакта (в нашем случае, вектор направленный от центра одного шара к центру другого), первая звездочка - скалярное умножение на \(n\), вторая - умножение вектора \(n\) на число.