Skip to content

Latest commit

 

History

History
563 lines (389 loc) · 30.6 KB

lesson11.md

File metadata and controls

563 lines (389 loc) · 30.6 KB

Лекция 11. Введение в ООП. Основные парадигмы ООП. Классы и объекты.

Что такое ООП, и что же такое класс и объект.

Объектно-ориентированное программирование (в дальнейшем - ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов. Взаимодействие между операциями при помощи объектов.

Есть говорить человеческим языком, то в обычной жизни мы не оперируем понятиями строка или кортеж, мы оперируем объектами.

Мы говорим: "Подай чашку", "Пришли фотку", "Прикольный стол" и т. д. Так вот чашка, фотка и стол в этих примерах будут являться объектами. Объекты могут обладать некоторыми атрибутами (цвет, название, размер и т. д.) и методами (они же действия, например, на кнопку можно нажать, автомобиль может ехать, карандаш может писать и т. д.)

В центре ООП находится понятие объекта.

Объекты создаются на основе классов.

Класс - это шаблон для создания объекта. Допустим, у нас есть класс автомобиль, автомобиль - это класс, а конкретный Lexus, синего цвета и 2015 года выпуска - это уже объект класса автомобиль.

Объект — это сущность, экземпляр класса, содержащий свои атрибуты и свои методы, созданный при помощи шаблона (т. е. класса).

Атрибут класса - это данные, принадлежащие классу, например, цвет автомобиля (каждый автомобиль какого-то цвета, но пока у нас нет "экземпляра," мы не можем сказать, какого цвета просто понятие автомобиль).

Метод класса - это функция, описанная внутри объекта, которая описывает определенное действие, например, кофемашина делает кофе.

Ключевое слово self

Что такое 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) -> str:
      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) -> bool:
      return speed < self.top_speed

   # Назначить максимальную скорость
   def set_top_speed(self, speed: int) -> None:
      self.top_speed = speed

   # Назначить количество колес. Обратите внимания, 
   # такого атрибута изначально вообще не было 
   # (лучше так не делать, но технически нет никаких ограничений)
   def set_count_of_wheels(self, wheels: int) -> None:
      self.wheels = wheels


# Как создать объект? Для этого нужно просто "вызвать" класс

lamborghini = Car()
print(lamborghini.find_color_and_top_speed())  # This car's top speed is 250 and color is red
# Когда мы вызываем метод у объекта lamborghini, сам объект lamborghini передается в качестве аргумента self!!
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, сам объект cherry передается в качестве аргумента self!!
cherry.top_speed = 140
cherry.color = 'yellow'
print(cherry.wheels)  # ВЫЗОВЕТ ОШИБКУ, у нас нет такого атрибута
cherry.set_count_of_wheels(4)
print(cherry.wheels)  # Все ок, напечатает 4
cherry.size = 'small'  # Можно добавлять атрибуты к любому объекту если это необходимо, у ламборгини такого атрибута не будет, но делать так тоже обычно не стоит 

Объекты

В python вообще все является объектом

И строка, и число, и список и т. д.

И функция, и метод...

И None, и сами типы данных...

И вообще все к чему вы сможете дотянуться. Эта информация понадобится нам в дальнейшем.

Парадигмы ООП

ООП держится на трёх основных и одной второстепенной парадигме.

Наследование

Наследование — это свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым или родительским. Новый класс — потомком, наследником или производным классом.

Например, у нас есть базовый класс автомобиль и три наследника, легковой, самосвал и фура. Все три класса могут иметь общие атрибуты, например, двигатель или материал лобового стекла, или методы, например, газ и тормоз, но при этом иметь свои особенные атрибуты или методы, например, только у фуры будет больше 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()

Практически всегда, когда нам нужно в дочернем классе выполнить такое же действие, как и в родительском, нам необходимо основываться на данных из родительского (в конце блока вы узнаете, что это даже целое правило).

Но как вызвать код из другого класса? Нам поможет метод super().

