ООП и диаграммы классов UML
Содержание
Кратко о том, что такое ООП
Объектно-ориентированное программирование - это методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.
Объектно-ориентированое программирование активно оперирует следующими понятиями:
Инкапсуляция
Что это и зачем?
Инкапсуляция - упаковка данных и функций в единый компонент. В общем случае, в разных языках программирования термин «инкапсуляция» относится к одному из или обоим определениям:
- механизм языка, позволяющий ограничить доступ одних компонентов программы к другим.
Например, ограничивается доступ к переменным объекта класса. В Python, чтобы создать в классе скрытую переменную, такую переменную, что к ней имеют доступ только методы самого класса, нужно перед именем переменной поставить
__
(два подчеркивания).Давайте рассмотрим пример:
# coding=UTF-8 class TestClass: def __init__(self): self.public_variable = "I'm public!" self.__private_variable = "I'm too shy to be public!" def get_public_variable(self): return self.public_variable def get_private_variable(self): return self.__private_variable if __name__ == "__main__": test_class = TestClass() print(" ".join(["Public variable:", test_class.get_public_variable()])) print(" ".join(["Public variable:", test_class.public_variable])) print(" ".join(["Private variable:", test_class.get_private_variable()])) print(" ".join(["Private variable:", test_class._private_variable]))Если вы запустите этот код, то вы получите следующее:
Public variable: I'm public! Private variable: I'm too shy to be public! Traceback (most recent call last): File "private_access.py", line 13, in <module> print(" ".join(["Private variable:", test_class._private_variable])) AttributeError: 'TestClass' object has no attribute '_private_variable'Удобство инкапсуляции в следующем:
- Безопасность: никто не может залезть внутрь класса и записать в переменные все что захочет, тем самым, сломав вашу программу;
- Удобство: рефакторинг (переписывании кода). Вы можете начать переписывать класс, переназвать переменные и вам не придется бегать по коду и менять везде
test_class.public_variable
наtest_class.new_public_variable
, вам нужно будет поменять всего одну функциюget_public_variable
.
- языковая конструкция, позволяющая связать данные с методами, предназначенными для обработки этих данных.
Эта концепция очень близка к предыдущей. Давайте посмотрим на два кода:
# coding=UTF-8 class PositiveInt: __a = 0 def set_a(self, a): if a >=0: self.__a = int(a) else: print("Wrong parameter, an internal state won't change." ) def get_a(self): return self.__a if __name__ == "__main__": value = PositiveInt() print(value.get_a()) value.set_a(10) print(value.get_a()) value.set_a(-10) print(value.get_a())
# coding=UTF-8 class PositiveInt: a = 0 if __name__ == "__main__": value = PositiveInt() print(value.a) value.a = 10 print(value.a) param = -10 if param > 10: value.a = param else: print("Wrong parameter, an internal stayte won't change." ) print(value.a)Собственно, оба этих кода делают одно и тоже.
Давайте представим, что пришел код-ревьюер, который проверял ваш код на чистоту/читаемость/верность стиля и сказал что нужно переименовать
__a
в__positive_integer
, потому что так по названию переменной понятней, зачем она нужна.То в случае кода 1 вы поменяете код в трех местах в классе
PositiveInt
и больше нигде. По сути, внутренности класса поменялись, но никто из тех, кто обращался к этому классу, этого не заметил.А в случае 2 помимо самого класса вам придется ходить по всему коду и везде менять имя переменной, что, согласитесь, не очень удобно. А еще это может вызвать кучу ошибок.
Класс
Что это?
Класс - это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (методы и уровни доступа к переменным класса).
Зачем это нужно?
- Для создания сложной структуры данных со сложным поведением;
- Для поддержки механизмов инкапсуляции, полиморфизма и наследования;
- Для удобства. Большая задача разбивается на много функциональных блоков меньшего размера, каждый из который реализуется классом.
Объект
Что это?
Объект - это конкретный экземпляр класса, поля которого проинициализированы.
Зачем это нужно?
См.Класс.
Наследование
Наследование - это метод расширения функциональности классов и снижения дубликации кода, когда один класс полностью забирает себе (наследует) все поля и методы другого класса (класса родителя) и добавляет новые поля и методы или переопределяет старые, тем самым расширяя/изменяя функциональность класса в сравнении с классом-родителем.
Определения
Рассмотрим простое наследование, пусть класс Derived --> Base. В Python 3 это осуществляется следующим кодом:
class Base:
pass
class Derived(Base):
pass
Класс Base
в данном случае является базовым классом, родительским классом, надклассом, суперклассом, предком.
Класс Derived
по отношению к нему является производным классом, дочерним классом, подклассом, потомком.
Говорят, что Derived
наследует, расширяет или специализирует Base
.
В языке Python 3 существует единый базовый класс object, который неявно является предком всех объектов вообще.
Класс Object определяет базовые методы всех классов, они могут быть переопределены у конкретного класса..
Зачем это нужно?
Давайте рассмотрим пример, когда это может понадобиться. Классы создаются для объединения кода и функций, его обрабатывающих. Однако, несколько классов часто оказываются настолько похожими, что код приходится дублировать.вфаеыучюсщь
class Student(UniversityMember):
group = None
passToUniversity = ''
status = True
def checkStatus(self):
return self.status
def dismiss(self):
self.status = False
self.pass_to_university = None
class Teacher(UniversityMember):
cathedral = None
passToUniversity = ''
status = True
def checkStatus(self):
return self.status
def dismiss(self):
self.status = False
self.pass_to_university = None
class Administrator(UniversityMember):
passToUniversity = ''
status = True
def checkStatus(self):
return self.status
def dismiss(self):
self.status = False
self.pass_to_university = None
В данном случае и у студента, и у преподавателя, и у администратора должны быть свойства status
и pass_to_university
, возможность проверки статуса и возможность увольнения.
Можно заметить, что в примере очень много дублирующегося кода. Это плохо. Если мы захотим что-то поменять, нам придется менять в трех местах как минимум. Если забудем что-то поменять, то это приведет к ошибке. В масштабах большого программного продукта это приведет к катастрофе.
Наследование классов
Заменим дублирование кода явным наследованием от абстактного класса (см.АДТ) UniversityMember
:
class UniversityMember:
passToUniversity = ''
status = True
def checkStatus(self):
return self.status
def dismiss(self):
self.status = False
self.pass_to_university = None
class Student(UniversityMember):
group = None
class Teacher(UniversityMember):
cathedral = None
class Administrator(UniversityMember):
pass
Диаграмма, которая отображает отношения между классами называется диаграммой классов, и на ней могут быть изображены также методы и атрибуты классов.
Язык объектно-ориентированного моделирования UML включает в себя не только диаграммы классов, но и множество других диаграмм, позволяющих лучше представить будущую программу.
За более подробной информацией можно обратиться к Wikipedia или пойти в гугл.
В нашем случае при помощи UML отношение классов можно представить следующим образом:
И более полная версия, включающая в себя поля и методы классов:
Перегрузка методов
Любой метод можно переопределить, то есть повторно реализовать в подклассе. В этом случае для экземпляров базового класса будет вызываться базовый метод, а для экземпляров производного -- перегруженный.
class Base:
def hello():
print("Hello! I'm base class!")
class Derived(Base):
def hello():
print("Hello! I'm derived class!")
b = Base()
d = Derived()
b.hello() # Hello! I'm base class!
d.hello() # Hello! I'm derived class!
Этот механизм называется динамическим связыванием методов или полиморфизмом.
В языке Python используется механизм грубого определения типа (утиная типизация):
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
Это значит, что если нам нужно вызвать некий метод объекта, то не важно, к какому классу относится этот объект, главное, чтобы он имел метод, который предполагается вызвать.
Подстановочный критерий Барбары Лисков
Правильно используйте наследование!
Механизм наследования используется для моделирования отношений типа "является".
В случае с классами Student
, Teacher
и Administrator
мы могли бы ошибочно сделать Administrator
предком Student
и Teacher
, поскольку это позволяет сэкономить код, да и вроде бы они только расширяют его функциональность...
class Administrator:
passToUniversity = ''
status = True
def checkStatus(self):
return self.status
def dismiss(self):
self.status = False
self.pass_to_university = None
class Student(Administrator):
group = None
class Teacher(Administrator):
cathedral = None
Однако нарушена логика: ни студент не является админстратором, ни преподаватель. При развитии проекта у администратора могут появиться некоторые новые атрибуты или методы, которые попадут в другие классы вследствие архитектурной ошибки.
Именно для того, чтобы избежать этой ошибочной логики, мы применили абстрактное мышление и придумали класс UniversityMember
.
Подстановочный критерий Барбары Лисков гласит также, что класс-потомок не только должен уметь делать всё то же, что и предок, но и не должен требовать для этого ничего нового.
Роберт С. Мартин определил этот принцип так:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Идея в том, чтобы выделять в отдельный класс все не специфические для объектов свойства, и наследоваться уже от этого универсального класса. Т.е. в базовый класс, от которого наследуются, могут добавляться только те поля и методы, которые нужны всем наследникам. В таком случае, если вы возьмете функцию, которая использует класс А, возьмете класс В, который унаследован он А и передадите в эту функцию, все будет работать.
Грубо говоря, если электрик чинил розетку за рубли, то его потомок должен, во-первых, уметь чинить розетку, во-вторых, уметь получить за это рубли (а не только доллары) и, в-третьих, не требовать для выполнения своей работы предварительных "танцев с бубном" (специфических предварительных инициализаций) или передачи дополнительных параметров в виде коробки конфет или бутылки водки.
Множественное наследование
При множественном наследовании у класса может быть более одного предка. В этом случае класс-потомок наследует методы всех предков.
class SuperBase: # Предок предка
def do(self):
print('Метод суперпредка!')
class Base1(SuperBase): # Предок 1
def do_it(self):
print('Метод предка 1')
class Base2: # Предок 2
def do_it(self):
print('Метод предка 2')
class Derived(Base1, Base2): # Наследник
def do_it_by_myself(self):
print('Метод наследника')
d = Derived() # инстанциация
d.do_it_by_myself() # Если в классе-потомке есть перегруженный метод с искомым названием
# то он будет вызван независимо от наличия таких же методов у предков.
d.do_it() # Если такого метода нет, то он ищется в порядке "лествичного права":
# в первую очередь у ближайших предков -- слева-направо,
# затем у их предков в том же порядке слева-направо, пока не будет найден.
# В данном случае будет вызван метод предка 1.
d.do() # Метод суперпредка вызывается, только если такого нет
# ни у класса, ни у его ближайших предков
Полиморфизм
Что это?
Полиморфизм - это способность объекта использовать методы производного класса, который не существует на момент создания базового.
Зачем это нужно?
Звучит сложно.
Предположим, что нам нужно три типа публикаций: новости, объявления и статьи. В чем-то они похожи — у всех них есть заголовок и текст, у новостей и объявлений есть дата. В чем-то они разные — у статей есть авторы, у новостей — источники, а у объявлений — дата, после которой оно становится не актуальным.
Самые простые варианты, которые приходят в голову — написать три отдельных класса и работать с ними. Или написать один класс, в которым будут все свойства, присущие всем трем типам публикаций, а задействоваться будут только нужные. Но ведь для разных типов аналогичные по логике методы должны работать по-разному. Делать несколько однотипных методов для разных типов (get_news, get_announcements, get_articles), как уже обсуждалось, не есть хорошо. Тут нам и поможет полиморфизм.
# coding=UTF-8
class Publication:
__title = ""
__text = ""
def __init__(self, title, text):
self.__title = title
self.__text = text
def get_title(self):
return self.__title
def get_text(self):
return self.__text
def set_title(self, title):
self.__title = title
def set_text(self, text):
self.__text = text
def __str__(self):
return self.get_str()
class News(Publication):
__publication_date = ""
__sources = []
def __init__(self, title, text, publication_date, sources):
super().__init__(title, text)
self.__publication_date = publication_date
self.set_sources(sources)
def set_publication_date(self, publication_date):
self.__publication_date = publication_date
def get_publication_date(self):
return self.__publication_date
def set_sources(self, sources):
if not isinstance(sources, list):
self.__sources = [sources]
else:
self.__sources = sources
def get_sources(self):
return self.__sources
def get_str(self):
return " ".join(["News:", self.get_title(), "\n",
"Text:", self.get_text(), "\n",
"Publication date:", self.get_publication_date(), "\n",
"Sources: ", " ".join(self.get_sources()), "\n",
"------------------------------------------------\n"])
class Announcement(Publication):
__out_date = ""
def __init__(self, title, text, out_date):
super().__init__(title, text)
self.__out_date = out_date
def get_out_date(self):
return self.__out_date
def set_out_date(self, out_date):
self.__out_date = out_date
def get_str(self):
return " ".join(["Announcement:", self.get_title(), "\n",
"Text:", self.get_text(), "\n",
"Out date:", self.get_out_date(), "\n",
"------------------------------------------------\n"])
class Article(Publication):
__authors = []
def __init__(self, title, text, authors):
super().__init__(title, text)
self.set_authors(authors)
def set_authors(self, authors):
if not isinstance(authors, list):
self.__authors = [authors]
else:
self.__authors = authors
def get_authors(self):
return self.__authors
def get_str(self):
return " ".join(["Article:", self.get_title(), "\n",
"Text:", self.get_text(), "\n",
"Authors:", ", ".join(self.get_authors()), "\n",
"------------------------------------------------\n"])
if __name__ == "__main__":
news = News("Braking news!", "That's a really exiting news!", "12 of November 2016", ["CNN", "BBC"])
announce = Announcement("New announcement!", "I want to by an elephant!", "15 of December 2016")
article = Article("We have new investigation", "Мы изобрели зелененький глазовыколупыватель", ["Профессор Бред",
"Ассистент Капитан Очевидность"])
strange_list = [news, announce, "Просто кусок непонятного бреда", article]
for element in strange_list:
if isinstance(element, Publication):
print(element)
Метод __str__(self)
есть у всех объектов в Python
и вызывается когда мы пишем в коде print(some_object)
, т.е. на самом деле, print(some_object)
интерпретатором Python
превращается в some_object.__str__()
.
У каждого объекта в Python
есть два очень похожих метода __repr__(self)
и __str__(self)
. Оба этих метода возвращают строку.
__str__(self)
возвращает строку, которая кратко в неформальном стиле описывает объект. То, что показывается пользователю, когда он делаетprint
.__repr__(self)
возвращает строку, которая полностью описывает объект. Как правило, по строке, которую возвращает__repr__
, можно понять тип объекта и получить всю информацию о его состоянии.
Пример:
# coding=UTF-8
>>> from decimal import Decimal
>>>
>>> a = Decimal(1.2)
>>> print(a) # В этом случае вызовется __str__(self)
1.2
>>> a # А в этом __repr__(self)
Decimal('1.2')
>>>
Вернемся к программе выше:
Собстенно, в Publication
есть метод __str__(self)
внутри которого вызывается get_str(self)
.
get_str(self)
в Publication
не реализован.
get_str(self)
реализован в потомках Publication
. Поэтому, когда мы делаем print(element)
, то __str__(self)
будет вызван из родителя Publication
, потому что в детях он не переопределен.
А вот get_str(self)
будет вызван уже из потомков, т.к. в Publication
он не реализован.
Т.е. Publication
использует метод, который будет определен только в потомке.
Потомок, тем самым, будет менять результат работы предка - это и есть полиморфизм, один вызов, разное поведение.
Упражнение №1: Расшарь соседу
Подозреваю, что после прочтения опуса выше вы чувствуете себя так:
Чтобы облегчить шта-симптомы, хорошей идеей после прочтения текста выше будет повернуться к соседу и попытаться ему объяснить каждую из основных концепций ООП.
Можно использовать бумажку и ручку. Главная задача запомнить названия и зачем каждая концепция нужна, привести примеры (которые были приведены, а еще лучше придумать свои). Если что-то не поняли, обсудите это с соседом.
У вас 10 минут, только не очень громко, другим мешать иначе будете :)
P.S. Если понимаете, что совсем не поняли, перечитайте дома, погуглите в интернете непонятные моменты.
Упражнение №2: Warm up
2.1. Треугольник и прямоугольник
Напишите программу, которая объявляет класс Shape
, конструктор которого принимает ширину и высоту.
После этого унаследуйте от него класс Triangle
и Rectangle
. Реализуйте метод area()
, который возвращает площадь этих фигур.
Продемонстрируйте работоспособность программы.
2.2 Дочки-матери
Напишите программу с классом Mother
от которого наследуется класс Daughter
.
Сделайте так, чтобы результат print(object) был разный.
Воспользуйтесь принципами полиморфизма, наследования и инкапсуляции.
2.3 Животные
Реализйте класс Animal. Внутри объявите поле для имени и возраста.
От класса Animal унаследуйте класс Zebra и Dolphin.
Оба класса могут вернуть описание, содержащее имя, возраст и какую-то доп.информацию, например, что это за вид животного.
Воспользуйтесь принципами полиморфизма, наследования и инкапсуляции.
Упражнение №3. Игра "Арифметические драконы"
Игра "Арифметические драконы" предназначена для обучения детей арифметике. На героя нападает дракон, который задаёт вопрос на сложение (если дракон зелёный), вычитание (красный) или умножение (чёрный).
- Разбейтесь по командам по два программиста и сядьте за один компьютер.
- Скачайте архив arithmetic_dragons
- Реализуйте следующие классы:
и добейтесь работоспособности игры.
Далее можете ввести новых атакующих юнитов:
- тролля, который задаёт вопрос "Угадай число от 1 до 5"
- тролля, который задаёт вопрос на простоту числа
- тролля, который просит разложить число на множители и перечислить их через запятую