Инструкция позволяющая создать описание объекта определенного типа python

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

Содержание

  • Преимущества и недостатки ООП Python
  • Класс
  • Объекты
  • Атрибуты класса
  • Атрибуты класса против атрибутов экземпляров
  • Методы
  • Статичные методы
  • Возврат множественных значений из метода
  • Метод str
  • Конструкторы
  • Локальные переменные против глобальных
  • Локальные переменные
  • Глобальная переменная
  • Модификаторы доступа
  • Наследование
  • Множественное наследование Python
  • Полиморфизм
  • Перегрузка метода
  • Переопределение метода
  • Инкапсуляция
  • Подведем итоги

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

Есть вопросы по Python?

На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

Telegram Чат & Канал

Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

Паблик VK

Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

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

  • мощность двигателя;
  • марка;
  • модель;
  • производитель, и т. д.

Соответственно, болид можно запустить, остановить, ускорить, и так далее. Гонщик может быть еще одним объектом в Формуле-1. Гонщик имеет национальность, возраст, пол, и так далее, кроме этого, он обладает таким функционалом, как управление болидом, рулевое управление, переключение передач.

Как и в этом примере, в объектно-ориентированном программировании мы создадим объекты, которые будут соответствовать реальным аспектам.

Стоит обратить внимание на то, что объектно-ориентированное программирование — не зависящая от языка программирования концепция. Это общая концепция программирования и большинство современных языков, такие как Java, C#, C++ и Python поддерживают объектно-ориентированное программирование.

В этой статье мы разберем подробную инструкцию объектно-ориентированного программирования в Python, но перед этим, рассмотрим некоторые преимущества и недостатки объектно-ориентированного программирования.

Рассмотрим несколько основных преимуществ объектно-ориентированного программирования:

  1. Объектно-ориентированное программирование подразумевает повторное использование. Компьютерная программа написанная в форме объектов и классов может быть использована снова в других проектах без повторения кода;
  2. Использование модулярного подхода в объектно-ориентированном программировании позволяет получить читаемый и гибкий код;
  3. В объектно-ориентированном программировании каждый класс имеет определенную задачу. Если ошибка возникнет в одной части кода, вы можете исправить ее локально, без необходимости вмешиваться в другие части кода;
  4. Инкапсуляция данных (которую мы рассмотрим дальше в статье) вносит дополнительный уровень безопасности в разрабатываемую программу с использованием объектно-ориентированного подхода;

Хотя объектно-ориентированное программирование обладает рядом преимуществ, оно также содержит определенные недостатки, некоторые из них находятся в списке ниже:

  1. Для создания объектов необходимо иметь подробное представление о разрабатываемом программном обеспечении;
  2. Не каждый аспект программного обеспечения является лучшим решением для реализации в качестве объекта. Для новичков может быть тяжело прочертить линию в золотой середине;
  3. С тем, как вы вносите все новые и новые классы в код, размер и сложность программы растет в геометрической прогрессии;

В следующем разделе мы рассмотрим ряд самых важных концепций объектно-ориентированного программирования.

Как и следует из названия, объектно-ориентированное программирование — это речь об объектах. Однако, перед тем как создать объект, нам нужно определить его класс.

Класс

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

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

Отношение между классом и объектом можно представить более наглядно, взглянув на отношение между машиной и Audi. Да, Audi – это машина. Однако, нет такой вещи, как просто машина. Машина — это абстрактная концепция, которую также реализуют в Toyota, Honda, Ferrari, и других компаниях.

Ключевое слово class используется для создания класса в Python. Название класса следует за ключом class, за которым следует двоеточие. Тело класса начинается с новой строки, с отступом на одну вкладку влево.

Давайте рассмотрим, как мы можем создать самый простой класс в Python. Взглянем на следующий код:

# Создаем класс Car

class Car:

    # создаем атрибуты класса

    name = «c200»

    make = «mercedez»

    model = 2008

    # создаем методы класса

    def start(self):

        print («Заводим двигатель»)

    def stop(self):

        print («Отключаем двигатель»)

В примере выше мы создали класс под названием Car с тремя атрибутами: имя name, марка make и модель model. Наш класс также содержит два метода: start() и stop().

Объекты

Раннее мы поняли, что класс предоставляет чертеж объекта. Однако, чтобы на самом деле использовать объекты и методы класса, вам нужно создать объект из этого класса. Существует несколько методов и атрибутов класса, которые можно использовать вне объекта, мы рассмотрим их в следующем разделе.

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

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

Давайте создадим объект класса Car, который мы создали в предыдущем разделе.

# Создаем объект класса Car под названием car_a

car_a = Car()

# Создаем объект класса Car под названием car_b

car_b = Car()

В этом скрипте мы создали два объекта класса Car: car_a и car_b. Чтобы узнать тип созданных нами объектов, мы можем использовать метод type и передать ему названия наших объектов. Выполните следующий код:

В выдаче вы увидите:

Это говорит нам о том, что тип объекта car_b – класс Car.

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

В этом скрипте мы вызываем метод start() через объект car_b. Выдача будет выглядеть следующим образом:

Аналогично, вы можете получить доступ к атрибуту, пользуясь следующим синтаксисом:

В выдаче вы увидите значение атрибута модели, как показано ниже:

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

В предыдущей секции мы разобрались, как создавать объекты класса и как мы можем использовать эти объекты для получения доступа к атрибутам класса

В Python, каждый объект содержит определенные атрибуты по умолчанию и методы в дополнение к определенным пользователем атрибутами. Чтобы посмотреть на все атрибуты и методы объекта, используйте встроенную функцию под названием dir(). Попробуем взглянуть на все атрибуты объекта car_b, который мы создали в предыдущем разделе. Выполните следующий скрипт:

В выдаче вы увидите следующие атрибуты:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

[‘__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__’,

‘make’,

‘model’,

‘name’,

‘start’,

‘stop’]

Эта встроенная функция очень полезна при изучении атрибутов и функций объекта, особенно при использовании через REPL.

Атрибуты класса против атрибутов экземпляров

Атрибуты могут быть наглядно отнесены к двум типам:

  • атрибуты класса
  • атрибуты экземпляров

Атрибуты класса делятся среди всех объектов класса, в то время как атрибуты экземпляров являются собственностью экземпляра.
Помните, что экземпляр — это просто альтернативное название объекта.

Атрибуты экземпляра объявляются внутри любого метода, в то время как атрибуты класса объявляются вне любого метода.

Следующий пример прояснит эту разницу:

class Car:

    # создаем атрибуты класса

    car_count = 0

    # создаем методы класса

    def start(self, name, make, model):

        print(«Двигатель заведен»)

        self.name = name

        self.make = make

        self.model = model

        Car.car_count += 1

В указанном выше скрипте мы создаем класс Car с одним атрибутом класса под названием car_count и три атрибута экземпляра под названием name, make и model. Класс содержит один метод start(), который содержит наши три атрибута экземпляров. Значения атрибутов экземпляров переданы в качестве аргументов методу start(). Внутри метода start, атрибут car_count увеличен на один.

Стоит упомянуть, что внутри метода, атрибуты экземпляра ссылаются при помощи ключевого слова self, в то время как атрибуты класса ссылаются при помощи названия класса.

Давайте создадим объект класса Car и вызовем метод start().

car_a = Car()  

car_a.start(«Corrola», «Toyota», 2015)  

print(car_a.name)  

print(car_a.car_count)

В скрипте выше мы вывели название атрибута экземпляра и атрибута класса car_count. В выдаче вы увидите, что атрибут car_count будет иметь значение 1, как показано ниже:

Двигатель заведен

Corrola  

1

Теперь создадим еще один объект класса Car и вызываем метод start().

car_b = Car()  

car_b.start(«City», «Honda», 2013)  

print(car_b.name)  

print(car_b.car_count)

Сейчас если вы выведите значение атрибута car_count, вы увидите 2 в выдаче. Это связано с тем, что атрибут car_count является атрибутом класса и таким образом он разделяется между экземплярами. Объект car_a увеличил свое значение до 1, в то время как car_b увеличил свое значение еще раз, так что итоговое значение равняется 2. Выдача выглядит следующим образом:

Методы

Как мы выяснили ранее, в объектно-ориентированном программировании, методы используются для реализации функционалов объекта. В предыдущем разделе мы создали методы start() и stop() для класса Car. До этих пор, мы использовали объекты класса для вызова методов. Однако, есть тип методов, который может быть вызван напрямую при помощи имени класса. Такой метод называется статичным методом.

Статичные методы

Для объявления статического метода, вам нужно указать дескриптор @staticmethod перед названием метода, как показано ниже:

class Car:

    @staticmethod

    def get_class_details():

        print («Это класс Car»)

Car.get_class_details()

В коде выше мы создали класс Car с одним статичным методом get_class_details(). Давайте вызовем этот метод, используя название класса.

Вы можете видеть что нам не нужно создавать экземпляр класса Car для вызова метода get_class_details(), вместо этого мы просто использовали название класса. Стоит упомянуть, что статические методы могут иметь доступ только к атрибутам класса в Python, вы не сможете обратиться к методам через self.

Возврат множественных значений из метода

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

class Square:

    @staticmethod

    def get_squares(a, b):

        return a*a, b*b

print(Square.get_squares(3, 5))

В скрипте выше мы создали класс под названием Square со статичным методом get_squares(). Метод принимает два параметра. Он умножает каждый параметр на себя и возвращает оба результата при помощи оператора return. В выдаче указанного выше скрипта вы увидите квадраты 3 и 5.