Допустим, у нас есть класс, который занимается тем, что просто возвращает нам цену продукта. И еще два класса, которые вычисляют скидку 10%. И второй класс отнимает еще 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.9


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:
90.0
"""
print(super_discount_counter.calculate_price())
"""
In SuperDiscountCounter calculate price:
In DiscountCounter calculate price:
In PriceDiscounter calculate price:
72.0
"""

Устаревшие синтаксисы

Для Python существуют несколько различных версий, включая 2.х и 3.х.

Версии 2.х считаются устаревшими, но все-таки иногда можно встретить код на Python 2.x или "отнаследовавшийся от него".

class A():  # Вариант из python2, работать будет, но и удивлять коллег тоже
    pass


class B(object):
    """
    Вариант, который тоже будет работать 
    и на самом деле показывает нам суть любого класса и объекта в Python, 
    вообще все унаследовано от объекта.
    """


class C:  # Традиционный способ для объявления класса в python3
    pass

Заодно познакомились с ключевым словом pass, которое нужно в качестве заглушки, так как класс или функция не могут быть пустыми, но могут быть с такой заглушкой. Вместо заглушки лучше все-таки ставить комментарий, если есть необходимость создать пустой класс (а она вполне бывает).

Методы type(), isinstance() и issubclass()

У любого объекта всегда есть тип данных. И по факту этим типом данных всегда является класс (да, 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 будет подсвечивать, что вы не описали абстрактный метод.

Полиморфизм

Полиморфизм — это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта, т. е. способность одной функции (метода), действовать по-разному в зависимости от обстоятельств (которые мы сами указываем).

По сути, это возможность использовать одни и те же методы или интерфейсы к различным структурам, например, обычный знак +, ведь мы можем сложить числа, а можем и строки, и получим разный результат, но применим один и тот же метод.

"1" + "1" # "11"
1 + 1 #2
# А ведь это один и тот же плюс

В примере с автомобилем, если мы умеем водить мерседес, то скорее всего у нас не вызовет проблем управлять тойотой или фордом. Ведь для управления мы будем использовать точно те же самые интерфейсы (руль, педали, поворотники и т. д.)

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 инкапсуляция очень условная (всегда можно получить доступ куда угодно, было бы желание). Как сказал создатель языка, Гвидо Ван Россум: "Мы все же взрослые люди, зачем мы будем кого-то ограничивать?"

В первую очередь код пишется для людей, поэтому и разделение существует на уровне понимания людей.

Существует три вида состояния атрибутов и свойств, и для их разделения используется специальный синтаксис.

  1. Атрибуты и методы, чьи названия начинаются с букв, называются public. И они доступны везде. В объекте, в классе, в наследовании.

  2. Атрибуты и методы, чьи названия начинаются с символа _. Они называются protected, и подразумевается, что мы будем их использовать исключительно в классе и в наследовании, но не будем использовать в объектах.

  3. Атрибуты и методы которые начинаются с __. Они называются 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()

        
class Truck(Car):
    def _this_is_protected_method(self):
       super()._this_is_protected_method() # все хорошо
       
    def __private_method(self):
       super().__private_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()  # сработает и вызовет защищенный и приватный методы

Все тонкости и детали можно познать только на практике! Так что задачи на практику/домашние:

  1. Описываем телефон:

    Класс телефон. У него должны быть:

    • Поле для описания номера.
    • Метод, чтобы задать номер телефона.
    • Защищенное поле для счетчика входящих звонков.
    • Метод, который вернет нам количество принятых звонков.
    • Метод принять звонок, который добавляет к счетчику единицу.

    Создайте три разных объекта телефона.

    Поменяйте всем изначальный номер.

    Примите по несколько звонков на каждом (разное количество)

    Напишите функцию, которая принимает список из объектов телефонов, а возвращает общее количество принятых звонков со всех телефонов.

  2. Опишите класс для шахматной фигуры.

    Фигура должна содержать такие атрибуты:

    • Цвет (белый или черный).
    • Место на доске (тут есть варианты, или два отдельных поля, для описания координат или одно, но, например, кортеж из двух чисел). И такие методы как:
    • Изменить цвет (ничего не принимает, только меняет цвет на противоположный).
    • Изменить место на доске (принимает или две переменные, или один кортеж из двух элементов), не забудьте проверить, что мы не пытаемся поставить фигуру за пределы доски (оба значения от 0 до 7).
    • Абстрактный метод проверки потенциального хода (детали ниже). На данном этапе фигуры могут стоять на одной и той же клетке, пока нам это не важно.

    Опишите классы для пешки, коня, офицера, ладьи, ферзя и короля. Все, что в них нужно добавить - это один метод для проверки, возможно, ли за один ход поменять место фигуры на доске (все ходят по-разному, у пешек будет еще и разница от цвета). Метод принимает опять же или две цифры, или один кортеж. И опять же проверяем, не выходит ли значение за пределы доски (Так как нам необходим этом функционал дважды, я бы делал его как отдельный защищенный метод в родительском классе)

    И функцию, которая принимает список фигур и потенциальную новую клетку, а возвращает список из фигур. Но только тех, которые могут за один ход добраться до этой клетки.

  3. Создайте класс Student с такими полями:

    • Имя
    • Возраст
    • Список оценок

    И класс Group:

    • список Student
    • название

    Эти два класса нам понадобятся в рамках всего блока