Объектно-ориентированное программирование (в дальнейшем - ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов. Взаимодействие между операциями при помощи объектов.
Есть говорить человеческим языком, то в обычной жизни мы не оперируем понятиями строка или кортеж, мы оперируем объектами.
Мы говорим: "Подай чашку", "Пришли фотку", "Прикольный стол" и т. д. Так вот чашка, фотка и стол в этих примерах будут являться объектами. Объекты могут обладать некоторыми атрибутами (цвет, название, размер и т. д.) и методами (они же действия, например, на кнопку можно нажать, автомобиль может ехать, карандаш может писать и т. д.)
В центре ООП находится понятие объекта
.
Объекты создаются на основе классов
.
Класс - это шаблон для создания объекта. Допустим, у нас есть класс автомобиль, автомобиль - это класс, а конкретный Lexus 2015 года выпуска - это уже объект класса автомобиль.
Объект — это сущность, экземпляр класса, содержащий свои атрибуты и свои методы, созданный при помощи шаблона (т. е. класса).
Атрибут класса - это данные, принадлежащие классу, например, цвет автомобиля (каждый автомобиль какого-то цвета, но пока у нас нет "экземпляра," мы не можем сказать, какого цвета просто понятие автомобиль).
Метод класса - это функция, описанная внутри объекта, которая описывает определенное действие, например, кофемашина делает кофе.
Что такое self
? self
- это специальный аргумент в методах класса, который является ссылкой на экземпляр.
В большинстве случаев (когда нет, обсудим отдельно на следующих занятиях), первым аргументом любого метода будет self
.
Он обязательный. Чисто технически можно написать любое слово первым аргументом, и это тоже будет работать. Но не надо
так делать. Не сбивайте ни себя, ни других разработчиков. Первый аргумент большинства методов это self
.
self
- это конкретный объект внутри метода класса.
Если у нас есть класс студент, то через self
мы можем получить доступ ко всем атрибутам и методам конкретного
студента, например, списку оценок или методу "прогулять занятие".
Доступ к атрибутам и методам предоставляется через точку.
Еще про self
:
- у котов внутри есть мурчалка;
- она реализована для всех котов в классе Кот;
- в объекте кот надо как-то вызвать метод мурчало у класса Кот;
- как ты это сделаешь?
- Кот.мурчало()
- ежели ты вызовешь Кот.мурчало(), муркнут сразу все коты на свете;
- а ежели ты вызовешь self.мурчало(), муркнет только тот кот, на которого указывает self.
Наконец, к коду:
# Используем ключевое слово `class`
class Car:
# Опишем класс Машина, у которого будет два атрибута: цвет и максимальная скорость.
# Я указал для них типы данных, но для Python это не обязательно, скорее, удобный инструмент.
color: str = 'red'
top_speed: int = 250
# И несколько методов.
# Вернуть строку с максимальной скоростью и цветом
def find_color_and_top_speed(self):
return f'this car top speed is {self.top_speed} and color is {self.color}'
# Вернуть булево значение, которое отвечает на вопрос, может ли машина ехать с указанной скоростью
def is_car_can_go_with_needed_speed(self, speed: int):
return speed < self.top_speed
# Назначить максимальную скорость
def set_top_speed(self, speed: int):
self.top_speed = speed
# Назначить количество колес. Обратите внимания,
# такого атрибута изначально вообще не было
# (лучше так не делать, но технически нет никаких ограничений)
def set_count_of_wheels(self, wheels: int):
self.wheels = wheels
# Как создать объект? Для этого нужно просто "вызвать" класс
lamborghini = Car()
print(lamborghini.find_color_and_top_speed()) # This car's top speed is 250 and color is red
print(lamborghini.is_car_can_go_with_needed_speed(200)) # True
# Доступ к атрибутам можно получить напрямую
print(lamborghini.top_speed) # 250
lamborghini.set_top_speed(150) # Назначаем новую максимальную скорость для этого объекта
print(lamborghini.is_car_can_go_with_needed_speed(200)) # False
cherry = Car()
print(cherry.find_color_and_top_speed()) # This car's top speed is 250 and color is red
cherry.top_speed = 140
cherry.color = 'yellow'
print(cherry.wheels) # ВЫЗОВЕТ ОШИБКУ, у нас нет такого атрибута
cherry.set_count_of_wheels(4)
print(cherry.wheels) # Все ок, напечатает 4
cherry.size = 'small' # Можно добавлять атрибуты к любому объекту если это необходимо, у ламборгини такого атрибута не будет
В python вообще все является объектом
И строка, и число, и список и т. д.
ООП держится на трёх основных и одной второстепенной парадигме.
Наследование
— это свойство системы, позволяющее описать новый класс на основе уже существующего с частично или
полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым или
родительским. Новый класс — потомком, наследником или производным классом.
Например, у нас есть базовый класс автомобиль и три наследника, легковой, самосвал и фура. Все три класса могут иметь общие атрибуты, например, двигатель или материал лобового стекла, или методы, например, газ и тормоз, но при этом иметь свои особенные атрибуты или методы, например, только у фуры будет больше 4 колес, у самосвала будет метод поднять кузов, или у фуры - отцепить груз.
При описании ООП мне очень нравятся бытовые примеры на автомобилях. Представьте, что при разработке новой модели BMW, конструкторы решили бы начисто забыть о том, что у них уже были предыдущие модели. Тогда им пришлось бы абсолютно каждую новую модель разрабатывать с нуля. Возможно ли это? Конечно. Есть ли в этом необходимость? Очень сомнительно. Если можно взять прошлую модель, немного изменить дизайн, поменять систему тормозов и вставить новые фары, и вуаля, новая модель готова при минимальных затратах и максимальном результате. С наследованием точно также, можно описывать все классы с нуля, но часто это очень неудобно и затратно.
class Car:
wheels = 4
doors = 4
current_speed = 0
max_speed = 200
def go(self):
self.current_speed = self.max_speed / 2
def stop(self):
self.current_speed = 0
# тут класс унаследовал атрибуты wheels и current_speed, а также все методы
class Track(Car):
doors = 2
max_speed = 120
# а тут класс унаследовал все атрибуты кроме max_speed, а также все методы
class SportCar(Car):
max_speed = 350
track = Track()
sport = SportCar()
track.go()
sport.go()
print(track.curent_speed) # 60
print(sport.curent_speed) # 175
Практически всегда, когда нам нужно в дочернем классе выполнить такое же действие как и в родительском, нам необходимо основываться на данных из родительского (в конце блока вы узнаете, что это даже целое правило).
Но как вызвать код из другого класса? Нам поможет метод super()
.
Допустим, у нас есть класс, который занимается тем, что просто возвращает нам цену продукта. И еще два класса, которые вычисляют скидку 20%. И второй класс отнимает еще 20% уже от уменьшенной цены.
class PriceCounter:
price = 100
def calculate_price(self):
print('In PriceCounter calculate price')
return self.price
class DiscountCounter(PriceCounter):
def calculate_price(self):
print('In DiscountCounter calculate price: ')
return super().calculate_price() * 0.8
class SuperDiscountCounter(DiscountCounter):
def calculate_price(self):
print('In SuperDiscountCounter calculate price: ')
return super().calculate_price() * 0.8
price_counter = PriceCounter()
discount_counter = DiscountCounter()
super_discount_counter = SuperDiscountCounter()
print(price_counter.calculate_price())
"""
In PriceDiscounter calculate price:
100
"""
print(discount_counter.calculate_price())
"""
In DiscountCounter calculate price:
In PriceDiscounter calculate price:
80.0
"""
print(super_discount_counter.calculate_price())
"""
In SuperDiscountCounter calculate price:
In DiscountCounter calculate price:
In PriceDiscounter calculate price:
64.0
"""
Для Python существуют несколько различных версий, включая 2.х и 3.х.
Версии 2.х считаются устаревшими, но все-таки иногда можно встретить код на Python 2.x или "отнаследовавшийся от него".
class A(): # Вариант из python2, работать будет, но и удивлять коллег тоже
pass
class B(object):
"""
Вариант, который тоже будет работать
и на самом деле показывает нам суть любого класса и объекта в Python,
вообще все унаследовано от объекта.
"""
class C: # Традиционный способ для объявления класса в python3
pass
Заодно познакомились с ключевым словом pass
, которое нужно в качестве заглушки
, так как класс или функция не могут
быть пустыми, но могут быть с такой заглушкой. Вместо заглушки лучше все-таки ставить комментарий, если есть
необходимость создать пустой класс (а она вполне бывает).
У любого объекта всегда есть тип данных. И по факту этим типом данных всегда является класс (да, str
, int
, list
и т. д. и даже функции - это тоже классы)
Чтобы узнать тип данных любого объекта, необходимо вызвать метод type()
.
class A:
pass
a = A()
num = 10
text = 'test_str'
collection = [1, 2, 3]
def some_func():
pass
print(type(a)) # <class '__main__.A'>
print(type(num)) # <class 'int'>
print(type(text)) # <class 'str'>
print(type(collection)) # <class 'list'>
print(type(some_func)) # <class 'function'>
Для того чтобы сравнить и узнать, является ли объект подклассом, существует специальная функция isinstance()
. Она
принимает на вход объект и класс, либо кортеж из классов, а возвращает булево значение.
И есть такая же функция, которая принимает не объект, а сам класс: issubclass()
.
class A:
pass
class B(A):
pass
class C(B):
pass
a = A()
b = B()
c = C()
print(isinstance(a, A)) # True
print(isinstance(a, B)) # False
print(isinstance(b, A)) # True
print(isinstance(c, (A, C))) # True
print(issubclass(type(a), A)) # True
print(issubclass(B, A)) # True
print(issubclass(A, C)) # False
print(issubclass(type(a), (B, C))) # False
Абстракция в объектно-ориентированном программировании — это использование только тех характеристик объекта, которые с достаточной точностью представляют его в данной системе.
Часто говорят, что абстракция - это не обязательная парадигма ООП.
Вернемся к примерам с автомобилями. Когда мы управляем автомобилем, мы часто используем руль и поворотники, но часто ли мы меняем настройку зеркал или подогрева сидений? Не особо. Так вот абстракция о том, чтобы выделять главное и не тратить лишние ресурсы на второстепенное.
Если говорить простыми словами, то это возможность описать реализацию метода только в том классе, где это необходимо. А в родительском только описать название и, возможно, какие-то комментарии к будущей реализации.
class Animal:
def sound(self):
raise NotImplementedError # Вызвать ошибку, как это работает, рассмотрим через несколько занятий.
class Mouse(Animal):
def sound(self):
return 'pee pee'
class Lion(Animal):
def sound(self):
return 'roar'
Если в этом примере не описать метод sound()
, то технически все будет работать, но любая IDE будет подсвечивать, что
вы не описали абстрактный метод.
Полиморфизм — это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта, т. е. способность одной функции (метода), действовать по-разному в зависимости от обстоятельств (которые мы сами указываем).
По сути, это возможность использовать одни и те же методы или интерфейсы к различным структурам, например, обычный
знак +
, ведь мы можем сложить числа, а можем и строки, и получим разный результат, но применим один и тот же метод.
В примере с автомобилем, если мы умеем водить мерседес, то скорее всего у нас не вызовет проблема управлять тойотой или фордом. Ведь для управления мы будем использовать точно те же самые интерфейсы (руль, педали, поворотники и т. д.)
class English:
def greeting(self):
print("Hello")
class French:
def greeting(self):
print("Bonjour")
def intro(language):
language.greeting()
flora = English()
michelle = French()
intro(flora)
intro(michelle)
Прошлый пример из абстракции тоже подходит, ведь разные звери издают разные звуки. А для нас это не имеет значения, мы
всего лишь вызовем метод sound()
.
Инкапсуляция
- принцип "убирания с глаз" функционала или данных с предоставлением только входных и выходных
параметров. Например, если в автомобиле повернуть руль налево, то колёса тоже повернутся налево, но как именно это
происходит от нас скрыто, мы не знаем, какие именно рычаги и шестерёнки в этот момент двигаются внутри автомобиля.
Так же, например, с кофе машиной, мы знаем, что если нажать кнопку с капучино, то мы получим чашку кофе. Какие в этот момент процессы происходят внутри, нас практически не интересует. Что-то нагревается, где-то по трубкам бежит жидкость. Но нас интересует только результат. Могу ли я разобрать кофемашину и посмотреть, из чего она состоит? Могу, но у меня нет в этом необходимости.
Так же и в программировании, мы можем "скрыть" внутренние процессы.
В Python инкапсуляция очень условная (всегда можно получить доступ куда угодно, было бы желание). Как сказал создатель языка, Гвидо Ван Россум: "Мы все же взрослые люди, зачем мы будем кого-то ограничивать?"
В первую очередь код пишется для людей, поэтому и разделение существует на уровне понимания людей.
Существует три вида состояния атрибутов и свойств, и для их разделения используется специальный синтаксис.
-
Атрибуты и методы, чьи названия начинаются с букв, называются
public
. И они доступны везде. В объекте, в классе, в наследовании. -
Атрибуты и методы, чьи названия начинаются с символа
_
. Они называютсяprotected
, и подразумевается, что мы будем их использовать исключительно в классе и в наследовании, но не будем использовать в объектах. -
Атрибуты и методы которые начинаются с
__
. Они называютсяprivate
и подразумевается, что мы будем их использовать исключительно внутри самого класса.
class Car:
color = 'red'
_top_speed = 250
__max_carrying = 1000
def find_color_and_top_speed(self):
return f'This car top speed is {self._top_speed} and color is {self.color}.'
def can_go_with_needed_speed(self, speed):
return speed < self._top_speed
def can_get_weight(self, weight):
return self.__max_carrying > weight
def change_max_carrying(self, new_carrying):
self.__max_carrying = new_carrying
def __private_method(self):
print('This is private method')
def _this_is_protected_method(self):
print('This is protected method')
def run_hidden_and_protected_methods(self):
self.__private_method()
self._this_is_protected_method()
car = Car()
car.color # всё нормально
car._top_speed # сработает, но мы же сами описали это свойство так, чтобы сообщить, что не надо так его использовать
car.__max_carrying # не сработает (будет ошибка, что этот атрибут не найден)
car._Car__max_carrying # Сработает, и это как раз описание того, что добраться можно куда угодно. Но сам синтаксис нам говорит, что мы что-то не то делаем
car.__max_carrying = 800 # не сработает
car.change_max_carrying(800) # сработает
сar.__hidden_method() # не сработает (будет ошибка, что этого метода не существует)
car._this_is_protected_method() # сработает, но опять же не надо этого делать
car.run_hidden_and_protected_methods() # сработает и вызовет защищенный и приватный методы
Все тонкости и детали можно познать только на практике! Так что задачи на практику/домашние:
-
Описываем телефон:
Класс телефон. У него должны быть:
- Поле для описания номера.
- Метод, чтобы задать номер телефона.
- Защищенное поле для счетчика входящих звонков.
- Метод, который вернет нам количество принятых звонков.
- Метод принять звонок, который добавляет к счетчику единицу.
Создайте три разных объекта телефона.
Поменяйте всем изначальный номер.
Примите по несколько звонков на каждом (разное количество)
Напишите функцию, которая принимает список из объектов телефонов, а возвращает общее количество принятых звонков со всех телефонов.
-
Опишите класс для шахматной фигуры.
Фигура должна содержать такие атрибуты:
- Цвет (белый или черный).
- Место на доске (тут есть варианты, или два отдельных поля, для описания координат или одно, но, например, кортеж из двух чисел). И такие методы как:
- Изменить цвет (ничего не принимает, только меняет цвет на противоположный).
- Изменить место на доске (принимает или две переменные, или один кортеж из двух элементов), не забудьте проверить, что мы не пытаемся поставить фигуру за пределы доски (оба значения от 0 до 7).
- Абстрактный метод проверки потенциального хода (детали ниже). На данном этапе фигуры могут стоять на одной и той же клетке, пока нам это не важно.
Опишите классы для пешки, коня, офицера, ладьи, ферзя и короля. Все, что в них нужно добавить - это один метод для проверки, возможно, ли за один ход поменять место фигуры на доске (все ходят по-разному, у пешек будет еще и разница от цвета). Метод принимает опять же или две цифры, или один кортеж. И опять же проверяем, не выходит ли значение за пределы доски (Так как нам необходим этом функционал дважды, я бы делал его как отдельный защищенный метод в родительском классе)
И функцию, которая принимает список фигур и потенциальную новую клетку, а возвращает список из фигур. Но только тех, которые могут за один ход добраться до этой клетки.
-
Везде описать типизации (в функциях, атрибутах и методах).
-
ЗАДАЧА СО ЗВЁЗДОЧКОЙ. Сохранять информацию о принятых звонках в базу данных при помощи кода. (Пока только сохранять, читать будем позже)
Для этого понадобится установить модуль
psycopg2
черезpip install
и разобраться, как им пользоваться.