Метод str

До этого момента мы выводили атрибуты при помощи метода print(). Посмотрим, что случится, если мы выведем объект класса.

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

class Car:

    # создание методов класса

    def start(self):

        print («Двигатель заведен»)

car_a = Car()  

print(car_a)

В скрипте выше мы создали объект car_a класса Car и вывели его значение на экран. По сути мы относимся к объекту car_a как к строке. Выдача выглядит следующим образом:

<__main__.Car object at 0x000001CCCF4335C0>

Выдача показывает локацию памяти, где хранится наш объект. Каждый объект Python по умолчанию содержит метод __str__ . Когда вы используете объект в качестве строки, вызывается метод __str__ , который по умолчанию выводит локацию памяти объекта. Однако, вы также можете предоставить собственное определение метода __str__ . Например, как в следующем примере:

# создание класса Car

class Car:

    # создание методов класса

    def __str__(self):

        return «Car class Object»

    def start(self):

        print («Двигатель заведен»)

car_a = Car()  

print(car_a)

В скрипте выше, мы переопределили метод __str__ , предоставив наше собственное определение метода. Теперь, если вы выведите объект car_a, вы увидите сообщение «Car class Object» в консоли. Это сообщение, которое мы внесли в наш пользовательский метод __str__ .

Использование этого метода позволяет вам создавать пользовательские и более осмысленные описания, когда объект выводится. Вы можете даже отобразить кое-какие данные внутри класса, такие как название класса Car.

Конструкторы

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

Для создания конструктора вам нужно создать метод с ключевым словом __init__. Взгляните на следующий пример:

class Car:

    # создание атрибутов класса

    car_count = 0

    # создание методов класса

    def __init__(self):

        Car.car_count +=1

        print(Car.car_count)

В скрипте выше мы создали класс Car с одним атрибутом класса car_count. Класс содержит конструктор, который увеличивает значение car_count и выводит итоговое значение на экран.

Теперь, когда объект класса Car будет создан, конструктор также будет вызван, значение car_count увеличится и отобразится на экране. Создадим простой объект и посмотрим, что выйдет:

car_a = Car()  

car_b = Car()  

car_c = Car()

В выдаче вы увидите выведенное значение 1, 2 и 3, поскольку для каждого объекта значение переменной car_count увеличивается и отображается на экране.

За исключением названия, конструктор может использоваться как обычный метод. Вы можете передавать и получать значения из конструктора. Он обычно используется таким образом, когда вам нужно инициализировать значения атрибута при создании экземпляра класса.

Локальные переменные против глобальных

Мы знаем, что есть два типа атрибутов Python: атрибуты экземпляра и атрибуты класса.

Атрибуты класса также называются переменными. В зависимости от области видимости, переменные также могут относиться к двум типам: локальные переменные и глобальные переменные.

Локальные переменные

Локальная переменная в классе — это переменная, доступ к которой возможен только внутри блока кода, в котором она определена. Например, если вы определите переменную внутри метода, к нему не удастся получить доступ откуда-либо вне метода. Посмотрим на следующий скрипт:

# создаем класс Car

class Car:  

    def start(self):

        message = «Двигатель заведен»

        return message

В скрипте выше мы создали локальную переменную message внутри метода start() класса Car. Теперь создадим объект класса Car и попытаемся получить доступ к локальной переменной message, как показано ниже:

car_a = Car()  

print(car_a.message)

Скрипт выше приводит к следующей ошибке AttributeError:

AttributeError: ‘Car’ object has no attribute ‘message’

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

Глобальная переменная

Глобальная переменная определяется вне любого блока, то есть метода, операторов-if, и тому подобное. Доступ к глобальной переменной может быть получен где угодно в классе. Рассмотрим следующий пример.

# создаем класс Car

class Car:  

    message1 = «Двигатель заведен»

    def start(self):

        message2 = «Автомобиль заведен»

        return message2

car_a = Car()  

print(car_a.message1)

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

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

Атрибуты экземпляра и класса отличаются способом получения доступа к ним. Другими словами, речь идет об использовании названия класса и использовании названия экземпляра. С другой стороны, глобальные и локальные переменные отличаются своими областями видимости, другими словами, местами, где к ним может быть получен доступ.

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

Модификаторы доступа

Модификаторы доступа в Python используются для модификации области видимости переменных по умолчанию. Есть три типа модификаторов доступов в Python ООП:

  1. публичный — public;
  2. приватный — private;
  3. защищенный — protected.

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

Для создания приватной переменной, вам нужно проставить префикс двойного подчеркивание __ с названием переменной.

Для создания защищенной переменной, вам нужно проставить префикс из одного нижнего подчеркивания _ с названием переменной. Для публичных переменных, вам не нужно проставлять префиксы вообще.

Давайте взглянем на публичные, приватные и защищенные переменные в действии. Выполните следующий скрипт:

class Car:  

    def __init__(self):

        print («Двигатель заведен»)

        self.name = «corolla»

        self.__make = «toyota»

        self._model = 1999

Здесь мы создали простой класс Car с конструктором и тремя переменными: name, make, и model (название, марка и модель). Переменная name является публичной, в то время как переменные make и model являются приватными и защищенными, соответственно.

Давайте создадим объект класса Car и попытаемся получить доступ к переменной name. Выполним следующий скрипт:

car_a = Car()  

print(car_a.name)

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

Теперь попробуем вывести значение переменной make. Выполняем следующий скрипт:

В выдаче мы получим следующее уведомление об ошибке:

AttributeError: ‘Car’ object has no attribute ‘make’

Мы рассмотрели большую часть основных концепций объектно-ориентированного программирования в предыдущих двух секциях. Теперь, поговорим о столбах объектно-ориентированного программирования:

  • Полиморфизм;
  • Наследование;
  • Инкапсуляция.

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

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

В объектно-ориентированном программировании, наследование означает отношение IS-A. Например, болид — это транспорт. Наследование это одна из самых удивительных концепций объектно-ориентированного программирования, так как оно подразумевает повторное использование.

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

Рассмотрим на очень простой пример наследования. Выполним следующий скрипт:

# Создание класса Vehicle

class Vehicle:  

    def vehicle_method(self):

        print(«Это родительский метод из класса Vehicle»)

# Создание класса Car, который наследует Vehicle

class Car(Vehicle):  

    def car_method(self):

        print(«Это метод из дочернего класса»)

В скрипте выше мы создаем два класса: Vehicle и Car, который наследует класс Vehicle. Чтобы наследовать класс, вам нужно только вписать название родительского класса внутри скобок, которая следует за названием дочернего класса. Класс Vehicle содержит метод vehicle_method(), а дочерний класс содержит метод car_method(). Однако, так как класс Car наследует класс Vehicle, он также наследует и метод vehicle_method().

Рассмотрим это на практике и выполним следующий скрипт:

car_a = Car()  

car_a.vehicle_method() # Вызываем метод родительского класса

В этом скрипте мы создали объект класса Car вызывали метод vehicle_method() при помощи объекта класса Car. Вы можете обратить внимание на то, что класс Car не содержит ни одного метода vehicle_method(), но так как он унаследовал класс Vehicle, который содержит vehicle_method(), класс Car также будет использовать его. Выдача выглядит следующим образом:

Это родительский метод из класса Vehicle

Множественное наследование Python

В Python, родительский класс может иметь несколько дочерних, и, аналогично, дочерний класс может иметь несколько родительских классов. Давайте рассмотрим первый сценарий. Выполним следующий скрипт:

# создаем класс Vehicle

class Vehicle:  

    def vehicle_method(self):

        print(«Это родительский метод из класса Vehicle»)

# создаем класс Car, который наследует Vehicle

class Car(Vehicle):  

    def car_method(self):

        print(«Это дочерний метод из класса Car»)

# создаем класс Cycle, который наследует Vehicle

class Cycle(Vehicle):  

    def cycleMethod(self):

        print(«Это дочерний метод из класса Cycle»)

В этом скрипте, родительский класс Vehicle наследуется двумя дочерними классами — Car и Cycle. Оба дочерних класса будут иметь доступ к vehicle_method() родительского класса. Запустите следующий скрипт, чтобы увидеть это лично:

car_a = Car()  

car_a.vehicle_method() # вызов метода родительского класса

car_b = Cycle()  

car_b.vehicle_method() # вызов метода родительского класса

В выдаче вы увидите выдачу метода vehicle_method() дважды, как показано ниже:

Это родительский метод из класса Vehicle

Это родительский метод из класса Vehicle

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

class Camera:  

    def camera_method(self):

        print(«Это родительский метод из класса Camera»)

class Radio:  

    def radio_method(self):

        print(«Это родительский метод из класса Radio»)

class CellPhone(Camera, Radio):  

     def cell_phone_method(self):

        print(«Это дочерний метод из класса CellPhone»)

В скрипте выше мы создали три класса: Camera, Radio, и CellPhone. Классы Camera и Radio наследуются классом CellPhone. Это значит, что класс CellPhone будет иметь доступ к методам классов Camera и Radio. Следующий скрипт подтверждает это:

cell_phone_a = CellPhone()  

cell_phone_a.camera_method()  

cell_phone_a.radio_method()

Выдача будет выглядеть следующим образом:

Это родительский метод из класса Camera

Это родительский метод из класса Radio

Полиморфизм

Термин полиморфизм буквально означает наличие нескольких форм. В контексте объектно-ориентированного программирования, полиморфизм означает способность объекта вести себя по-разному.

