Игра "Пушка"

Общая информация

При процедурном программировании программа разбивается на части в соответствии с алгоритмом: каждая функция является составной частью алгоритма. При объектно-ориентированном программировании программа строится как совокупность взаимодействующих объектов. Что же такое объект? С точки зрения объектно-ориентированного подхода, объект - это нечто, обладающее состоянием и поведением. По сути, состояние - это данные, соответствующие объекту, например рост, вес и громкость гавканья собаки. Поведением называется набор операций, которые можно производить с объектом. Например, в случае с собакой: покормить, погладить, погулять. Эти операции, которые можно выполнять над объектом обычно называют методами (по крайней мере в контексте языка 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__ есть и другие стандартные методы, которые можно определить в описании класса.

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

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

class Dog:
    def __repr__(self):
        return "Dog('" + self.angry + "', " + self.count + ")"

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

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

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

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

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

Упражнения - игра "Пушка"

Упражнение №1

Команда студентов начала разрабатывать игру "Пушка". Для тестирования использовался обфусцированный исходный файл, который позволяет увидеть процесс, но скрывает исходный код: obfuscated

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

Подсказка: для начала, исправте код так, чтобы он запускался, и поправте все 'FIXME'.

Упражнение №2

Улучшите программу из №1 добавив 2 цели.

Упражнение №3

Улучшите программу из №2 сделав цели движущимися.