Введение

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

Части статьи

  1. Статические методы
  2. Методы класса
  3. Полиморфизм
  4. Инкапсуляция, сокрытие данных и методы установки
  5. Принцип замещения Лискова
  6. Сеттер и декоратор @property

Примечание

Напоминаю, что в прошлой статье я реализовал два класса, Pokémon и Evolve1:

Покемон

Развитие1:

Давайте продолжим и построим эти два метода.

Часть №1 — Статические методы

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

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

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

Теперь давайте создадим Charmander и проверим, если это выходные:

Как мы видим, is_weekday() вообще не принимает никаких аргументов.

Часть № 2 — Методы класса

Метод класса — это метод, который принадлежит всему классу, а не конкретному экземпляру класса.

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

Точно так же работает метод класса.

Например, давайте создадим метод класса для изменения number_of_legs класса Pokemon:

Новый класс покемонов выглядит так:

Теперь создадим Чармандера и проверим количество ножек:

Теперь давайте изменим количество ног для ВСЕХ покемонов на четыре и снова проверим Чармандера:

Большой. Как мы видим, новый метод класса может изменить атрибут number_of_legs для ВСЕХ покемонов.

Метод класса как второй конструктор

Методы класса на самом деле имеют еще одну очень важную функциональность в ООП.

Их можно использовать как еще один способ построения и создания экземпляра класса (точно так же, как __init __ является конструктором). Например, давайте создадим метод класса для создания покемона, просто назвав его.

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

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

Давайте добавим его в класс покемонов:

Давайте посмотрим, как работает этот новый конструктор. Давайте создадим Charmander, просто вызвав его имя:

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

Выход:

Примечание. Для таких гиков, как я, это на самом деле статистика Чармандера:

Давайте также добавим этот метод класса в класс Evolve1:

Теперь мы также можем создать Charmeleon, вызвав его имя:

Выход:

Часть №3 — Полиморфизм

В ООП полиморфизм — это идея создания единого интерфейса между всеми классами.

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

Эта концепция может быть немного запутанной. Давайте проверим пример нарушения идеи полиморфизма.

Например, как покемоны, так и классы Evolve1 могут атаковать других покемонов:

Выход:

Однако обратите внимание, что когда Чармандер наносит удар Чармелеону, он использует метод strike(), а когда Чармелеон наносит удар Чармандеру, он использует метод special_strike().

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

Пользователь не должен знать, эволюционировал класс или нет, прежде чем ударить другого покемона.

Сделаем единый интерфейс для нанесения ударов покемону:

Теперь пусть они снова ударят друг друга:

Выход:

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

Часть № 4 — Инкапсуляция, скрытие данных и методы установки

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

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

Давайте добавим метод set level_up(), который повысит уровень покемона на единицу и увеличит его характеристики на 2%. Другими словами, я даю пользователям возможность изменять атрибуты класса БЕЗ прямого доступа к ним.

Новый метод set выглядит следующим образом:

Я объясню больше о методах set и декораторе @propery позже. Я также добавлю новый атрибут под названием «уровень».

Новый класс покемонов теперь выглядит как его:

Давайте создадим Charmander и проверим его текущие атрибуты:

Выход:

Теперь давайте прокачаем его до уровня 3:

И давайте проверим новую статистику:

Выход:

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

Часть № 5 — Принцип замещения Лискова

Принцип Лискова — это эмпирическое правило проектирования наследования между классами. В основном это выглядит так:

Допустим, у меня есть два класса, родительский класс (Pokémon) и дочерний класс (Evolve1).

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

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

Например, дочерний класс Evolve1 имеет метод special_strike(), который имеет те же входные и выходные данные, что и метод strike() родительского класса.

Другими словами, они взаимозаменяемы, и поэтому здесь применяется принцип подстановки Лискова.

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

Классический пример нарушения принципа подстановки Лискова — пример Square, Rectangle:

Квадрат (дочерний элемент) и прямоугольник (родительский) Пример:

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

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

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

Квадрат

Таким образом, прямоугольник и квадрат принимают одни и те же аргументы (h и w) и могут печатать свою площадь. Значит, если я их унаследовал, они должны принимать те же аргументы, верно?

Попробуем передать аргументы, w = 3 и h = 7.

Выход:

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

Вывод состоит в том, что Square и Rectangle не должны наследоваться друг от друга.

Часть № 6 — Сеттер и декоратор @property

В ООП важно добавить функциональность для изменения атрибутов после создания класса. Следовательно, мы должны добавить методы, которые позволяют пользователю изменять атрибуты, называемые методами set.

Давайте посмотрим на примеры лучших и плохих практик создания заданных методов:

  1. Пример изменения зарплаты сотрудника — Плохая практика:
  • Создание класса Rectangle
  • Создание прямоугольника с отрицательной шириной:

Выход:

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

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

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

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

  1. Пример изменения баланса клиента с помощью @property и setter — Best Practice:
  • Создание класса Клиент
  • Создание клиента с отрицательным балансом:

Выход:

Большой. Как и прежде, мы не можем создать клиента с отрицательным балансом. Теперь давайте попробуем создать клиента с положительным балансом и изменить его на отрицательный.

  • Создание клиента с положительным балансом и изменение его на отрицательный:
  • Изменение баланса на отрицательный:

Выход:

Большой. Теперь python будет выдавать ошибку даже в том случае, если экземпляр был создан с положительной зарплатой и только потом изменился на отрицательную.

Вот и все, что касается второй части серии. Надеюсь, вы узнали что-то новое.

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