Полиморфизм в программировании реализуется через перегрузку метода, либо через его переопределение.

Перегрузка метода

Перегрузка метода относится к свойству метода вести себя по-разному, в зависимости от количества или типа параметров. Взглянем на очень простой пример перегрузки метода. Выполним следующий скрипт:

# создаем класс Car

class Car:  

   def start(self, a, b=None):

        if b is not None:

            print (a + b)

        else:

            print (a)

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

Попробуем с одним аргументом для начала:

car_a = Car()  

car_a.start(10)

В выдаче мы можем видеть 10. Теперь попробуем передать два аргумента:

В выдаче вы увидите 30.

Переопределение метода

Переопределение метода относится к наличию метода с одинаковым названием в дочернем и родительском классах. Определение метода отличается в родительском и дочернем классах, но название остается тем же. Давайте посмотрим на простой пример переопределения метода в Python.

# создание класса Vehicle

class Vehicle:  

    def print_details(self):

        print(«Это родительский метод из класса Vehicle»)

# создание класса, который наследует Vehicle

class Car(Vehicle):  

    def print_details(self):

        print(«Это дочерний метод из класса Car»)

# создание класса Cycle, который наследует Vehicle

class Cycle(Vehicle):  

    def print_details(self):

        print(«Это дочерний метод из класса Cycle»)

В скрипте выше, классы Cycle и Car наследуют класс Vehicle. Класс Vehicle содержит метод print_details(), который переопределен дочерним классом. Теперь, если вы вызовите метод print_details(), выдача будет зависеть от объекта, через который вызывается метод. Выполните следующий скрипт, чтобы понять суть на деле:

car_a = Vehicle()  

car_a. print_details()

car_b = Car()  

car_b.print_details()

car_c = Cycle()  

car_c.print_details()

Выдача будет выглядеть вот так:

Это родительский метод из класса Vehicle

Это дочерний метод из класса Car

Это дочерний метод из класса Cycle

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

Инкапсуляция

Инкапсуляция — это третий столп объектно-ориентированного программирования. Инкапсуляция просто означает скрытие данных. Как правило, в объектно-ориентированном программировании один класс не должен иметь прямого доступа к данным другого класса. Вместо этого, доступ должен контролироваться через методы класса.

Чтобы предоставить контролируемый доступ к данным класса в Python, используются модификаторы доступа и свойства. Мы уже ознакомились с тем, как действуют модификаторы доступа. В этом разделе мы посмотрим, как действуют свойства.

Предположим, что нам нужно убедиться в том, что модель автомобиля должна датироваться между 2000 и 2018 годом. Если пользователь пытается ввести значение меньше 2000 для модели автомобиля, значение автоматически установится как 2000, и если было введено значение выше 2018, оно должно установиться на 2018. Если значение находится между 2000 и 2018 — оно остается неизменным. Мы можем создать свойство атрибута модели, которое реализует эту логику. Взглянем на пример:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

# создаем класс Car

class Car:

    # создаем конструктор класса Car

    def __init__(self, model):

        # Инициализация свойств.

        self.model = model

    # создаем свойство модели.

    @property

    def model(self):

        return self.__model

    # Сеттер для создания свойств.

    @model.setter

    def model(self, model):

        if model < 2000:

            self.__model = 2000

        elif model > 2018:

            self.__model = 2018

        else:

            self.__model = model

    def getCarModel(self):

        return «Год выпуска модели « + str(self.model)

carA = Car(2088)  

print(carA.getCarModel())

Свойство имеет три части. Вам нужно определить атрибут, который является моделью в скрипте выше. Затем, вам нужно определить свойство атрибута, используя декоратор @property. Наконец, вам нужно создать установщик свойства, который является дескриптором @model.setter в примере выше.

Теперь, если вы попробуете ввести значение выше 2018 в атрибуте модели, вы увидите, что значение установлено на 2018. Давайте проверим это. Выполним следующий скрипт:

car_a = Car(2088)  

print(car_a.get_car_model())

Здесь мы передаем 2088 как значение для модели, однако, если вы введете значение для атрибута модели через функцию get_car_model(), вы увидите 2018 в выдаче.

Подведем итоги

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

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

Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.

E-mail: vasile.buldumac@ati.utm.md

Образование
Universitatea Tehnică a Moldovei (utm.md)

  • 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
  • 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»

Данный урок посвящен объектно-ориентированному программированию в Python. Разобраны такие темы как создание объектов и классов, работа с конструктором, наследование и полиморфизм в Python.

Основные понятия объектно-ориентированного программирования

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

Выделяют три основных “столпа” ООП- это инкапсуляция, наследование и полиморфизм.

Инкапсуляция

Под инкапсуляцией понимается сокрытие деталей реализации, данных и т.п. от внешней стороны. Например, можно определить класс “холодильник”, который будет содержать следующие данные: производитель, объем, количество камер хранения, потребляемая мощность и т.п., и методы: открыть/закрыть холодильник, включить/выключить, но при этом реализация того, как происходит непосредственно включение и выключение пользователю вашего класса не доступна, что позволяет ее менять без опасения, что это может отразиться на использующей класс «холодильник» программе. При этом класс становится новым типом данных в рамках разрабатываемой программы. Можно создавать переменные этого нового типа, такие переменные называются объекты.

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

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

Примером базового класса, демонстрирующего наследование, можно определить класс “автомобиль”, имеющий атрибуты: масса, мощность двигателя, объем топливного бака и методы: завести и заглушить. У такого класса может быть потомок – “грузовой автомобиль”, он будет содержать те же атрибуты и методы, что и класс “автомобиль”, и дополнительные свойства: количество осей, мощность компрессора и т.п..

Полиморфизм

Полиморфизм позволяет одинаково обращаться с объектами, имеющими однотипный интерфейс, независимо от внутренней реализации объекта. Например, с объектом класса “грузовой автомобиль” можно производить те же операции, что и с объектом класса “автомобиль”, т.к. первый является наследником второго, при этом обратное утверждение неверно (во всяком случае не всегда). Другими словами полиморфизм предполагает разную реализацию методов с одинаковыми именами. Это очень полезно при наследовании, когда в классе наследнике можно переопределить методы класса родителя.

Создание классов и объектов

Создание класса в Python начинается с инструкции class. Вот так будет выглядеть минимальный класс.

class C: 
    pass

Класс состоит из объявления (инструкция class), имени класса (нашем случае это имя C) и тела класса, которое содержит атрибуты и методы (в нашем минимальном классе есть только одна инструкция pass).

Для того чтобы создать объект класса необходимо воспользоваться следующим синтаксисом:

имя_объекта = имя_класса()

Статические и динамические атрибуты класса

Как уже было сказано выше, класс может содержать атрибуты и методы. Атрибут может быть статическим и динамическим (уровня объекта класса). Суть в том, что для работы со статическим атрибутом, вам не нужно создавать экземпляр класса, а для работы с динамическим – нужно. Пример:

class Rectangle:
    default_color = "green"

    def __init__(self, width, height):
        self.width = width
        self.height = height

В представленном выше классе, атрибут default_color – это статический атрибут, и доступ к нему, как было сказано выше, можно получить не создавая объект класса Rectangle.

>>> Rectangle.default_color
'green'

width и height – это динамические атрибуты, при их создании было использовано ключевое слово self. Пока просто примите это как должное, более подробно про self будет рассказано ниже. Для доступа к width и height предварительно нужно создать объект класса Rectangle:

>>> rect = Rectangle(10, 20)
>>> rect.width
10
>>> rect.height
20

Если обратиться через класс, то получим ошибку:

>>> Rectangle.width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Rectangle' has no attribute 'width'

При этом, если вы обратитесь к статическому атрибуту через экземпляр класса, то все будет ОК, до тех пор, пока вы не попытаетесь его поменять. 

Проверим ещё раз значение атрибута default_color:

>>> Rectangle.default_color
'green'

Присвоим ему новое значение:

>>> Rectangle.default_color = "red"
>>> Rectangle.default_color
'red'

Создадим два объекта класса Rectangle и проверим, что default_color у них совпадает:

>>> r1 = Rectangle(1,2)
>>> r2 = Rectangle(10, 20)
>>> r1.default_color
'red'
>>> r2.default_color
'red'

Если поменять значение default_color через имя класса Rectangle, то все будет ожидаемо: у объектов r1 и r2 это значение изменится, но если поменять его через экземпляр класса, то у экземпляра будет создан атрибут с таким же именем как статический, а доступ к последнему будет потерян:

Меняем default_color через r1:

>>> r1.default_color = "blue"
>>> r1.default_color
'blue'

При этом у r2 остается значение статического атрибута:

>>> r2.default_color
'red'
>>> Rectangle.default_color
'red'

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

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

Добавим к нашему классу метод. Метод – это функция, находящаяся внутри класса и выполняющая определенную работу.

Методы бывают статическими, классовыми (среднее между статическими и обычными) и уровня класса (будем их называть просто словом метод). Статический метод создается с декоратором @staticmethod, классовый – с декоратором @classmethod, первым аргументом в него передается cls, обычный метод создается без специального декоратора, ему первым аргументом передается self:

class MyClass:

    @staticmethod
    def ex_static_method():
        print("static method")

    @classmethod
    def ex_class_method(cls):
        print("class method")

    def ex_method(self):
        print("method")

Статический и классовый метод можно вызвать, не создавая экземпляр класса, для вызова ex_method() нужен объект:

>>> MyClass.ex_static_method()
static method

>>> MyClass.ex_class_method()
class method

