Урок 6. Принципы ООП. Классы, объекты, поля и методы. Уровни доступа.

Поговорим про основные принципы объектно-ориентированного программирования: абстракцию, инкапсуляцию, наследование и полиморфизм. Научимся создавать классы и объекты классов в Python. Рассмотрим, чем отличаются понятия поля, свойства, методы и атрибуты класса. Изучим особенности организации уровней доступа к атрибутам: Public, Protected и Private.

Logo Python Course Lesson 5

Урок 6.
Принципы ООП. Классы, объекты, поля и методы. Уровни доступа.

Поговорим про основные принципы объектно-ориентированного программирования: абстракцию, инкапсуляцию, наследование и полиморфизм. Научимся создавать классы и объекты классов в Python. Рассмотрим, чем отличаются понятия поля, свойства, методы и атрибуты класса. Изучим особенности организации уровней доступа к атрибутам: Public, Protected и Private.

ТЕОРЕТИЧЕСКИЙ БЛОК

В КОНЦЕ УРОКА ЕСТЬ ВИДЕО
One
1

Что такое ООП?

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

По сути любая программа представляет собой совокупность данных и операций по их обработке. Но что важнее, сами данные или операции над ними? В языках, в основе работы которых лежит принцип процедурного программирования (Basic, C, Pascal, Go), главным является код для обработки данных. При этом сами данные имеют второстепенное значение.
    Smartiqa Процедурное программирование. Работа с данными.
    Процедурное программирование. Работа с данными.
    В чем суть процедурного подхода? Процедурное программирование – это написание функций и их последовательный вызов в некоторой главной(main) функции.

    Для каких проектов подходит процедурное программирование? Идеальные условия для применения данного подхода - простые программы, где весь функционал можно реализовать несколькими десятками процедур/функций. Функции аккуратно вложены друг в друга и легко взаимодействуют посредством передачи данных из одной функции в другую.

    Однако проблема в том, что когда мы выходим за пределы этих идеальных условий, выплывают наружу многие недостатки данного подхода:

    1. В больших проектах приходится создавать огромное количество процедур и функций. В свою очередь, это неизбежно ведет к возникновению множества ошибок и снижает читаемость кода программы.
    2. Все данные внутри процедуры видны только локально, а значит их нельзя использовать в другом месте. Как следствие, код наполняется дубликатами.
    3. Высокий порог вхождения - по статистике начинающим данный поход дается сложнее, чем ООП.
    Долгое время процедурный подход был довольно популярным и считался самым прогрессивным. Однако объемы кода программ росли, и ему на смену пришло объектно-ориентированное программирование. Вот как определяет данный подход Википедия:
    Объектно-ориентированное программирование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования.
    В основе ООП лежит простая и элегантная идея, в соответствии с которой главное в программе - это данные. Именно они определяют, какие методы будут использоваться для их обработки. Т. е. данные первичны, код для обработки этих данных - вторичен.
      Smartiqa Объектно-ориентированное программирование. Работа с данными.
      Объектно-ориентированное программирование. Работа с данными.
      Итак, чем же хорош подход ООП?
      1. Программа разбивается на объекты. Каждый объект отвечает за собственные данные и их обработку. Как результат - код становится проще и читабельней.
      2. Уменьшается дупликация кода. Нужен новый объект, содержимое которого на 90% повторяет уже существующий? Давайте создадим новый класс и унаследуем эти 90% функционала от родительского класса!
      3. Упрощается и ускоряется процесс написания программ. Можно сначала создать высокоуровневую структуру классов и базовый функционал, а уже потом перейти к их подробной реализации.
      Подытожим
      1. В процедурном подходе основой программы является функция. Функции вызывают друг друга и при необходимости передают данные. В программе функции живут отдельно, данные — отдельно.
      2. Основной недостаток процедурного подхода - сложность создания и поддержки больших программ. Наличие сотен функций в таких проектах очень часто приводит к ошибкам и спагетти-коду.
      3. В основе объектно-ориентированного программирования лежит понятие объекта. Объект совмещает в себе и функции и данные.
      4. Основное преимущество ООП перед процедурным программированием - изоляция кода на уровне классов, что позволяет писать более простой и лаконичный код.
      Two
      2

      Объекты и классы в ООП

      Мир, в котором мы живем, состоит из объектов. Это деревья, солнце, горы, дома, машины, бытовая техника. Каждый из этих объектов имеет свой набор характеристик и предназначение. Несложно догадаться, что именно объектная картина реального мира легла в основу ООП. Разберем несколько ключевых понятий, основываясь на Википедии:
        Класс — в объектно-ориентированном программировании, представляет собой шаблон для создания объектов, обеспечивающий начальные значения состояний: инициализация полей-переменных и реализация поведения функций или методов.
        Объект — некоторая сущность в цифровом пространстве, обладающая определённым состоянием и поведением, имеющая определенные свойства (поля) и операции над ними (методы). Как правило, при рассмотрении объектов выделяется то, что объекты принадлежат одному или нескольким классам, которые определяют поведение (являются моделью) объекта. Термины «экземпляр класса» и «объект» взаимозаменяемы.
        На что необходимо обратить внимание?
        1. Класс описывает множество объектов, имеющих общую структуру и обладающих одинаковым поведением. Класс - это шаблон кода, по которому создаются объекты. Т. е. сам по себе класс ничего не делает, но с его помощью можно создать объект и уже его использовать в работе.
        2. Данные внутри класса делятся на свойства и методы. Свойства класса (они же поля) - это характеристики объекта класса.
        3. Методы класса - это функции, с помощью которых можно оперировать данными класса.
        4. Объект - это конкретный представитель класса.
        5. Объект класса и экземпляр класса - это одно и то же.
        Грубо говоря
        Класс = Свойства + Методы
        Рассмотрим простой пример. Перед нами класс "Автомобиль". Если мыслить абстрактно, то он представляет собой набор чертежей и инструкций, с помощью которых можно собрать машину. При этом каждая машина, которую мы будем собирать, должна обладать рядом характеристик, которые соответствуют нашему классу. Как мы уже выяснили, данные характеристики - называются свойствами класса и в нашем примере могут быть следующими:
        1. Цвет
        2. Объем двигателя
        3. Мощность
        4. Тип коробки передач
        Так же наш автомобиль может выполнять какие-то действия, характерные для всего класса. Эти действия, как мы теперь знаем, есть методы класса, и выглядеть они могут вот так:
        1. Ехать
        2. Остановиться
        3. Заправиться
        4. Поставить на сигнализацию
        5. Включить дворники
        Начинка класса готова, теперь можно переходить к созданию объектов.

        Что общего будет в объектах? Все объекты создаются по одному шаблону, то есть на выходе обязательно будут машины, никаких велосипедов и мотоциклов. Они будут выкрашены в какой-то цвет, ехать они будут за счет наличия в них двигателя, скорость будет регулироваться с помощью коробки передач. Также объекты данного класса будут обладать одинаковыми методами - все машины этого класса будут ездить, периодически им будет нужна заправка, а от угона они будут защищены установкой сигнализации.


        Smartiqa Объектно-ориентированное программирование. Класс Автомобиль.
        ООП. Класс Автомобиль.
        Но в чем разница? Значения свойств будут различаться. Одна машина красная, другая - зеленая. У одной объем двигателя 1968 см3 и коробка-робот, а у другой - 1395 см3 и ездить владельцу придется на механике.

        Вывод: Объекты класса на выходе похожие и одновременно разные. Различаются, как правило, свойства. Методы остаются одинаковыми.

        Пример созданного объекта "Автомобиль Volkswagen Tiguan":
        1. Свойства: Цвет="Белый", Объем двигателя="1984 см3", Мощность="180 л.с.", Тип коробки передач="Робот"
        2. Методы: Ехать, Остановиться, Заправиться, Поставить на сигнализацию, Включить дворники


        Three
        3

        Принципы ООП: абстракция, инкапсуляция, наследование, полиморфизм

        Как мы уже сказали, на текущий момент ООП является самой востребованной и распространенной парадигмой программирования. Концепция ООП строится на основе 4 принципов, которые мы предлагаем вам кратко рассмотреть.

        Принцип 1. Абстракция

        Дадим определение:
        Абстракция - принцип ООП, согласно которому объект характеризуется свойствами, которые отличают его от всех остальных объектов и при этом четко определяют его концептуальные границы.
        Т. е. абстракция позволяет:
        1. Выделить главные и наиболее значимые свойства предмета.
        2. Отбросить второстепенные характеристики.

        Когда мы имеем дело с составным объектом - мы прибегаем к абстракции. Например, мы должны понимать, что перед нами абстракция, если мы рассматриваем объект как "дом", а не совокупность кирпича, стекла и бетона. А если уже представить множество домов как "город", то мы снова приходим к абстракции, но уже на уровень выше.

        Зачем нужна абстракция? Если мыслить масштабно - то она позволяет бороться со сложностью реального мира. Мы отбрасываем все лишнее, чтобы оно нам не мешало, и концентрируемся только на важных чертах объекта.
        Smartiqa Объектно-ориентированное программирование. Абстракция.
        ООП. Абстракция.

        Принцип 2. Инкапсуляция

        Абстракция утверждает следующее: "Объект может быть рассмотрен с общей точки зрения". А инкапсуляция от себя добавляет: "И это единственная точка зрения, с которой вы вообще можете рассмотреть этот объект.". А если вы внимательно посмотрите на название, то увидите в нем слово "капсула". В этой самой "капсуле" спрятаны данные, которые мы хотим защитить от изменений извне.

        Дадим определение:
        Инкапсуляция - принцип ООП, согласно которому сложность реализации программного компонента должна быть спрятана за его интерфейсом.
        На что обратить внимание?
        1. Отсутствует доступ к внутреннему устройству программного компонента.
        2. Взаимодействие компонента с внешним миром осуществляется посредством интерфейса, который включает публичные методы и поля.
        А теперь опять пример с домом. Как в данном случае будет работать инкапсуляция? Она будет позволять нам смотреть на дом, но при этом не даст подойти слишком близко. Например, мы будем знать, что в доме есть дверь, что она коричневого цвета, что она открыта или закрыта. Но каким способом и из какого материала она сделана, инкапсуляция нам узнать не позволит.

        Для чего нужна инкапсуляция?
        1. Инкапсуляция упрощает процесс разработки, т. к. позволяет нам не вникать в тонкости реализации того или иного объекта.
        2. Повышается надежность программ за счет того, что при внесении изменений в один из компонентов, остальные части программы остаются неизменными.
        3. Становится более легким обмен компонентами между программами.
        Smartiqa Объектно-ориентированное программирование. Инкапсуляция.
        ООП. Инкапсуляция.

        Принцип 3. Наследование

        Наследование используется в случае, если одни объекты аналогичны другим за исключением нескольких различий. Дадим определение:
        Наследование - способ создания нового класса на основе уже существующего, при котором класс-потомок заимствует свойства и методы родительского класса и также добавляет собственные.
        На что обратить внимание?
        1. Класс-потомок = Свойства и методы родителя + Собственные свойства и методы.
        2. Класс-потомок автоматически наследует от родительского класса все поля и методы.
        3. Класс-потомок может дополняться новыми свойствами.
        4. Класс-потомок может дополняться новыми методами, а также заменять(переопределять) унаследованные методы. Переопределить родительский метод - это как? Это значит, внутри класса потомка есть метод, который совпадает по названию с методом родительского класса, но функционал у него новый - соответствующий потребностям класса-потомка.

        Для чего нужно наследование?
        1. Дает возможность использовать код повторно. Классы-потомки берут общий функционал у родительского класса.
        2. Способствует быстрой разработке нового ПО на основе уже существующих открытых классов.
        3. Наследование позволяет делать процесс написания кода более простым.

        Снова перед нами объект Дом. Дом можно построить, отремонтировать, заселить или снести. В нем есть фундамент, крыша, окна и двери. В виде списка это может выглядеть следующим образом:

        СВОЙСТВА

        1) Тип фундамента
        2) Материал крыши
        3) Количество окон
        4) Количество дверей
        МЕТОДЫ

        1) Построить
        2) Отремонтировать
        3) Заселить
        4) Снести

        А если мы захотим создать объект Частный дом? Данный объект по-прежнему будет являться домом, а значит будет обладать свойствами и методами класса Дом. Например, в нем так же есть окна и двери, и такой дом по-прежнему можно построить или отремонтировать. Однако при этом у него также будут собственные свойства и методы, ведь именно они отличают новый класс от его родителя. Новый класс Частный дом может выглядеть следующим образом:

        СВОЙСТВА

        1) Тип фундамента (УНАСЛЕДОВАНО)
        2) Материал крыши (УНАСЛЕДОВАНО)
        3) Количество окон (УНАСЛЕДОВАНО)
        4) Количество дверей (УНАСЛЕДОВАНО)
        5) Количество комнат
        6) Тип отопления
        7) Наличие огорода
        МЕТОДЫ


        1) Построить (УНАСЛЕДОВАНО)
        2) Отремонтировать (УНАСЛЕДОВАНО)
        3) Заселить (УНАСЛЕДОВАНО)
        4) Снести (УНАСЛЕДОВАНО)
        5) Изменить фасад
        6) Утеплить
        7) Сделать пристройку

        С такой же легкостью мы можем создать еще один класс-потомок - Многоэтажный дом. Его свойства и методы могут выглядеть так:

        СВОЙСТВА

        1) Тип фундамента (УНАСЛЕДОВАНО)
        2) Материал крыши (УНАСЛЕДОВАНО)
        3) Количество окон (УНАСЛЕДОВАНО)
        4) Количество дверей (УНАСЛЕДОВАНО)
        5) Количество квартир
        6) Количество подъездов
        7) Наличие коммерческой недвижимости
        МЕТОДЫ

        1) Построить (УНАСЛЕДОВАНО)
        2) Отремонтировать (УНАСЛЕДОВАНО)
        3) Заселить (УНАСЛЕДОВАНО)
        4) Снести (УНАСЛЕДОВАНО)
        5) Выбрать управляющую компанию
        6) Организовать собрание жильцов
        7) Нанять дворника

        Т. е. если подытожить, наследование позволяет нам использовать функционал уже существующих классов для создания новых. Просто представьте, что будет, если возможность наследования исчезнет. В этом случае пришлось бы копировать свойства и методы класса-родителя в класс-потомок. А если наследников 2? 5? 20? Придется копировать 20 раз. А что, если с течением времени вам потребуется внести изменение в код базового класса? Будете менять код во всех 20ти потомках? А что, если изменений тоже не одно, а например, 100? Думаю, что теперь вы убеждены, что наследование не зря является базовым принципом объектно-ориентированного программирования.
        Python. ООП Наследование

        Принцип 4. Полиморфизм

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

        Также для понимания работы этого принципа важным является понятие абстрактного метода:
        Абстрактный метод (он же виртуальный метод) - это метод класса, реализация для которого отсутствует.
        А теперь попробуем собрать все воедино. Для этого снова обратимся к классам Дом, Частный дом и Многоквартирный дом. Предположим, что на этапе написания кода мы еще знаем, какой из домов(частный или многоэтажный) нам предстоит создать, но вот то, что какой-то из них придется строить, мы знаем наверняка. В такой ситуации поступают следующим образом:
        1. В родительском классе(в нашем случае - класс Дом) создают пустой метод(например, метод Построить() ) и делают его абстрактным.
        2. В классах-потомках создают одноименные методы, но уже с соответствующей реализацией. И это логично, ведь например, процесс постройки Частного и Многоквартирного дома отличается кардинально. К примеру, для строительства Многоквартирного дома необходимо задействовать башенный кран, а Частный дом можно построить и собственными силами. При этом данный процесс все равно остается процессом строительства.
        3. В итоге получаем метод с одним и тем же именем, который встречается во всех классах. В родительском - имеем только интерфейс, реализация отсутствует. В классах-потомках - имеем и интерфейс и реализацию. Причем в отличие от родительского класса реализация в потомках уже становится обязательной.
        4. Теперь мы можем увидеть полиморфизм во всей его красе. Даже не зная, с объектом какого из классов-потомков мы работаем, нам достаточно просто вызвать метод Построить(). А уже в момент исполнения программы, когда класс объекта станет известен, будет вызвана необходимая реализация метода Построить().
        Как итог - за одинаковым названием могут скрываться методы с совершенно разным функционалом, который в каждом конкретном случае соответствует нуждам класса, к которому он относится.
        Smartiqa Полиморфизм
        Four
        4

        Классы и объекты в Python

        Теперь давайте посмотрим, как реализуется ООП в рамках языка программирования Python. Синтаксис для создания класса выглядит следующим образом:
        Синтаксис
        
        class <название_класса>:
            <тело_класса>
        
        А вот так компактно смотрится пример объявления класса с минимально возможным функционалом:
        Python
        
        class Car:
            pass
        
        Как мы видим, для задания класса используется инструкция class, далее следует имя класса Car и двоеточие. После них идет тело класса, которое в нашем случае представлено оператором pass. Данный оператор сам по себе ничего не делает - фактически это просто заглушка.

        Чтобы создать объект класса, нужно воспользоваться следующим синтаксисом:
        Синтаксис
        
        <имя_объекта> = <имя_класса>()
        
        И в качестве примера создадим объект класса Car:
        Python
        
        car_object = Car()
        
        Five
        5

        Атрибуты класса в Python

        Давайте договоримся, что атрибутом класса/объекта мы будем называть любой элемент класса/объекта (переменную, метод, подкласс), на который мы можем сослаться через символ точки. Т. е. вот так: MyClass.<атрибут> или my_object.<атрибут>.

        Все атрибуты можно разделить на 2 группы:
        1. Встроенные(служебные) атрибуты
        2. Пользовательские атрибуты
        Предлагаем отдельно поговорить о каждой из этих групп:
        Python. ООП Атрибуты класса

        1. Встроенные атрибуты

        Называть данную группу атрибутов встроенными - это своего рода условность, и сейчас мы объясним почему. Суть в том, что на самом деле все классы в Python (начиная с 3-й версии) имеют один общий родительский класс - object. Это значит, что когда вы создаете новый класс, вы неявно наследуете его от класса object, и потому свежесозданный класс наследует его атрибуты. Именно их мы и называем встроенными(служебными). Вот некоторые из них(заметьте, что в списке есть как поля, так и методы):
        Это важно
        В теории ООП конструктор класса - это специальный блок инструкций, который вызывается при создании объекта. При работе с питоном может возникнуть мнение, что метод __init__(self) - это и есть конструктор, но это не совсем так. На самом деле, при создании объекта в Python вызывается метод __new__(cls, *args, **kwargs) и именно он является конструктором класса.

        Также обратите внимание, что __new__() - это метод класса, поэтому его первый параметр cls - ссылка на текущий класс. В свою очередь, метод __init__() является так называемым инициализатором класса. Именно этот метод первый принимает созданный конструктором объект. Как вы уже, наверное, не раз замечали, метод __init__() часто переопределяется внутри класса самим программистом. Это позволяет со всем удобством задавать параметры будущего объекта при его создании.

        2. Пользовательские атрибуты

        Это атрибуты, которые непосредственно составляют основной функционал класса. Если служебные атрибуты наследуются от базового класса object, то пользовательские - пишутся программистом во время реализации начинки класса и дальнейшей работы с ним.

        Список атрибутов класса / объекта можно получить с помощью команды dir(). Если взять самый простой класс:
        Python
        
        class Phone:
            pass
        
        То мы получим вот такой список атрибутов:
        Python
        
        ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
        
        Как видим, в нем есть только встроенные атрибуты, которые наш класс по-умолчанию унаследовал от базового класса object. А теперь добавим ему функционала:
        Синтаксис
        
        class Phone:
        
            color = 'Grey'
        
            def turn_on(self):
                pass
        
            def call(self):
                pass
        
        
        И теперь посмотрим, как изменился список атрибутов класса:
        Синтаксис
        
        ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'color', 'call', 'turn_on']
        
        Несложно заметить, что в конце списка добавились три новых пользовательских атрибута: переменная color и методы turn_on() и call().
        Подытожим:
        1. Атрибутами называем совокупность полей и методов класса / объекта.
        2. Атрибуты делятся на встроенные и пользовательские.
        3. Все классы в Python имеют общий родительский класс - он называется object.
        4. Класс object предоставляет всем своим потомкам набор служебных атрибутов (как переменных (например, __dict__ и __doc__ ), так и методов (например, __str__ ) ).
        5. Многие из служебных атрибутов можно переопределить внутри своего класса.
        6. Поля и методы, которые описываются программистом в теле класса, являются пользовательскими и добавляются в общий список атрибутов наряду со встроенными атрибутами.
        Six
        6

        Поля (свойства) класса в Python

        Поля(они же свойства или переменные) можно (так же условно) разделить на две группы:
        1. Статические поля
        2. Динамические поля
        В чем же разница?
        Python. ООП Поля класса

        1. Статические поля (они же переменные или свойства класса)

        Это переменные, которые объявляются внутри тела класса и создаются тогда, когда создается класс. Создали класс - создалась переменная:
        Python
        
        class Phone:
        
            # Статические поля (переменные класса)
            default_color = 'Grey'
            default_model = ‘C385’
        
            def turn_on(self):
                pass
        
            def call(self):
                pass
        
        Вот так выглядит список атрибутов класса после его создания:
        Python
        
        ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'call', 'default_color', 'default_model', 'turn_on']
        
        Кроме того, у нас есть возможность получить или изменить такое свойство, просто обратившись к самому классу по его имени(экземпляр класса при этом создавать не нужно).
        Python - Интерактивный режим
        
        >>> Phone.default_color
        'Grey'
        
        # Изменяем цвет телефона по умолчанию с серого на черный
        >>> Phone.default_color = 'Black'
        >>> Phone.default_color
        'Black'
        

        2. Динамические поля (переменные или свойства экземпляра класса)

        Это переменные, которые создаются на уровне экземпляра класса. Нет экземпляра - нет его переменных. Для создания динамического свойства необходимо обратиться к self внутри метода:
        Python
        
        class Phone:
        
            # Статические поля (переменные класса)
            default_color = 'Grey'
            default_model = 'C385'
        
            def __init__(self, color, model):
                # Динамические поля (переменные объекта)
                self.color = color
                self.model = model
        
        Python - Интерактивный режим
        
        # Создадим экземпляр класса Phone - телефон красного цвета модели 'I495’
        >>> my_phone_red = Phone('Red', 'I495')
        
        # Полный список атрибутов созданного экземпляра:
        >>> dir(my_phone_red)
        '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'color', 'default_color', 'default_model', 'model']
        
        # Прочитаем статические поля объекта 
        >>> my_phone_red.default_color
        'Grey'
        >>> my_phone_red.default_model
        'C385'
        
        # Прочитаем динамические поля объекта
        >>> my_phone_red.color
        'Red'
        >>> my_phone_red.model
        'I495'
        
        # Создадим еще один экземпляр класса Phone - такой же телефон, но другого цвета
        >>> my_phone_blue = Phone('Blue', 'I495')
        
        # Прочитаем динамические поля объекта
        >>> my_phone_blue.color
        'Blue'
        >>> my_phone_blue.model
        'I495'
        
        Что такое self в Python?
        Служебное слово self - это ссылка на текущий экземпляр класса. Как правило, эта ссылка передается в качестве первого параметра метода Python:

        class Apple:

        ____# Создаем объект с общим количеством яблок 12
        ____def __init__(self):
        ________self.whole_amount = 12

        ____# Съедаем часть яблок для текущего объекта
        ____def eat(self, number):
        ________self.whole_amount -= number


        Стоит обратить внимание, что на самом деле слово self не является зарезервированным. Просто существует некоторое соглашение, по которому первый параметр метода именуется self и передает ссылку на текущий объект, для которого этот метода был вызван. Хотите назвать первый параметр метода по-другому - пожалуйста.

        В других языках программирования(например, Java или C++) аналогом этого ключа является служебное слово this.
        Обратите внимание, что объект класса сочетает в себе как статические атрибуты(уровня класса), так и динамические(собственные - уровня объекта).
        Подытожим:
        1. Для создания статической переменной достаточно объявления класса, причем данная переменная создается непосредственно в теле класса.
        2. Динамические переменные создаются только в рамках работы c экземпляром класса.
        3. Чтоб создать переменную экземпляра, необходимо воспользоваться конструкцией self.<имя_переменной> внутри одного из методов.
        4. Экземпляр класса сочетает в себе совокупность как статических (уровня класса), так и динамических (уровня самого экземпляра) полей.
        5. Значения динамических переменных для разных объектов класса могут (и чаще всего так и делают) различаться.
        Seven
        7

        Методы (функции) класса в Python

        Как вы уже знаете, функции внутри класса называются методами. Методы так же бывают разными, а именно - их можно разделить на 3 группы:
        1. Методы экземпляра класса (они же обычные методы)
        2. Статические методы
        3. Методы класса
        А в чем отличие между ними, давайте разбираться.
        Python. ООП Методы класса

        1. Методы экземпляра класса (Обычные методы)

        Это группа методов, которые становятся доступны только после создания экземпляра класса, то есть чтобы вызвать такой метод, надо обратиться к экземпляру. Как следствие - первым параметром такого метода является слово self. И как мы уже обсудили выше, с помощью данного параметра в метод передается ссылка на объект класса, для которого он был вызван. Теперь пример:
        Python
        
        class Phone:
        
            def __init__(self, color, model):
                self.color = color
                self.model = model
        
            # Обычный метод
            # Первый параметр метода - self
            def check_sim(self, mobile_operator):
                if self.model == 'I785' and mobile_operator == 'MTS':
                    print('Your mobile operator is MTS')
        
        Python - Интерактивный режим
        
        # Импортируем наш класс для работы с ним
        >>> from phone import Phone
        
        # Создаем экземпляр класса
        >>> my_phone = Phone('red', 'I785')
        
        # Обращаемся к методу check_sim() через объект my_phone
        >>> my_phone.check_sim('MTS')
        Your mobile operator is MTS
        
        Стоит заметить, что, как правило, данная группа методов является самой многочисленной и часто используемой в сравнении со статическими методами и методами класса.

        2. Статические методы

        Статические методы - это обычные функции, которые помещены в класс для удобства и тем самым располагаются в области видимости этого класса. Чаще всего это какой-то вспомогательный код.

        Важная особенность заключается в том, что данные методы можно вызывать посредством обращения к имени класса, создавать объект класса при этом не обязательно. Именно поэтому в таких методах не используется self - этому методу не важна информация об объектах класса.

        Чтобы создать статический метод в Python, необходимо воспользоваться специальным декоратором - @staticmethod. Выглядит это следующим образом:
        Python
        
        class Phone:
        
            # Статический метод справочного характера
            # Возвращает хэш по номеру модели
            # self внутри метода отсутствует
            @staticmethod
            def model_hash(model):
                if model == 'I785':
                    return 34565
                elif model == 'K498':
                    return 45567
                else: 
                    return None
        
            # Обычный метод
            def check_sim(self, mobile_operator):
                pass
        
        Python - Интерактивный режим
        
        >>> from phone import Phone
        
        # Вызываем статический метод model_hash, просто обращаясь к имени класса
        # Объект класса Phone при этом создавать не надо
        >>> Phone.model_hash('I785')
        34565
        

        3. Методы класса

        Методы класса являются чем-то средним между обычными методами (привязаны к объекту) и статическими методами (привязаны только к области видимости). Как легко догадаться из названия, такие методы тесно связаны с классом, в котором они определены.

        Обратите внимание, что такие методы могут менять состояние самого класса, что в свою очередь отражается на ВСЕХ экземплярах данного класса. Правда при этом менять конкретный объект класса они не могут (этим занимаются методы экземпляра класса).

        Чтобы создать метод класса, необходимо воспользоваться соответствующим декоратором - @classmethod. При этом в качестве первого параметра такого метода передается служебное слово cls, которое в отличие от self является ссылкой на сам класс (а не на объект). Рассмотрим пример:
        Python
        
        class Phone:
        
            def __init__(self, color, model, os):
                self.color = color
                self.model = model
                self.os = os
        
            # Метод класса
            # Принимает 1) ссылку на класс Phone и 2) цвет в качестве параметров
            # Создает специфический объект класса Phone(особенность объекта в том, что это игрушечный телефон)
            # При этом вызывается инициализатор класса Phone
            # которому в качестве аргументов мы передаем цвет и модель,
            # соответствующую созданию игрушечного телефона
            @classmethod
            def toy_phone(cls, color):
                toy_phone = cls(color, 'ToyPhone', None)
                return toy_phone
        
            # Статический метод
            @staticmethod
            def model_hash(model):
                pass
        
            # Обычный метод
            def check_sim(self, mobile_operator):
                pass
        
        Python - Интерактивный режим
        
        >>> from phone import Phone
        
        # Создаем объект игрушечный телефон
        # Обращаемся к методу класса toy_phone через имя класса и точку
        >>> my_toy_phone = Phone.toy_phone('Red')
        >>> my_toy_phone
        <phone.Phone object at 0x101a236d0>
        
        Как видно из примера, методы класса часто используются, когда
        1. Необходимо создать специфичный объект текущего класса
        2. Нужно реализовать фабричный паттерн - создаём объекты различных унаследованных классов прямо внутри метода
        eight
        8

        Уровни доступа атрибутов в Python

        Вам наверняка известно, что в классических языках программирования (таких как C++ и Java) доступ к ресурсам класса реализуется с помощью служебных слов public, private и protected:

        1. Private. Приватные члены класса недоступны извне - с ними можно работать только внутри класса.
        2. Public. Публичные методы наоборот - открыты для работы снаружи и, как правило, объявляются публичными сразу по-умолчанию.
        3. Protected. Доступ к защищенным ресурсам класса возможен только внутри этого класса и также внутри унаследованных от него классов (иными словами, внутри классов-потомков). Больше никто доступа к ним не имеет

        С точки зрения разграничения доступа к атрибутам класса Python является особенным языком - в нем отсутствует механизм, который мог бы запретить доступ к переменной или методу внутри класса. Вместо этого создатели Python предложили соглашение, в соответствии с которым:

        1. Если переменная/метод начинается с одного нижнего подчеркивания (_protected_example), то она/он считается защищенным (protected).
        2. Если переменная/метод начинается с двух нижних подчеркиваний (__private_example), то она/он считается приватным (private).

        Python. ООП Уровни доступа атрибутов
        Все члены класса в Python являются публичными по умолчанию. Любой член класса может быть доступен за пределами самого класса. Вот так выглядит создание и работа с публичными (public) методами в Python:
        Python
        
        class Phone:
        
            def __init__(self, color):
                # Объявляем публичное поле color
                self.color = color
        
        Python
        
        >>> from phone import Phone
        
        # Создаем экземпляр класса Phone
        >>> phone = Phone('Grey')
        
        # Обращаемся к свойству color
        >>> phone.color
        'Grey'
        
        # Изменяем свойство color
        >>> phone.color = 'Red'
        >>> phone.color
        'Red'
        
        Как видите, никаких проблем. Идем дальше. Как мы уже сказали, в соотвествии с соглашением чтобы сделать атрибут класса защищенным (protected), необходимо добавить к имени символ подчеркивания _ . Как, например, здесь:
        Python
        
        class Phone:
        
            def __init__(self, color):
                # Объявляем защищенное поле _color
                self._color = color
        
        Однако по факту в Python такой атрибут все равно будет доступен снаружи класса. Вы все еще можете выполнить операции, которые мы рассмотрели выше:
        Python
        
        # Создаем экземпляр класса Phone
        >>> phone = Phone('Grey')
        
        # Обращаемся к защищенному свойству _color
        >>> phone._color
        'Grey'
        
        # Изменяем защищенное свойство _color
        >>> phone._color = 'Red'
        >>> phone._color
        'Red'
        
        Иными словами, это больше вопрос ответственности программиста - он не должен работать с атрибутами, имена которых начинаются с нижнего подчёркивания _ , снаружи класса.

        Аналогично, два нижних подчеркивания __ в названии свойства/метода делают его приватным (private). Здесь уже интереснее - получить доступ к таким атрибутам напрямую нельзя (но если очень хочется, то все равно можно - об этом чуть ниже):
        Python
        
        class Phone:
        
            def __init__(self, color):
                # Объявляем приватное поле __color
                self.__color = color
        
        Python
        
        >>> from phone import Phone
        >>> phone = Phone('Grey')
        
        # Пытаемся обратиться к приватному свойству и получаем ошибку
        >>> phone.__color
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'Phone' object has no attribute '__color'
        
        Однако если мы заглянем в список атрибутов нашего класса, то увидим, что на самом деле наше свойство просто получило другое имя. И как вы уже наверное догадались, оно все еще доступно извне, но теперь уже по другому имени:
        Python
        
        # Получаем список атрибутов класса, в котором находим новое имя свойства: '_Phone__color'
        >>> dir(phone)
        ['_Phone__color', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
        
        # Обращаемся к защищенному свойству по новому имени
        >>> phone._Phone__color
        'Grey'
        
        # Меняем значение защищенного свойства
        >>> phone._Phone__color = 'Blue'
        >>> phone._Phone__color
        'Blue'
        
        Подытожим:
        1. Существует три уровня доступа к свойствам/методам класса: public, protected, private
        2. Физически данный механизм ограничения доступа к атрибутам класса в Python реализован слабо, что от части может противоречить одному из главных принципов ООП - инкапсуляции.
        3. Однако существует некоторое соглашение, по которому в Python задать уровень доступа к свойству/методу класса можно с помощью добавления к имени одного (protected) или двух (private) подчеркиваний. Ответственность за соблюдение данного соглашения ложится на плечи программистов.
        eight
        9

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

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

        Для того, чтобы в Python создать новый класс с помощью механизма наследования, необходимо воспользоваться следующим синтаксисом:
        Синтаксис
        
        class <имя_нового_класса>(<имя_родителя>):
        
        Теперь давайте рассмотрим пример применения механизма наследования в действии. Перед нами класс Phone (Телефон), у которого есть одно свойство is_on и три метода:

        1. Инициализатор: __init__()
        2. Включение: turn_on()
        3. Звонок: call()
        Python
        
        # Родительский класс
        class Phone:
        
            # Инициализатор
            def __init__(self):
                self.is_on = False
        
            # Включаем телефон
            def turn_on(self):
                self.is_on = True
        
            # Если телефон включен, делаем звонок
            def call(self):
                if self.is_on:
                    print('Making call...')
        
        В результате объект такого класса получит следующий набор атрибутов:
        Python
        
        >>> my_phone = Phone()
        >>> dir(my_phone)
        ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'call', 'is_on', 'turn_on']
        
        Среди данной совокупности атрибутов нас больше всего интересуют пользовательские свойства и методы: '__init__', 'call', 'is_on', 'turn_on'

        А теперь предположим, что мы захотели создать новый класс - MobilePhone (Мобильный телефон). Хоть этот класс и новый, но это по-прежнему телефон, а значить - его все так же можно включить и по нему можно позвонить. А раз так, то нам нет смысла реализовывать этот функционал заново, а можно просто унаследовать его от класса Phone. Выглядит это так:
        Python
        
        class Phone:
        
            def __init__(self):
                self.is_on = False
        
            def turn_on(self):
                self.is_on = True
        
            def call(self):
                if self.is_on:
                    print('Making call...')
        
        
        # Унаследованный класс
        class MobilePhone(Phone):
        
            # Добавляем новое свойство battery
            def __init__(self):
                super().__init__()
                self.battery = 0
        
        # Заряжаем телефон на величину переданного значения
        def charge(self, num):
            self.battery = num
            print(f'Charging battery up to ... {self.battery}%')
        
        
        Как вы видите, в новом классе добавились свойство battery и метод charge(). При этом мы помним, что это класс-потомок Phone, а значит от унаследовал и его функционал тоже. Создадим объект нового класса и посмотрим список его атрибутов:
        Python
        
        >>> my_mobile_phone = MobilePhone()
        >>> dir(my_mobile_phone)
        ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'battery', 'call', 'charge', 'is_on', 'turn_on']
        
        Теперь мы видим, что пользовательские атрибуты состоят из унаследованных ('is_on', 'call', 'turn_on') и новых ('__init__', 'battery', 'charge'). Все они теперь принадлежат классу MobilePhone. Пример использования:
        Python
        
        # Импортируем оба класса
        >>> from phone import Phone, MobilePhone
        
        # Создаем объект класса MobilePhone
        >>> my_mobile_phone = MobilePhone()
        
        # Включаем телефон и делаем звонок
        >>> my_mobile_phone.turn_on()
        >>> my_mobile_phone.call()
        Making call...
        
        # Заряжаем мобильный телефон
        >>> my_mobile_phone.charge(76)
        Charging battery up to ... 76%
        
        Что такое super?
        Как вы могли заметить, в инициализаторе (метод __init__) наследуемого класса вызывается метод super(). Что это за метод и зачем он нужен?

        Главная задача этого метода - дать возможность наследнику обратиться к родительскому классу. В классе родителе Phone есть свой инициализатор, и когда в потомке MobilePhone мы так же создаем инициализатор (а он нам действительно нужен, так как внутри него мы хотим объявить новое свойство) - мы его перегружаем. Иными словами, мы заменяем родительский метод __init__() собственным одноименным методом. Это чревато тем, что родительский метод просто в принципе не будет вызван, и мы потеряем его функционал в классе наследнике. В конкретном случае, потеряем свойство is_on.

        Чтобы такой потери не произошло, мы можем:
        1. Внутри инициализатора класса-наследника вызвать инициализатор родителя (для этого вызываем метод super().__init__())
        2. А затем просто добавить новый функционал
        Давайте еще раз взглянем на метод __init__() класса MobilePhone:

        def __init__(self):
        ____super().__init__()
        ____self.battery = 0


        Обратите внимание, что вызывать родительский метод необходимо в первую очередь.
        eight
        10

        Полиморфизм в Python

        Как вы уже знаете, полиморфизм позволяет перегружать одноименные методы родительского класса в классах-потомках. Что в свою очередь дает возможность использовать перегруженный метод в случаях, когда мы еще не знаем, для какого именно класса он будет вызван. Мы просто указываем имя метода, а объект класса, к которому он будет применен, определится по ходу выполнения программы. Чтобы стало более понятно, давайте рассмотрим пример:
        Python
        
        # Родительский класс
        class Phone:
        
            def __init__(self):
                self.is_on = False
        
            def turn_on(self):
                pass
        
            def call(self):
                pass
        
            # Метод, который выводит короткую сводку по классу Phone
            def info(self):
                print(f'Class name: {Phone.__name__}')
                print(f'If phone is ON: {self.is_on}')
        
        
        # Унаследованный класс
        class MobilePhone(Phone):
        
            def __init__(self):
                super().__init__()
                self.battery = 0
        
            # Такой же метод, который выводит короткую сводку по классу MobilePhone
            # Обратите внимание, что названия у методов совпадают - оба метода называются info()
            # Однако их содержимое различается
            def info(self):
                print(f'Class name: {MobilePhone.__name__}')
                print(f'If mobile phone is ON: {self.is_on}')
                print(f'Battery level: {self.battery}')
        
        
        # Демонстрационная функция
        
        # Создаем список из классов
        # В цикле перебираем список и для каждого элемента списка(а элемент - это класс)
        # Создаем объект и вызываем метод info()
        # Главная особенность: запись object.info() не дает информацию об объекте, для которого будет вызван метод info()
        # Это может быть объект класса Phone, а может - объект класса MobilePhone
        # И только в момент исполнения кода становится ясно, для какого именно объекта нужно вызывать метод info()
        def show_polymorphism():
            for item in [Phone, MobilePhone]:
                print('-------')
                object = item()
                object.info()
        
        Вызываем наш демонстрационный метод:
        Python
        
        >>> from phone import Phone, MobilePhone
        >>> from phone import show_polymorphism
        >>> show_polymorphism()
        -------
        Class name: Phone
        If phone is ON: False
        -------
        Class name: MobilePhone
        If mobile phone is ON: False
        Battery level: 0
        

        ПРАКТИЧЕСКИЙ БЛОК

        One
        1

        Задача "Покупка дома"

        Итак, мы с вами узнали, почему при разработке современных программ использование объектно-ориентированного подхода является обязательным условием. Также разобрались в понятиях Класс, Объект(Экземпляр), Атрибут, Свойство(Поле), Метод. Далее посмотрели, какими эти самые атрибуты, свойства и методы бывают. А еще научились отличать Protected атрибуты от Private и разобрались, как реализована модель уровней доступа к атрибутам непосредственно в Python. Теперь давайте постараемся эти знания применить на практике.

        Перед вами задача "Покупка дома". С помощью подхода ООП и средств Python в рамках данной задачи необходимо реализовать следующую предметную структуру:
        Классовая структура

        Есть Человек, характеристиками которого являются:
        1. Имя
        2. Возраст
        3. Наличие денег
        4. Наличие собственного жилья

        Человек может:
        1. Предоставить информацию о себе
        2. Заработать деньги
        3. Купить дом

        Также же есть Дом, к свойствам которого относятся:
        1. Площадь
        2. Стоимость
        Для Дома можно:
        1. Применить скидку на покупку

        Также есть Небольшой Типовой Дом, обязательной площадью 40м2.
        Задание. Часть 1. Класс Human

        1. Создайте класс Human.

        2. Определите для него два статических поля: default_name и default_age.

        3. Создайте метод __init__(), который помимо self принимает еще два параметра: name и age. Для этих параметров задайте значения по умолчанию, используя свойства default_name и default_age. В методе __init__() определите четыре свойства: Публичные - name и age. Приватные - money и house.

        4. Реализуйте справочный метод info(), который будет выводить поля name, age, house и money.

        5. Реализуйте справочный статический метод default_info(), который будет выводить статические поля default_name и default_age.

        6. Реализуйте приватный метод make_deal(), который будет отвечать за техническую реализацию покупки дома: уменьшать количество денег на счету и присваивать ссылку на только что купленный дом. В качестве аргументов данный метод принимает объект дома и его цену.

        7. Реализуйте метод earn_money(), увеличивающий значение свойства money.

        8. Реализуйте метод buy_house(), который будет проверять, что у человека достаточно денег для покупки, и совершать сделку. Если денег слишком мало - нужно вывести предупреждение в консоль. Параметры метода: ссылка на дом и размер скидки
        Задание. Часть 2. Класс House

        1. Создайте класс House

        2. Создайте метод __init__() и определите внутри него два динамических свойства: _area и _price. Свои начальные значения они получают из параметров метода __init__()

        3. Создайте метод final_price(), который принимает в качестве параметра размер скидки и возвращает цену с учетом данной скидки.
        Задание. Часть 3. Класс SmallHouse

        1. Создайте класс SmallHouse, унаследовав его функционал от класса House

        2. Внутри класса SmallHouse переопределите метод __init__() так, чтобы он создавал объект с площадью 40м2
        Задание. Часть 4. Тесты

        1. Вызовите справочный метод default_info() для класса Human

        2. Создайте объект класса Human

        3. Выведите справочную информацию о созданном объекте (вызовите метод info()).

        4. Создайте объект класса SmallHouse

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

        6. Поправьте финансовое положение объекта - вызовите метод earn_money()

        7. Снова попробуйте купить дом

        8. Посмотрите, как изменилось состояние объекта класса Human
        Наш вариант выполнения предложенного задания смотрите в видео-инструкции:
        Хронометраж
        00:00 План видео
        00:45 Что такое ООП?
        04:00 Классы и объекты
        06:40 Принципы ООП
        06:55 Принципы ООП. Абстракция.
        08:00 Принципы ООП. Инкапсуляция.
        08:30 Принципы ООП. Наследование.
        10:25 Принципы ООП. Полиморфизм.
        12:40 Атрибуты класса
        14:20 Конструктор и инициализатор. Метод __init__().
        16:00 Поля (свойства) класса.
        17:05 Служебное слово self
        18:30 Методы
        23:00 Уровни доступа: Public, Protected, Private.
        27:05 ПРАКТИКА
        27:10 Классовая структура
        28:40 Класс Human
        42:10 Класс House
        47:20 Класс SmallHouse
        50:35 Тесты
        Two
        2

        Домашнее задание

        1. Алфавит
        Классовая структура
        Есть Алфавит, характеристиками которого являются:
        1. Язык
        2. Список букв
        Для Алфавита можно:
        1. Напечатать все буквы алфавита
        2. Посчитать количество букв
        Так же есть Английский алфавит, который обладает следующими свойствами:
        1. Язык
        2. Список букв
        3. Количество букв
        Для Английского алфавита можно:
        1. Посчитать количество букв
        2. Определить, относится ли буква к английскому алфавиту
        3. Получить пример текста на английском языке
        1. Алфавит
        Задание
        Класс Alphabet
        1. Создайте класс Alphabet
        2. Создайте метод __init__(), внутри которого будут определены два динамических свойства: 1) lang - язык и 2) letters - список букв. Начальные значения свойств берутся из входных параметров метода.
        3. Создайте метод print(), который выведет в консоль буквы алфавита
        4. Создайте метод letters_num(), который вернет количество букв в алфавите
        Класс EngAlphabet
        1. Создайте класс EngAlphabet путем наследования от класса Alphabet
        2. Создайте метод __init__(), внутри которого будет вызываться родительский метод __init__(). В качестве параметров ему будут передаваться обозначение языка(например, 'En') и строка, состоящая из всех букв алфавита(можно воспользоваться свойством ascii_uppercase из модуля string).
        3. Добавьте приватное статическое свойство __letters_num, которое будет хранить количество букв в алфавите.
        4. Создайте метод is_en_letter(), который будет принимать букву в качестве параметра и определять, относится ли эта буква к английскому алфавиту.
        5. Переопределите метод letters_num() - пусть в текущем классе классе он будет возвращать значение свойства __letters_num.
        6. Создайте статический метод example(), который будет возвращать пример текста на английском языке.
        Тесты:
        1. Создайте объект класса EngAlphabet
        2. Напечатайте буквы алфавита для этого объекта
        3. Выведите количество букв в алфавите
        4. Проверьте, относится ли буква F к английскому алфавиту
        5. Проверьте, относится ли буква Щ к английскому алфавиту
        6. Выведите пример текста на английском языке
        2. Садовник и помидоры
        Классовая структура
        Предлагаем создать следующую классовую структуру:

        Есть Помидор со следующими характеристиками:
        1. Индекс
        2. Стадия зрелости(стадии: Отсутствует, Цветение, Зеленый, Красный)
        Помидор может:
        1. Расти (переходить на следующую стадию созревания)
        2. Предоставлять информацию о своей зрелости
        Есть Куст с помидорами, который:
        1. Содержит список томатов, которые на нем растут
        И может:
        1. Расти вместе с томатами
        2. Предоставлять информацию о зрелости всех томатов
        3. Предоставлять урожай
        И также есть Садовник, который имеет:
        1. Имя
        2. Растение, за которым он ухаживает
        И может:
        1. Ухаживать за растением
        2. Собирать с него урожай
        2. Садовник и помидоры
        Задание
        Класс Tomato:
        1. Создайте класс Tomato
        2. Создайте статическое свойство states, которое будет содержать все стадии созревания помидора
        3. Создайте метод __init__(), внутри которого будут определены два динамических protected свойства: 1) _index - передается параметром и 2) _state - принимает первое значение из словаря states
        4. Создайте метод grow(), который будет переводить томат на следующую стадию созревания
        5. Создайте метод is_ripe(), который будет проверять, что томат созрел (достиг последней стадии созревания)
        Класс TomatoBush
        1. Создайте класс TomatoBush
        2. Определите метод __init__(), который будет принимать в качестве параметра количество томатов и на его основе будет создавать список объектов класса Tomato. Данный список будет храниться внутри динамического свойства tomatoes.
        3. Создайте метод grow_all(), который будет переводить все объекты из списка томатов на следующий этап созревания
        4. Создайте метод all_are_ripe(), который будет возвращать True, если все томаты из списка стали спелыми
        5. Создайте метод give_away_all(), который будет чистить список томатов после сбора урожая
        Класс Gardener
        1. Создайте класс Gardener
        2. Создайте метод __init__(), внутри которого будут определены два динамических свойства: 1) name - передается параметром, является публичным и 2) _plant - принимает объект класса TomatoBush, является protected
        3. Создайте метод work(), который заставляет садовника работать, что позволяет растению становиться более зрелым
        4. Создайте метод harvest(), который проверяет, все ли плоды созрели. Если все - садовник собирает урожай. Если нет - метод печатает предупреждение.
        5. Создайте статический метод knowledge_base(), который выведет в консоль справку по садоводству.
        Тесты:
        1. Вызовите справку по садоводству
        2. Создайте объекты классов TomatoBush и Gardener
        3. Используя объект класса Gardener, поухаживайте за кустом с помидорами
        4. Попробуйте собрать урожай
        5. Если томаты еще не дозрели, продолжайте ухаживать за ними
        6. Соберите урожай
        Также предлагаем вам к рассмотрению и изучению наши варианты решений и соответствующие комментарии к каждому из заданий.
        Как вам материал?

        Читайте также