Предыдущая статья — Введение в объектно-ориентированное программирование: класс Blob и модульность.
Добро пожаловать в очередную статью нашей серии про объектно-ориентированное программирование. В этой статье мы собираемся изучить концепцию наследования.
Наследование — это основная форма модульности, но на самом деле она также играет заметную роль в масштабировании и поддерживаемости программного обеспечения.
При создании классов и использовании объектно-ориентированного программирования мы обычно исходим из идеи, что то, что мы делаем, может использоваться в дальнейшем другими разработчиками либо нами самими. Именно этого принципа придерживаются программисты при создании продуктов, которыми будут пользоваться другие люди.
Представьте, что вы создаете инструмент для других людей. Допустим, это класс «молоток». У вашего молотка будут такие атрибуты как ручка, и, скорей всего, металлическая головка для забивания гвоздей. Это минимально жизнеспособный продукт (MVP), и его вполне можно отгружать в таком виде.
У вас конечно могут возникнуть и другие идеи. Например, можно было бы сделать молоток с двумя сторонами для забивания гвоздей разного размера, или, может быть, создать какую-нибудь комбинированную головку, где одна сторона металлическая, а другая — резиновая!
Тут у вас может возникнуть соблазн реализовать свои идеи, потому что они кажутся вам разумными. Но вы сами не хотите этого делать. И вы решаете позволить клиентам изменять класс молотка, если они захотят, например, резиновый молоток (что будет эквивалентом покупки вместо этого резинового молотка). То есть, вы хотите просто выпустить самую простую версию своего продукта и посмотреть, что думают клиенты.
Вполне возможно, что почти все ваши клиенты попросят, чтобы молоток имел одну сторону для забивания гвоздей, а другую — для их вытаскивания. Вы даже раньше не думали об этом, но теперь вы добавляете то, что имеет гораздо больше смысла на практике.
Наследование позволяет создавать суперпростые классы, а пользователи в дальнейшем могут их изменять. Если вы получаете достаточно запросов на что-то новое, вы можете подумать о добавлении этого, но сначала позвольте вашим клиентам поиграть с этим. Вернемся к нашему примеру с классом Blob
и рассмотрим наследование от него:
class BlueBlob(Blob): pass
В этом классе ничего не происходит, кроме наследования от класса Blob
. Класс Blob
— это наш базовый или родительский класс. Его также можно назвать суперклассом. Мы не станем вам предлагать использовать все эти термины в других языках программирования, но в Python вы можете применять их все (вы заметите, что окружающие тоже так делают). Подобное многообразие терминов сложилось главным образом потому, что программисты, приходя в Python из других языков, приносят все эти термины с собой.
Новый класс BlueBlob
, унаследованный от Blob
, является нашим «дочерним» классом или подклассом. При наследовании от другого класса вы наследуете всё: все методы, включая и специальные.
Таким образом, в нашем случае мы наследуем все и ничего не меняем. Однако теперь мы можем добавлять наши собственные методы или даже переопределять уже существующие. Если, скажем, мы напишем новый метод __init__
, класс BlueBlob
будет считать его основным. Позже мы рассмотрим проблему, связанную с этим, и способы ее решения. А пока внесем следующие изменения:
def main(): blue_blobs = dict(enumerate([BlueBlob(BLUE,WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([BlueBlob(RED,WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)]))
Мы просто используем новый класс. Результат должен быть таким же, как и раньше:
Обычной задачей может быть добавление нашего собственного метода __init__
, но что произойдет, если мы попробуем это сделать?
class BlueBlob(Blob): def __init__(self): pass
Мы сразу получим ошибку:
TypeError: __init__() takes 1 positional argument but 4 were given
Ведь мы переопределили наш первоначальный метод __init__
! И что, нам теперь нужно вручную переписать весь код из этого метода? Ничего подобного, в нашем распоряжении есть метод super()
!
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): super().__init__(color, x_boundary, y_boundary) self.color = BLUE
Теперь у нас все кляксы этого класса будут синими.
Вызов метода super()
позволяет нам динамически обращаться к базовому классу. Мы также могли бы написать следующий код:
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): Blob.__init__(self, color, x_boundary, y_boundary) self.color = BLUE
Но у такой записи есть недостаток: мы быстро столкнемся с большим количеством проблем при множественном наследовании. Некоторое время назад метод super()
в Python считался довольно спорным, но Раймонд Хеттингер сделал отличный доклад, с которым вы обязательно должны ознакомиться, под названием Super considered super! Он достаточно длинный, но его изучение поможет вам понять метод super()
в Python. К тому же, Раймонд Хеттингер превосходный оратор.
Мы также можем добавлять новые методы, например следующим образом:
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): Blob.__init__(self, color, x_boundary, y_boundary) self.color = BLUE def move_fast(self): self.x += random.randrange(-5,5) self.y += random.randrange(-5,5)
Затем в методе draw_environment
мы можем заменить blob.move ()
на blob.move_fast ()
:
def draw_environment(blob_list): game_display.fill(WHITE) for blob_dict in blob_list: for blob_id in blob_dict: blob = blob_dict[blob_id] pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size) blob.move_fast() blob.check_bounds() pygame.display.update()