>>> MyClass.ex_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ex_method() missing 1 required positional argument: 'self'

>>> m = MyClass()
>>> m.ex_method()
method

Конструктор класса и инициализация экземпляра класса

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

Создадим реализацию класса Rectangle с измененным конструктором и инициализатором, через который задается ширина и высота прямоугольника:

class Rectangle:

    def __new__(cls, *args, **kwargs):
        print("Hello from __new__")
        return super().__new__(cls)

    def __init__(self, width, height):
        print("Hello from __init__")
        self.width = width
        self.height = height


>>> rect = Rectangle(10, 20)
Hello from __new__
Hello from __init__

>>> rect.width
10

>>> rect.height
20

Что такое self?

До этого момента вы уже успели познакомиться с ключевым словом self. self – это ссылка на текущий экземпляр класса, в таких языках как Java, C# аналогом является ключевое слово this. Через self вы получаете доступ к атрибутам и методам класса внутри него:

class Rectangle:

    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

В приведенной реализации метод area получает доступ к атрибутам width и height для расчета площади. Если бы в качестве первого параметра не было указано self, то при попытке вызвать area программа была бы остановлена с ошибкой.

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

Если вы знакомы с языками программирования Java, C#, C++ то, наверное, уже задались вопросом: “а как управлять уровнем доступа?”. В перечисленных языка вы можете явно указать для переменной, что доступ к ней снаружи класса запрещен, это делается с помощью ключевых слов (private, protected и т.д.). В Python таких возможностей нет, и любой может обратиться к атрибутам и методам вашего класса, если возникнет такая необходимость. Это существенный недостаток этого языка, т.к. нарушается один из ключевых принципов ООП – инкапсуляция. Хорошим тоном считается, что для чтения/изменения какого-то атрибута должны использоваться специальные методы, которые называются getter/setter, их можно реализовать, но ничего не помешает изменить атрибут напрямую. При этом есть соглашение, что метод или атрибут, который начинается с нижнего подчеркивания, является скрытым, и снаружи класса трогать его не нужно (хотя сделать это можно).

Внесем соответствующие изменения в класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self._width = width
        self._height = height

    def get_width(self):
        return self._width

    def set_width(self, w):
        self._width = w

    def get_height(self):
        return self._height

    def set_height(self, h):
        self._height = h

    def area(self):
        return self._width * self._height

В приведенном примере для доступа к _width и _height используются специальные методы, но ничего не мешает вам обратиться к ним (атрибутам) напрямую.

>>> rect = Rectangle(10, 20)

>>> rect.get_width()
10

>>> rect._width
10

Если же атрибут или метод начинается с двух подчеркиваний, то тут напрямую вы к нему уже не обратитесь (простым образом). Модифицируем наш класс Rectangle:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    def get_width(self):
        return self.__width

    def set_width(self, w):
        self.__width = w

    def get_height(self):
        return self.__height

    def set_height(self, h):
        self.__height = h

    def area(self):
        return self.__width * self.__height

Попытка обратиться к __width напрямую вызовет ошибку, нужно работать только через get_width():

>>> rect = Rectangle(10, 20)

>>> rect.__width
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__width'

>>> rect.get_width()
10

Но на самом деле это сделать можно, просто этот атрибут теперь для внешнего использования носит название: _Rectangle__width:

>>> rect._Rectangle__width
10

>>> rect._Rectangle__width = 20

>>> rect.get_width()
20

Свойства

Свойством называется такой метод класса, работа с которым подобна работе с атрибутом. Для объявления метода свойством необходимо использовать декоратор @property.

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

Сделаем реализацию класса Rectangle с использованием свойств:

class Rectangle:

    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def area(self):
        return self.__width * self.__height

Теперь работать с width и height можно так, как будто они являются атрибутами:

>>> rect = Rectangle(10, 20)

>>> rect.width
10

>>> rect.height
20

Можно не только читать, но и задавать новые значения свойствам:

>>> rect.width = 50

>>> rect.width
50

>>> rect.height = 70

>>> rect.height
70

Если вы обратили внимание: в setter’ах этих свойств осуществляется проверка входных значений, если значение меньше нуля, то будет выброшено исключение ValueError:

>>> rect.width = -10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "test.py", line 28, in width
    raise ValueError
ValueError

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

В организации наследования участвуют как минимум два класса: класс родитель и класс потомок. При этом возможно множественное наследование, в этом случае у класса потомка может быть несколько родителей. Не все языки программирования поддерживают множественное наследование, но в Python можно его использовать. По умолчанию все классы в Python являются наследниками от object, явно этот факт указывать не нужно.

Синтаксически создание класса с указанием его родителя выглядит так:

class имя_класса(имя_родителя1, [имя_родителя2,…, имя_родителя_n])

Переработаем наш пример так, чтобы в нем присутствовало наследование:

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c


class Rectangle(Figure): 

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError 

    def area(self):
        return self.__width * self.__height

Родительским классом является Figure, который при инициализации принимает цвет фигуры и предоставляет его через свойства. Rectangle – класс наследник от Figure. Обратите внимание на его метод __init__: в нем первым делом вызывается конструктор (хотя это не совсем верно, но будем говорить так) его родительского класса:

super().__init__(color)

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

Теперь у объекта класса Rectangle помимо уже знакомых свойств width и height появилось свойство color:

>>> rect = Rectangle(10, 20, "green")

>>> rect.width
10

>>> rect.height
20

>>> rect.color
'green'

>>> rect.color = "red"

>>> rect.color
'red'

Полиморфизм

Как уже было сказано во введении в рамках ООП полиморфизм, как правило, используется с позиции переопределения методов базового класса в классе наследнике. Проще всего это рассмотреть на примере. Добавим в наш базовый класс метод info(), который печатает сводную информацию по объекту класса Figure и переопределим этот метод в классе Rectangle, добавим  в него дополнительные данные:

class Figure:

    def __init__(self, color):
        self.__color = color

    @property
    def color(self):
        return self.__color

    @color.setter
    def color(self, c):
        self.__color = c

    def info(self):
       print("Figure")
       print("Color: " + self.__color)


class Rectangle(Figure):

    def __init__(self, width, height, color):
        super().__init__(color)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, w):
        if w > 0:
            self.__width = w
        else:
            raise ValueError

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, h):
        if h > 0:
            self.__height = h
        else:
            raise ValueError

    def info(self):
        print("Rectangle")
        print("Color: " + self.color)
        print("Width: " + str(self.width))
        print("Height: " + str(self.height))
        print("Area: " + str(self.area()))

    def area(self):
        return self.__width * self.__height

Посмотрим, как это работает

>>> fig = Figure("orange")

>>> fig.info()
Figure
Color: orange

>>> rect = Rectangle(10, 20, "green")

>>> rect.info()
Rectangle
Color: green
Width: 10
Height: 20
Area: 200

Таким образом, класс наследник может расширять функционал класса родителя.

P.S.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

<<< Python. Урок 13. Модули и пакеты   Python. Урок 15. Итераторы и генераторы>>>

Прежде чем приступить к теории, давайте решим следующую задачу.

Напишем программу, которая будет моделировать объекты класса «Автомобиль». При моделировании необходимо определить степень детализации объектов, которая зависит от действий, выполняемых этими объектами.

  • Пусть все автомобили имеют разный цвет.
  • Двигатель можно запустить, если в баке есть топливо.
  • Двигатель можно заглушить.
  • На автомобиле можно отправиться в путь на N километров при соблюдении следующих условий: двигатель запущен и запас топлива в баке и средний расход позволяют проехать этот путь.
  • После поездки запас топлива уменьшается в соответствии со средним расходом.
  • Автомобиль можно заправить до полного бака в любой момент времени.

Выделим важные для нашей программы свойства объектов класса: цвет, средний расход топлива, объём топливного бака, запас топлива, общий пробег.

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

Попробуем описать объекты этого класса с помощью коллекций и функций:

def create_car(color, consumption, tank_volume, mileage=0):
    return {
        "color": color,
        "consumption": consumption,
        "tank_volume": tank_volume,
        "reserve": tank_volume,
        "mileage": mileage,
        "engine_on": False
    }


def start_engine(car):
    if not car["engine_on"] and car["reserve"] > 0:
        car["engine_on"] = True
        return "Двигатель запущен."
    return "Двигатель уже был запущен."


def stop_engine(car):
    if car["engine_on"]:
        car["engine_on"] = False
        return "Двигатель остановлен."
    return "Двигатель уже был остановлен."


def drive(car, distance):
    if not car["engine_on"]:
        return "Двигатель не запущен."
    if car["reserve"] / car["consumption"] * 100 < distance:
        return "Малый запас топлива."
    car["mileage"] += distance
    car["reserve"] -= distance / 100 * car["consumption"]
    return f"Проехали {distance} км. Остаток топлива: {car['reserve']} л."


def refuel(car):
    car["reserve"] = car["tank_volume"]


def get_mileage(car):
    return f"Пробег {car['mileage']} км."


def get_reserve(car):
    return f"Запас топлива {car['reserve']} л."


car_1 = create_car(color="black", consumption=10, tank_volume=55)

