13Введение в объектно-ориентированное программирование. Инкапсуляция
13.1 Введение
В парадигме объектно-ориентированного программирования основной структурно-функциональной единицей служит объект - абстракция (модель), упрощенное представление окружающих нас предметов и/или явлений. Каждый объект обладает некоторыми свойствами и некоторым поведением. Причем программисты ни в коем случае не стремятся построить полную модель некоторого объекта, а берут только те свойства и то поведение, которое необъодимо для решения поставленной задачи. Свойства в программировании выражаются переменными, поведение - функциями.
Объекты бывают разных типов и перед их использованием необходимо их создать. Для этого нужно создать “чертёж”, шаблон, описывающий свойства и поведение объекта. Такие шаблоны называются классами.
# определение самого простого пустого класса. Определение начинается с ключевого слова class. Затем идет имя класса и двоеточие# заглушка pass позволяет нам избежать синтаксической ошибки из-за незаконченности# Каждый класс уже обладает набором стандартных методов (например, приведение класса к строке)class Cat:pass# Так мы создаем конкретный объект (экземпляр класса). ИмяКласса() - специальная функция, создающаяя объект - конструктор.cat = Cat()print(cat)
<__main__.Cat object at 0x70e4d2ebd810>
Объектно-ориентированное программирование (ООП) базируется на 3 основопоагающих принципах:
Инкапсуляция - управление доступом к свойствам и поведению объекта
Наследование - передача свойств и поведения от класса-родителя к классу-наследнику.
Полиморфизм - возможность создавать различные реализации одной абстрактной сущности.
Полиморфизм (множество форм) реализуется с помощью наследования, когда мы можем создать иерархию классов: от самого абстрактного к конкретной реализации.
Схема
Все классы в языке уже являются наследниками класса object. Каждый класс будет наследовать черты своего родителя, но обладать отличной от родителя реализацией.
Сегодняшний разговор будет об инкапсуляции, принципе который позволяет создавать безопасные интерфейсы взаимодействия с объектами за счет управления областью видимости дял окружения свойств и повдеения.
class Cat:""" Дадим некоторой конкретики нашим котикам. Дадим им кличку, цвет шерсти и простое поведение (интерфейс взаимодействия с объектом): они должны говорить нам, как их зовут и какой у них цвет шерсти. """def__init__(self, cat_name, fur_color):# Сигнатура функции""" Одна из реализациий полиморфизма - переопределение (задание новой реализации) функций. Здесь мы переопределяем конструктор, стандартную функцию унаследованную от object. Имена таких функций начинаются и заканчиваются с двух нижних подчеркиваний. Первым аргументом в сигнатуре функций внутри классов ВСЕГДА идёт self. С помощью self мы получаем доступ к содержимому самого класса. Помним о нотации object_name.wanted_property - обращение к свойству некоторого объекта. object_name.wanted_fun() - вызов функции некоторого объекта self.wanted_property - обращение к свойству класса внутри класса self.wanted_function() - вызов функции класса внутри класса В конструкторе мы инициализируем (задаём) свойства объекта. Их можно задавать в любом месте класса, здесь только те, которые нужны при создании Конструкторов может быть несколько в классе. """self.name = cat_name # Читаем как свойство самого класса равно параметру cat_nameself.fur_color = fur_color# Поведение выражается через функции. Поэтому здесь реалзиуем функции, которые должен уметь делать котик ПО ЗАДАНИЮ!# Это обычные функции, только внутри класса. Не забываем про self def who(self):print(f"My name is {self.name}.")def looking(self):returnf"I have {self.fur_color} fur!"
# Создаем экземпляр котика путем вызова конструктора с конкретными значениями аргументов и просим котика представиться.kitty_example = Cat("Murzik", "orange")kitty_example.who()
My name is Murzik.
# Какая же шерсть у котика?murzik_fur_color = kitty_example.looking()print(murzik_fur_color)
I have orange fur!
13.2 Задание
Реализуйте по аналогии класс Собака. Свойства: кличка и порода. Поведение: представиться (назвать кличку и породу), гавкать.
13.2.1 Ответ
class Dog:def__init__(self, name, breed):self.name = nameself.breed = breeddef who(self):print(f"My name is {self.name}. My breed is {self.breed}.")def bark(self):print("Hoof Hoof!")doggy = Dog("Bobby","redneck")doggy.who()doggy.bark()
My name is Bobby. My breed is redneck.
Hoof Hoof!
13.3 Управление доступом
Существуют три уровня доступа:
открытый (public) - переменная или функция доступна всем
защищенный (protected) - переменная или функция доступна только самому классу и его наследникам.
скрытый (private) - переменная или функция доступна только самому классу
Открытый уровень доступа означает, что ничего не мешает нам в любом месте обратиться к переменной или функции объекта и/или изменить его состояние. Это небезопасно, поэтому рекомендуется разделять переменный и функции по области видимости, оставляя открытой как можно меньшую часть программы.
# Мы так можем сделать в случае публичного доступаdoggy.name
'Bobby'
В Python все переменные и функции класса являются изначально открытыми. Разделение уровней доступа происходит на уровне соглашений имён.
Имя скрытой переменной/функции начинается с двух нижних подчеркиваний
Имя защищённой переменной/функции начинается с одного нижнего подчеркивания
class Penguin:def__init__(self, name, color):self.__name = name # Приватное поле nameself.__color = colordef __get_info(self):#Приватная функцияprint(f"penguin {self.__name}{self.__color}")
p = Penguin("Gerda", "black")p.__name
---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
Cell In[8], line 2 1 p = Penguin("Gerda", "black")
----> 2p.__nameAttributeError: 'Penguin' object has no attribute '__name'
p.__get_info()
---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
Cell In[9], line 1----> 1p.__get_info()
AttributeError: 'Penguin' object has no attribute '__get_info'
Как можно заметить, обратиться к скрытым полям и вызвать скрытую функцию снаружи класса напрямую нельзя. Способ есть, но в педагогических целях не показывается, потому что так делать плохо!
Программисты могут управлять доступом к скрытым или защищенным полям с помощью определения специальных функций, которые на жаргоне называют геттеры (для чтения) и сеттеры (для изменения). Наличие этих функций определяет права на эту переменную снаружи класса для пользователя. Их можно определять как обычные функции, а можно оборачивать их в специальные декораторы. Будет продемонстрировано оба подхода.
Декораторы будут разобраны на дальнейших занятиях. Пока следует знать, что это приспособления для расширения текущих возможностей без изменения уже имеющихся.
class Penguin:# реализация через обычные геттеры/сеттерыdef__init__(self, name, color):self.__name = name # Приватное поле nameself.__color = color# Геттер. Просто возвращает значение скрытого атрибута. Таккой геттер ещё называют тривиальнымdef get_name(self):returnself.__name# Сеттер. Просто изменяет значение скрытого атрибута.def set_name(self, new_name):self.__name = new_namep = Penguin("Helga","black")print(f"My first name is {p.get_name()}")p.set_name("Olga")print(f"Now my name is {p.get_name()}")
My first name is Helga
Now my name is Olga
class Penguin:# Реализация через декораторы# такая реализация наиболее принята в Pythondef__init__(self, name):self.__name = name@property# Так функция оборачивается в декоратор. @имя_декоратора над сигнатурой функцииdef name(self):returnself.__name# Здесь важно обратить фнимание на имя декоратора. @property_name.setter# Вместо property_name имя соответствующей функции с декоратором property@name.setterdef name(self, new_name):self.__name = new_namep = Penguin("Helga")print(f"My first name is {p.name}")p.name ="Olga"print(f"Now my name is {p.name}")
My first name is Helga
Now my name is Olga
По сути мы заменили вызов функции на обращение к переменной, что является более быстрой операцией
type(p.name)
str
Свойство может возвращать не только значение, но измененное по какой-то логике. Если отсутствует сеттер, мы не сможем его изменить. Взгляните внимательно на пример с классом Круг. Для его создание необходим радиус, но мы можем также определить свойство диаметра, которое пользователь не сможет изменить, что логично, т.к. он зависит от радиуса.
class Circle:def__init__(self, radius):self.__radius = radius@propertydef diametr(self):return2*self.__radius
c = Circle(5)c.diametr
10
Попытка изменить диаметр не увенчается успехом, что является признаком безопасности: если радиус и диаметр будут зависеть друг от друга как-то иначе, это повлияет на логику программы непредсказуемым для программиста способом.
c.diametr =13
---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
Cell In[15], line 1----> 1c.diametr=13AttributeError: can't set attribute 'diametr'
13.4 Подведение итогов
Python является объектно-ориентированным языком, следовательно, каждая сущность в нем является объектом.
Объект - это некоторая модель для решения нужной нам задачи. Объект обладает свойствами и поведением.
Экземпляр объекта создается по некоторому плану, чертежу, называемым классом. В классе можно и нужно разделять доступ к свойствам и поведению для окружения в целях безопасности.
13.5 Задание
13.5.1 Задание 1
Tip
Выполняется в течение первого семинара.
Реализуйте класс треугольник. Стороны должны быть скрытыми переменными. Реализуйте свойства периметр и площадь (теорема Герона). Реализуйте в конструкторе проверку на треугольность (теорема о соотношении сторон треугольника), в случае нарушения выбрасывайте исключение, которое в дальнейшем будете обрабатывать. Реализуйте функцию внутри класса для расчета углов между сторонами треугольника. Напишите программу для демонстрации возможностей класса.
13.5.2 Задание 2
Tip
Выполняется дома. Использовать профильные библиотеки для работы с последовательностями запрещается!
Реализуйте два класса. Первый класс - Seq - предназначен для работы с последовательностями. Он хранит в себе последовательность, информацию о последовательности (содержимое заголовка fasta записи). Класс должен уметь красиво приводить себя к строковому типу, выдавать длину последовательности и определять алфавит последовательности (белковый или нуклеотидный). Второй класс - FastaReader - ответственен за чтение файла в формате fasta. Он должен уметь определять соответствие файла формату и читать файл по записям, т.е возвращать отдельные экземпляры класса Seq. Отдельный пункт: оптимизация для работы с большими файлами (генераторы!). Подготовьте демонстрационную программу и примеры для тестирования (fasta файлы можно скачать с NCBI или Uniprot). Программа должна быть задокументирована, оформлена в виде git репозитория. Документация кода должна быть собрана с помощью специальных пакетов в html файлы. Нарисуйте UML диаграмму отношения ваших классов с полной спецификацией. Оформите релиз на GitHub с инструкцией по установке и запуску.