print(start_engine(car_1))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 100))
print(drive(car_1, 300))
print(get_mileage(car_1))
print(get_reserve(car_1))
print(stop_engine(car_1))
print(drive(car_1, 100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

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

Объектно-ориентированное программирование (ООП) позволяет устранить недостатки процедурного подхода. Язык программирования Python является объектно-ориентированным. Это означает, что каждая сущность (переменная, функция и т. д.) в этом языке является объектом определённого класса. Ранее мы говорили, что, например, целое число является в Python типом данных int. На самом деле есть класс целых чисел int.

Убедимся в этом, написав простую программу:

print(type(1))

Вывод программы:

<class 'int'>

Синтаксис создания класса в Python выглядит следующим образом:

class <ИмяКласса>:
    <описание класса>

Имя класса по стандарту PEP 8 записывается в стиле CapWords (каждое слово с прописной буквы).

Давайте перепишем пример про автомобили с использованием ООП. Создадим класс Car и пока оставим в нём инструкцию-заглушку pass:

class Car:
    pass

В классах описываются свойства объектов и действия объектов или совершаемые над ними действия.

Свойства объектов называются атрибутами. По сути атрибуты — переменные, в значениях которых хранятся свойства объекта. Для создания или изменения значения атрибута необходимо использовать следующий синтаксис:

<имя_объекта>.<имя_атрибута> = <значение>

Действия объектов называются методами. Методы очень похожи на функции, в них можно передавать аргументы и возвращать значения с помощью оператора return, но вызываются методы после указания конкретного объекта. Для создания метода используется следующий синтаксис:

def <имя_метода>(self, <аргументы>):
    <тело метода>

В методах первым аргументом всегда идёт объект self. Он является объектом, для которого вызван метод. self позволяет использовать внутри описания класса атрибуты объекта в методах и вызывать сами методы.

Во всех классах Python есть специальный метод __init__(), который вызывается при создании объекта. В этом методе происходит инициализация всех атрибутов класса. В методы можно передавать аргументы. Вернёмся к нашему примеру и создадим в классе метод __init__(), который будет при создании автомобиля принимать его свойства как аргументы:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

Итак, мы создали класс автомобилей и описали метод __init__() для инициализации его объектов. Для создания объекта класса нужно использовать следующий синтаксис:

<имя_объекта> = <ИмяКласса>(<аргументы метода __init__()>)

Создадим в программе автомобиль класса Car. Для этого добавим следующую строку в основной код программы после описания класса, отделив от класса, согласно PEP 8, двумя пустыми строками:

car_1 = Car(color="black", consumption=10, tank_volume=55)

Обратите внимание: наш код стало легче читать, потому что мы видим, что создаётся объект определённого класса, а не просто вызывается функция, из которой возвращается значение-словарь.

Опишем с помощью методов, какие действия могут совершать объекты класса Car. По PEP 8, между объявлением методов нужно поставить одну пустую строку.

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve


car_1 = Car(color="black", consumption=10, tank_volume=55)
print(car_1.start_engine())
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(100))
print(car_1.drive(300))
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")
print(car_1.stop_engine())
print(car_1.drive(100))

Вывод программы:

Двигатель запущен.
Проехали 100 км. Остаток топлива: 45.0 л.
Проехали 100 км. Остаток топлива: 35.0 л.
Проехали 100 км. Остаток топлива: 25.0 л.
Малый запас топлива.
Пробег 300 км.
Запас топлива 25.0 л.
Двигатель остановлен.
Двигатель не запущен.

Обратите внимание: взаимодействие с объектом класса вне описания класса осуществляется только с помощью методов, прямого доступа к атрибутам не происходит. Этот принцип ООП называется инкапсуляцией.

Инкапсуляция заключается в сокрытии внутреннего устройства класса за интерфейсом, состоящим из методов класса. Это необходимо, чтобы не нарушать логику работы методов внутри класса. Если не следовать принципу инкапсуляции и попытаться взаимодействовать с атрибутами напрямую, то могут происходить изменения, которые приведут к ошибкам. Например, если в нашем примере попытаться изменить пробег напрямую, а не с помощью метода drive(), то автомобиль проедет указанный путь даже с пустым баком и без расхода топлива:

car_1 = Car(color="black", consumption=10, tank_volume=55)
car_1.mileage = 1000
print(f"Пробег {car_1.get_mileage()} км.")
print(f"Запас топлива {car_1.get_reserve()} л.")

Вывод программы:

Пробег 1000 км.
Запас топлива 55 л.

Давайте напишем ещё один класс для электромобилей. Их отличие будет заключаться в замене топливного бака на заряд аккумуляторной батареи:

class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

Напишем функцию range_reserve(), которая будет определять для автомобилей классов Car и ElectricCar запас хода в километрах. Функции, которые могут работать с объектами разных классов, называются полиморфными. А сам принцип ООП называется полиморфизмом.

Говоря о полиморфизме в Python, стоит упомянуть принятую в этом языке так называемую «утиную типизацию» (Duck typing). Она получила своё название от шутливого выражения: «Если нечто выглядит как утка, плавает как утка и крякает как утка, это, вероятно, утка и есть» («If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck»). В программах на Python это означает, что, если какой-то объект поддерживает все требуемые от него операции, с ним и будут работать с помощью этих операций, не заботясь о том, какого он на самом деле типа.

Чтобы функция работала для объектов обоих классов, необходимо предусмотреть одинаковый интерфейс в классах. Это означает, что методы классов, используемые в функции, должны называться одинаково, принимать одинаковые аргументы и возвращать значения одного типа данных.

Запас хода в километрах можно вычислить, разделив запас топлива (или заряд батареи) на расход и умножив результат на 100. Определить запас топлива или заряд батареи можно с помощью метода get_reserve(). Для соблюдения принципа инкапсуляции добавим метод get_consumption() в оба класса для получения значения атрибута consumption. Тогда полиморфная функция запишется так:

def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100

Полностью программа с классами, полиморфной функцией и пример их использования представлены ниже:

class Car:

    def __init__(self, color, consumption, tank_volume, mileage=0):
        self.color = color
        self.consumption = consumption
        self.tank_volume = tank_volume
        self.reserve = tank_volume
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый запас топлива."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток топлива: {self.reserve} л."

    def refuel(self):
        self.reserve = self.tank_volume

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


class ElectricCar:

    def __init__(self, color, consumption, bat_capacity, mileage=0):
        self.color = color
        self.consumption = consumption
        self.bat_capacity = bat_capacity
        self.reserve = bat_capacity
        self.mileage = mileage
        self.engine_on = False

    def start_engine(self):
        if not self.engine_on and self.reserve > 0:
            self.engine_on = True
            return "Двигатель запущен."
        return "Двигатель уже был запущен."

    def stop_engine(self):
        if self.engine_on:
            self.engine_on = False
            return "Двигатель остановлен."
        return "Двигатель уже был остановлен."

    def drive(self, distance):
        if not self.engine_on:
            return "Двигатель не запущен."
        if self.reserve / self.consumption * 100 < distance:
            return "Малый заряд батареи."
        self.mileage += distance
        self.reserve -= distance / 100 * self.consumption
        return f"Проехали {distance} км. Остаток заряда: {self.reserve} кВт*ч."

    def recharge(self):
        self.reserve = self.bat_capacity

    def get_mileage(self):
        return self.mileage

    def get_reserve(self):
        return self.reserve

    def get_consumption(self):
        return self.consumption


def range_reserve(car):
    return car.get_reserve() / car.get_consumption() * 100


car_1 = Car(color="black", consumption=10, tank_volume=55)
car_2 = ElectricCar(color="white", consumption=15, bat_capacity=90)
print(f"Запас хода: {range_reserve(car_1)} км.")
print(f"Запас хода: {range_reserve(car_2)} км.")

Вывод программы:

Запас хода: 550.0 км.
Запас хода: 600.0 км.

В нашей программе можно заметить, что классы Car и ElectricCar имеют много общих атрибутов и методов. Это привело к дублированию кода. В следующей главе мы познакомимся с наследованием — принципом ООП, позволяющим устранить подобную избыточность кода.

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

Logo Python Course Lesson 5

Курс «Программирование на Python»

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

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

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

One

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

Вы наверняка слышали, что существуют два главных подхода к написанию программ:

  1. Процедурное программирование
  2. Объектно-ориентированное программирование (оно же ООП)

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

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

    Smartiqa Процедурное программирование. Работа с данными.

    Процедурное программирование. Работа с данными.

    В чем суть процедурного подхода? Процедурное программирование – это написание функций и их последовательный вызов в некоторой главной(main) функции.

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

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

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

    Долгое время процедурный подход был довольно популярным и считался самым прогрессивным. Однако объемы кода программ росли, и ему на смену пришло объектно-ориентированное программирование. Вот как определяет данный подход Википедия:

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

    В основе ООП лежит простая и элегантная идея, в соответствии с которой главное в программе — это данные. Именно они определяют, какие методы будут использоваться для их обработки. Т. е. данные первичны, код для обработки этих данных — вторичен.

      Smartiqa Объектно-ориентированное программирование. Работа с данными.

      Объектно-ориентированное программирование. Работа с данными.

      Итак, чем же хорош подход ООП?

      1. Программа разбивается на объекты. Каждый объект отвечает за собственные данные и их обработку. Как результат — код становится проще и читабельней.
      2. Уменьшается дупликация кода. Нужен новый объект, содержимое которого на 90% повторяет уже существующий? Давайте создадим новый класс и унаследуем эти 90% функционала от родительского класса!
      3. Упрощается и ускоряется процесс написания программ. Можно сначала создать высокоуровневую структуру классов и базовый функционал, а уже потом перейти к их подробной реализации.

      Подытожим

      1. В процедурном подходе основой программы является функция. Функции вызывают друг друга и при необходимости передают данные. В программе функции живут отдельно, данные — отдельно.
      2. Основной недостаток процедурного подхода — сложность создания и поддержки больших программ. Наличие сотен функций в таких проектах очень часто приводит к ошибкам и спагетти-коду.
      3. В основе объектно-ориентированного программирования лежит понятие объекта. Объект совмещает в себе и функции и данные.
      4. Основное преимущество ООП перед процедурным программированием — изоляция кода на уровне классов, что позволяет писать более простой и лаконичный код.

      Two

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

      Мир, в котором мы живем, состоит из объектов. Это деревья, солнце, горы, дома, машины, бытовая техника. Каждый из этих объектов имеет свой набор характеристик и предназначение. Несложно догадаться, что именно объектная картина реального мира легла в основу ООП. Разберем несколько ключевых понятий, основываясь на Википедии:

        Класс — в объектно-ориентированном программировании, представляет собой шаблон для создания объектов, обеспечивающий начальные значения состояний: инициализация полей-переменных и реализация поведения функций или методов.

        Объект — некоторая сущность в цифровом пространстве, обладающая определённым состоянием и поведением, имеющая определенные свойства (поля) и операции над ними (методы). Как правило, при рассмотрении объектов выделяется то, что объекты принадлежат одному или нескольким классам, которые определяют поведение (являются моделью) объекта. Термины «экземпляр класса» и «объект» взаимозаменяемы.

        На что необходимо обратить внимание?

        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

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

        Как мы уже сказали, на текущий момент ООП является самой востребованной и распространенной парадигмой программирования. Концепция ООП строится на основе 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

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

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

        class <название_класса>:
            <тело_класса>

        А вот так компактно смотрится пример объявления класса с минимально возможным функционалом:

        Как мы видим, для задания класса используется инструкция class, далее следует имя класса Car и двоеточие. После них идет тело класса, которое в нашем случае представлено оператором pass. Данный оператор сам по себе ничего не делает — фактически это просто заглушка.

        Чтобы создать объект класса, нужно воспользоваться следующим синтаксисом:

        <имя_объекта> = <имя_класса>()

        И в качестве примера создадим объект класса Car:

        Five

        Атрибуты класса в 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(). Если взять самый простой класс:

        То мы получим вот такой список атрибутов:

        ['__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

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

        Поля(они же свойства или переменные) можно (так же условно) разделить на две группы:

        1. Статические поля
        2. Динамические поля

        В чем же разница?

        Python. ООП Поля класса

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

        Это переменные, которые объявляются внутри тела класса и создаются тогда, когда создается класс. Создали класса — создалась переменная:

        class Phone:
        
            # Статические поля (переменные класса)
            default_color = 'Grey'
            default_model = ‘C385’
        
            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__', 'call', 'default_color', 'default_model', 'turn_on']

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

        Python — Интерактивный режим

        >>> Phone.default_color
        'Grey'
        
        # Изменяем цвет телефона по умолчанию с серого на черный
        >>> Phone.default_color = 'Black'
        >>> Phone.default_color
        'Black'

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

        Это переменные, которые создаются на уровне экземпляра класса. Нет экземпляра — нет его переменных. Для создания динамического свойства необходимо обратиться к self внутри метода:

        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

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

        Как вы уже знаете, функции внутри класса называются методами. Методы так же бывают разными, а именно — их можно разделить на 3 группы:

        1. Методы экземпляра класса (они же обычные методы)
        2. Статические методы
        3. Методы класса

        А в чем отличие между ними, давайте разбираться.

        Python. ООП Методы класса

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

        Это группа методов, которые становятся доступны только после создания экземпляра класса, то есть чтобы вызвать такой метод, надо обратиться к экземпляру. Как следствие — первым параметром такого метода является слово self. И как мы уже обсудили выше, с помощью данного параметра в метод передается ссылка на объект класса, для которого он был вызван. Теперь пример:

        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. Выглядит это следующим образом:

        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 является ссылкой на сам класс (а не на объект). Рассмотрим пример:

        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

        Уровни доступа атрибутов в 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:

        class Phone:
        
            def __init__(self, color):
                # Объявляем публичное поле color
                self.color = color

        >>> from phone import Phone
        
        # Создаем экземпляр класса Phone
        >>> phone = Phone('Grey')
        
        # Обращаемся к свойству color
        >>> phone.color
        'Grey'
        
        # Изменяем свойство color
        >>> phone.color = 'Red'
        >>> phone.color
        'Red'

        Как видите, никаких проблем. Идем дальше. Как мы уже сказали, в соотвествии с соглашением чтобы сделать атрибут класса защищенным (protected), необходимо добавить к имени символ подчеркивания _ . Как, например, здесь:

        class Phone:
        
            def __init__(self, color):
                # Объявляем защищенное поле _color
                self._color = color

        Однако по факту в Python такой атрибут все равно будет доступен снаружи класса. Вы все еще можете выполнить операции, которые мы рассмотрели выше:

        # Создаем экземпляр класса Phone
        >>> phone = Phone('Grey')
        
        # Обращаемся к защищенному свойству _color
        >>> phone._color
        'Grey'
        
        # Изменяем защищенное свойство _color
        >>> phone._color = 'Red'
        >>> phone._color
        'Red'

        Иными словами, это больше вопрос ответственности программиста — он не должен работать с атрибутами, имена которых начинаются с нижнего подчёркивания _ , снаружи класса.

        Аналогично, два нижних подчеркивания __ в названии свойства/метода делают его приватным (private). Здесь уже интереснее — получить доступ к таким атрибутам напрямую нельзя (но если очень хочется, то все равно можно — об этом чуть ниже):

        class Phone:
        
            def __init__(self, color):
                # Объявляем приватное поле __color
                self.__color = color

        >>> 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'

        Однако если мы заглянем в список атрибутов нашего класса, то увидим, что на самом деле наше свойство просто получило другое имя. И как вы уже наверное догадались, оно все еще доступно извне, но теперь уже по другому имени:

        # Получаем список атрибутов класса, в котором находим новое имя свойства: '_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

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

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

        Для того, чтобы в Python создать новый класс с помощью механизма наследования, необходимо воспользоваться следующим синтаксисом:

        class <имя_нового_класса>(<имя_родителя>):

        Теперь давайте рассмотрим пример применения механизма наследования в действии. Перед нами класс Phone (Телефон), у которого есть одно свойство is_on и три метода:

        1. Инициализатор: __init__()
        2. Включение: turn_on()
        3. Звонок: call()
        # Родительский класс
        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...')

        В результате объект такого класса получит следующий набор атрибутов:

        >>> 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. Выглядит это так:

        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, а значит от унаследовал и его функционал тоже. Создадим объект нового класса и посмотрим список его атрибутов:

        >>> 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. Пример использования:

        # Импортируем оба класса
        >>> 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

        Полиморфизм в 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()

        Вызываем наш демонстрационный метод:

        >>> 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

        Задача «Покупка дома»

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

        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

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

        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. Соберите урожай

        Также предлагаем вам к рассмотрению и изучению наши варианты решений и соответствующие комментарии к каждому из заданий.

        Как вам материал?

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

        12.1. Объектно-ориентированное программирование¶

        Python является объектно-ориентированным языком программирования, что означает наличие в языке средств объектно-ориентированного программирования (ООП).

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

        До сих пор мы писали программы с использованием парадигмы процедурного программирования. Процедурное программирование фокусируется на создании функций или процедур, которые работают с данными. Объектно-ориентированное программирование фокусируется на создании объектов, которые содержат и данные и функциональность.

        12.2. Определяемые пользователем типы данных¶

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

        Рассмотрим понятие математической точки. В пространстве двух измерений, точка — это два числа (координаты), с которыми работают как с одним объектом. В математике координаты точки часто записываются в скобках, разделенные запятой. Например, (0, 0) представляет начало координат, а (x, y) представляет точку, расположенную на x единиц правее и на y единиц выше, чем начало координат.

        Естественный способ представления точки на языке Python — с помощью двух чисел. Но остается вопрос: как именно объединить эти два числа в один составной объект? Очевидное и быстрое решение состоит в том, чтобы использовать список или кортеж, и в некоторых случаях оно будет наилучшим.

        Альтернативой является определение нового типа, называемого также классом. Этот подход требует немного больше усилий, но имеет преимущества, которые вскоре станут вам понятны.

        Определение нашего класса Point (англ.: точка) выглядит так:

        Определения классов могут встречаться в программе где угодно, но обычно их помещают в начале, после предложений import. Синтаксические правила для определения класса такие же, как и для других составных предложений. Первая строка — заголовок, начинающийся с ключевого слова class, за которым следуют имя класса и двоеточие, следующие строки — тело класса.

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

        Для этой цели подойдет и документирующая строка:

        class Point:
            "Point class for storing mathematical points."
        

        Создав класс Point, мы создали новый тип Point. Представители этого типа называются экземплярами или объектами этого типа. Создание экземпляра класса выполняется с помощью вызова класса. Классы, как и функции, можно вызывать, и мы создаем объект типа Point, вызывая класс Point:

        >>> type(Point)
        <type 'classobj'>
        >>> p = Point()
        >>> type(p)
        <type 'instance'>
        

        Переменная p содержит ссылку на новый объект типа Point.

        Можно думать о классе, как о фабрике по изготовлению объектов. Тогда наш класс Point — фабрика по изготовлению точек. Сам класс не является точкой, но содержит все, что необходимо для производства точек.

        12.3. Атрибуты¶

        Как и объекты реального мира, экземпляры классов обладают свойствами и поведением. Свойства определяются элементами-данными, которые содержит объект.

        Можно добавить новые элементы-данные к экземпляру класса с помощью точечной нотации:

        Этот синтаксис подобен синтаксису для обращения к переменной или функции модуля, например, math.pi или string.uppercase. И модули, и экземпляры класса создают свое собственное пространство имен, и синтаксис для доступа к элементам тех и других — атрибутам — один и тот же. В данном случае атрибуты, к которым мы обращаемся, — элементы-данные в экземпляре класса.

        Следующая диаграмма состояний показывает результат выполненных присваиваний:

        диаграмма состояний Point

        Переменная p ссылается на объект класса Point, который содержит два атрибута. Каждый из атрибутов ссылается на число.

        Тот же самый синтаксис используется для получения значений атрибутов:

        >>> print p.y
        4
        >>> x = p.x
        >>> print x
        3
        

        Выражение p.x означает: возьмите объект, на который указывает переменная p, затем возьмите значение атрибута x этого объекта. В приведенном примере мы присваиваем полученное значение переменной с именем x. Переменная x и атрибут x не вступают в конфликт имен, поскольку принадлежат разным пространствам имен.

        Точечную нотацию можно использовать как часть любого выражения, так что следующие предложения совершенно типичны:

        print '(%d, %d)' % (p.x, p.y)
        distance_squared = p.x * p.x + p.y * p.y
        

        Первая строка выводит (3, 4). Вторая строка вычисляет значение 25.

        12.4. Инициализирующий метод и self

        Поскольку наш класс Point предназначен для представления математических точек в двумерном пространстве, все экземпляры этого класса должны иметь атрибуты x и y. Но пока это не так для наших объектов Point.

        >>> p2 = Point()
        >>> p2.x
        Traceback (most recent call last):
          File "<stdin>", line 1, in ?
        AttributeError: Point instance has no attribute 'x'
        >>>
        

        Для решения этой проблемы добавим в наш класс инициализирующий метод.

        class Point:
            def __init__(self, x=0, y=0):
                self.x = x
                self.y = y
        

        Метод ведет себя как функция, но является частью объекта. Доступ к методу, как и доступ к атрибутам-данным, осуществляется при помощи точечной нотации. Инициализирующий метод вызывается автоматически, когда вызывается класс.

        Чтобы получше разобраться, как работают методы, давайте добавим еще один метод, distance_from_origin (англ.: расстояние от начала):

        class Point:
            def __init__(self, x=0, y=0):
                self.x = x
                self.y = y
        
            def distance_from_origin(self):
                return ((self.x ** 2) + (self.y ** 2)) ** 0.5
        

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

        >>> p = Point(3, 4)
        >>> p.x
        3
        >>> p.y
        4
        >>> p.distance_from_origin()
        5.0
        >>> q = Point(5, 12)
        >>> q.x
        5
        >>> q.y
        12
        >>> q.distance_from_origin()
        13.0
        >>> r = Point()
        >>> r.x
        0
        >>> r.y
        0
        >>> r.distance_from_origin()
        0.0
        

        В определении метода первый параметр всегда указывает на экземпляр класса. Традиционно этому параметру дают имя self. В только что рассмотренном примере параметр self последовательно указывает на объекты p, q, и r.

        12.5. Объекты как параметры¶

        Объект можно передать в качестве параметра, как любое другое значение. Например:

        def print_point(p):
            print '(%s, %s)' % (str(p.x), str(p.y))
        

        Функция print_point принимает объект Point в качестве аргумента и выводит его значение. Если выполнить print_point(p) с объектом p, определенным выше, то функция выведет (3, 4).

        12.6. Равенство объектов¶

        Смысл слова ‘равенство’ кажется совершенно ясным. Но если говорить об объектах, то мы скоро обнаружим неоднозначность этого слова.

        Например, что означает утверждение, что значения двух переменных типа Point равны? Что соответствующие объекты Point содержат одинаковые данные (координаты точки)? Или что обе переменные указывают на один и тот же объект?

        Чтобы выяснить, ссылаются ли две переменные на один и тот же объект, используется оператор ==. Например:

        >>> p1 = Point()
        >>> p1.x = 3
        >>> p1.y = 4
        >>> p2 = Point()
        >>> p2.x = 3
        >>> p2.y = 4
        >>> p1 == p2
        False
        

        Хотя p1 и p2 содержат одинаковые координаты, они являются разными объектами. Но если присвоить переменной p1 значение p2, то две переменных будут альтернативными именами одного и того же объекта:

        >>> p2 = p1
        >>> p1 == p2
        True
        

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

        Для того, чтобы сравнить содержимое объектов — проверить глубокое равенство — можно написать функцию, подобную этой:

        def same_point(p1, p2):
            return (p1.x == p2.x) and (p1.y == p2.y)
        

        Теперь, если создать два разных объекта, содержащих одинаковые данные, с помощью same_point можно выяснить, представляют ли они одну и ту же математическую точку.

        >>> p1 = Point()
        >>> p1.x = 3
        >>> p1.y = 4
        >>> p2 = Point()
        >>> p2.x = 3
        >>> p2.y = 4
        >>> same_point(p1, p2)
        True
        

        А если две переменные ссылаются на один и тот же объект, для них выполняется как поверхностное, так и глубокое равенство.

        12.7. Прямоугольники¶

        Пусть нам нужен класс для представления прямоугольников. Вопрос в том, какую информацию необходимо указать, чтобы описать прямоугольник? Для простоты предположим, что стороны прямоугольника ориентированы горизонтально и вертикально.

        Есть несколько вариантов. Мы могли бы указать координаты центра прямоугольника и его размер (ширину и высоту). Или указать координаты одного из углов и размер прямоугольника. Или указать координаты двух противоположных углов. Традиционный способ таков: указать левый верхний угол прямоугольника и его размер.

        Определим новый класс Rectangle (англ.: прямоугольник):

        И создадим экземпляр этого класса:

        box = Rectangle()
        box.width = 100.0
        box.height = 200.0
        

        Этот код создает новый объект Rectangle с двумя атрибутами — числами с плавающей точкой – width (англ.: ширина) и height (англ.: высота). А для того, чтобы указать левый верхний угол, можно вставить объект внутрь объекта!

        box.corner = Point()
        box.corner.x = 0.0
        box.corner.y = 0.0
        

        Операторы точка можно сочетать, как видно из этого примера. Выражение box.corner.x означает: возьмите объект, на который указывает box, получите его атрибут corner; затем возьмите объект, на который указывает этот атрибут, и получите атрибут x этого последнего объекта.

        Следующий рисунок иллюстрирует, что у нас получилось:

        объект Rectangle

        12.8. Объекты как возвращаемые значения¶

        Функции могут возвращать объекты. Например, функция find_center
        берет Rectangle в качестве аргумента и возвращает Point с координатами центра прямоугольника:

        def find_center(box):
            p = Point()
            p.x = box.corner.x + box.width/2.0
            p.y = box.corner.y - box.height/2.0
            return p
        

        Следующий код демонстрирует использование функции:

        >>> center = find_center(box)
        >>> print_point(center)
        (50.0, 100.0)
        

        12.9. Объекты изменяемы¶

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

        box.width = box.width + 50
        box.height = box.height + 100
        

        Обобщим этот код, определив функцию grow_rect (англ.: увеличить прямоугольник):

        def grow_rect(box, dwidth, dheight):
            box.width += dwidth
            box.height += dheight
        

        12.10. Копирование¶

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

        Вместо использования альтернативных имен для одного и того же объекта, во многих случаях полезно получить копию объекта. Модуль copy содержит функцию copy, которая способна скопировать любой объект:

        >>> import copy
        >>> p1 = Point()
        >>> p1.x = 3
        >>> p1.y = 4
        >>> p2 = copy.copy(p1)
        >>> p1 == p2
        False
        >>> same_point(p1, p2)
        True
        

        После импортирования модуля copy, с помощью функции copy мы создаем новый объект класса Point. Объекты p1 и p2 являются разными объектами, но содержат одинаковые данные.

        Для копирования простых объектов вроде Point, которые не содержат вложенных объектов, функции copy достаточно. Такое копирование называется поверхностным копированием.

        Для объектов, подобных объектам Rectangle, которые содержат ссылку на объект Point, функция copy не совсем то, что обычно требуется. Она скопирует ссылку на объект Point, так, что и старый объект Rectangle, и новый, будут ссылаться на один и тот же объект Point.

        Если мы создадим прямоугольник b1 и сделаем его копию b2 с помощью copy, то результат будет таким:

        прямоугольники

        Это, скорее всего, не то, что мы хотели получить. В этом случае вызов функции grow_rect с одним объектом Rectangle не повлияет на другой, однако, вызов move_rect (см. упражнения в конце главы) с любым из прямоугольников отразится на обоих! Такое поведение сбивает с толку и чревато ошибками.

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

        >>> b2 = copy.deepcopy(b1)
        

        Теперь b1 и b2 — совершенно разные объекты.

        Используя deepcopy, можно переписать grow_rect так, чтобы вместо изменения существующего объекта Rectangle, он создавал новый объект Rectangle с таким же расположением левого верхнего угла, но с другими размерами:

        def grow_rect(box, dwidth, dheight):
            import copy
            new_box = copy.deepcopy(box)
            new_box.width += dwidth
            new_box.height += dheight
            return new_box
        

        12.11. Time¶

        В качестве еще одного примера определенного пользователем типа, создадим класс Time (англ.: время) для хранения времени дня. Определение класса будет таким:

        Теперь мы можем создать новый объект класса Time и установить значения атрибутов для часов, минут и секунд:

        time = Time()
        time.hours = 11
        time.minutes = 59
        time.seconds = 30
        

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

        12.12. Снова чистые функции¶

        Вот черновая версия функции add_time:

        def add_time(t1, t2):
            sum = Time()
            sum.hours = t1.hours + t2.hours
            sum.minutes = t1.minutes + t2.minutes
            sum.seconds = t1.seconds + t2.seconds
            return sum
        

        Функция создает новый объект Time, инициализирует его атрибуты, и возвращает ссылку на него. Это — чистая функция, поскольку она не изменяет ни один из переданных ей объектов и не имеет побочных эффектов, вроде вывода значения на печать или получения ввода от пользователя.

        Вот пример использования этой функции. Мы создадим два объекта Time: current_time, содержащий текущее время, и bread_time, содержащий количество времени, необходимое хлебопечке для приготовления хлеба. Затем воспользуемся функцией add_time чтобы узнать, во сколько хлеб будет готов.

        >>> current_time = Time()
        >>> current_time.hours = 9
        >>> current_time.minutes = 14
        >>> current_time.seconds =  30
        >>> bread_time = Time()
        >>> bread_time.hours = 3
        >>> bread_time.minutes = 35
        >>> bread_time.seconds = 0
        >>> done_time = add_time(current_time, bread_time)
        

        Определим функцию print_time для вывода объекта Time, воспользовавшись оператором форматирования сток:

        def print_time(time):
            print "%02i:%02i:%02i" % (time.hours, time.minutes, time.seconds)
        

        Теперь выведем полученный нами результат:

        >>> print_time(done_time)
        12:49:30
        

        Программа выводит 12:49:30, и это правильный результат. Однако, в некоторых случаях результат работы функции add_time будет неверным. Можете сами привести пример такого случая?

        Проблема с функцией add_time в том, что функция не учитывает случаи, когда сумма секунд или минут превышает 60. Когда это случается, необходимо выполнить перенос из переполнившегося разряда в разряд минут или часов.

        Вот вторая, улучшенная, версия нашей функции:

        def add_time(t1, t2):
            sum = Time()
            sum.hours = t1.hours + t2.hours
            sum.minutes = t1.minutes + t2.minutes
            sum.seconds = t1.seconds + t2.seconds
        
            if sum.seconds >= 60:
                sum.seconds = sum.seconds - 60
                sum.minutes = sum.minutes + 1
        
            if sum.minutes >= 60:
                sum.minutes = sum.minutes - 60
                sum.hours = sum.hours + 1
        
            return sum
        

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

        12.13. Снова модифицирующие функции¶

        Бывают случаи, когда изменение функцией объектов, переданных ей как параметры, оказывается полезным. Обычно вызывающий код сохраняет ссылки на объекты, которые он передает функции в качестве параметров, так что все изменения, сделанные функцией, доступны в вызывающем коде. Как вы помните, функции, работающие таким образом, называются модифицирующими.

        Функцию increment, добавляющую указанное число секунд к объекту Time, наиболее естественно написать как модифицирующую. Вот ее черновая версия:

        def increment(time, seconds):
            time.seconds = time.seconds + seconds
        
            if time.seconds >= 60:
                time.seconds = time.seconds - 60
                time.minutes = time.minutes + 1
        
            if time.minutes >= 60:
                time.minutes = time.minutes - 60
                time.hours = time.hours + 1
        

        Первая строка выполняет основную операцию. Остальной код обрабатывает специальные случаи, которые мы обсудили выше.

        Корректна ли эта функция? Что случится, если количество секунд, переданное функции, намного больше, чем 60? В этом случае недостаточно одного переноса 1 в разряд минут; мы должны выполнять переносы до тех пор, пока значение seconds продолжает быть меньше 60. Одно из возможных решений — заменить предложение if предложением while:

        def increment(time, seconds):
            time.seconds = time.seconds + seconds
        
            while time.seconds >= 60:
                time.seconds = time.seconds - 60
                time.minutes = time.minutes + 1
        
            while time.minutes >= 60:
                time.minutes = time.minutes - 60
                time.hours = time.hours + 1
        

        Теперь функция работает правильно, но это не самое эффективное решение.

        12.14. Прототипирование и разработка дизайна программы¶

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

        Хотя этот подход в целом эффективен, но, если пренебречь тщательным обдумыванием решаемой задачи, он может привести к излишне усложненному и ненадежному коду. Усложненному – поскольку придется иметь дело со многими специальными случаями. И ненадежному — поскольку нельзя утверждать, что все такие случаи учтены и все ошибки найдены.

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

        В данном случае, анализ подскажет нам, что объект Time, представляющий количество времени, есть не что иное, как трехразрядное число с основанием 60! Действительно, секунды — это младший разряд единиц, минуты — разряд “шестидесяток”, а часы представлены самым старшим разрядом. “Единица” старшего разряда соответствует 3600 секундам.

        Когда мы писали функции add_time и increment, мы на самом деле выполняли сложение в системе счисления с основанием 60, вот почему нам пришлось делать переносы из одного разряда в другой.

        Это наблюдение предлагает другой подход к задаче в целом: мы можем преобразовать три компонента объекта Time в одно единственное число, и далее выполнять арифметические действия с этим числом. Следующая функция преобразует объект Time в целое число:

        def convert_to_seconds(t):
            minutes = t.hours * 60 + t.minutes
            seconds = minutes * 60 + t.seconds
            return seconds
        

        Все, что нам нужно теперь, — это способ преобразовать целое число обратно в Time:

        def make_time(seconds):
            time = Time()
            time.hours = seconds/3600
            seconds = seconds - time.hours * 3600
            time.minutes = seconds/60
            seconds = seconds - time.minutes * 60
            time.seconds = seconds
            return time
        

        Посмотрите внимательно на приведенный код, чтобы убедиться, что преобразование выполняется корректно. Если вы согласны с этим, то перепишите add_time с использованием этих функций:

        def add_time(t1, t2):
            seconds = convert_to_seconds(t1) + convert_to_seconds(t2)
            return make_time(seconds)
        

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

        12.15. Когда сложнее значит проще¶

        Преобразование представления чисел из системы счисления с одним основанием в другую, и затем обратно, на первый взгляд кажется сложнее, чем прямое манипулирование привычными нам тремя компонентами времени: часами, минутами и секундами. А раз так, то не лучше ли полагаться на привычку, когда имеем дело со временем?

        Но если мы нашли решение, основанное на представлении количества времени числом с основанием 60, и написали функции преобразования (convert_to_seconds и make_time), мы получаем более короткую и более надежную программу, которую легче читать и отлаживать.

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

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

        12.16. Глоссарий¶

        атрибут
        Один из именованных элементов некоторого составного типа.
        глубокое копирование
        Копирование содержимого объекта, а также всех вложенных объектов
        произвольного уровня вложенности. Реализовано в функции deepcopy
        модуля copy.
        класс
        Определенный пользователем тип данных. Можно думать о классе как о
        шаблоне для создания объектов — экземпляров этого класса.
        объект
        Экземпляр класса. Объекты часто используются для моделирования предметов
        или концепций реального мира.
        поверхностное копирование
        Копирование содержимого объекта, включая ссылки на вложенные объекты.
        Реализовано в функции copy модуля copy.
        разработка дизайна программы
        Деятельность, предполагающая анализ
        задачи, выработку и (часто) документирование основных решений относительно
        создаваемой программы прежде, чем начнется написание программы.
        разработка через прототипирование
        Способ разработки программ, предполагающий создание прототипа программы и
        дальнейшее его улучшение через тестирование и отладку.
        экземпляр класса
        Объект класса.

        12.17. Упражнения¶

        1. Создайте объект Point и выведите его с помощью print. Затем с помощью
          функции id напечатайте уникальный идентификатор объекта. Убедитесь, что
          выведенные шестнадцатеричное и десятичное значения — одно и то же число.
        2. Перепишите функцию distance из главы 5 так, чтобы ее параметрами были
          два объекта Point вместо четырех чисел.
        3. Напишите функцию с именем move_rect, которая принимает в качестве
          параметров объект Rectangle и два числа; имена числовых параметров dx
          и dy. Функция должна изменить положение прямоугольника, прибавив dx
          к координате x, и прибавив dy к координате y вложенного объекта
          corner.
        4. Перепишите функцию move_rect так, чтобы она создавала и возвращала новый
          объект Rectangle, вместо изменения существующего.
        5. Напишите логическую функцию after, которая принимает в качестве параметров
          два объекта Time, t1 и t2, и возвращает True, если t1
          следует за t2 хронологически, и False, если это не так.
        6. Перепишите функцию increment так, чтобы она не содержала циклов.
        7. Теперь перепишите функцию increment как чистую функцию и напишите вызовы
          обеих функций.

        Понравилась статья? Поделить с друзьями:

        А вот и еще наши интересные статьи:

      1. Письмо руководства аудиторской организации это
      2. Регистрация кассы онлайн пошаговая инструкция для ип
      3. Бета фпв цетус х инструкция на русском языке
      4. Руководство верховной рады украины
      5. Руководство по утеплению фасада

      6. 0 0 голоса
        Рейтинг статьи
        Подписаться
        Уведомить о
        guest

        0 комментариев
        Старые
        Новые Популярные
        Межтекстовые Отзывы
        Посмотреть все комментарии