Программирование и IT

Python: ООП

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

Обновлено
В этой статье

Наследование: переиспользовать, а не копировать#

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

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def pay(self):
        return self.salary

class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus

    def pay(self):
        return super().pay() + self.bonus

m = Manager("Пётр", 100, 50)
print(m.pay(), m.name)        # 150 Пётр
print(isinstance(m, Employee))  # True

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

Проверять тип нужно через isinstance, а не type(x) == Employee: второе
даст False для наследника и сломает всю идею.

Полиморфизм и утиная типизация#

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

class Dog:
    def speak(self):
        return "гав"

class Cat:
    def speak(self):
        return "мяу"

for animal in (Dog(), Cat()):
    print(animal.speak(), end=" ")   # гав мяу

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

Когда общий контракт всё-таки хочется зафиксировать, берут модуль abc:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

print(Square(4).area())   # 16
Shape()
  # TypeError: Can't instantiate abstract class Shape without an implementation
  # for abstract method 'area'

Инкапсуляция: подчёркивания вместо private#

Ключевых слов private и protected в языке нет. Есть соглашение и один
технический приём.

  • name — открытый атрибут, часть интерфейса.
  • _name — внутренний; трогать снаружи не следует, но язык не запрещает.
  • __name — имя искажается: внутри класса Account оно превращается в
    _Account__name.
class Account:
    def __init__(self, balance):
        self.__balance = balance

    @property
    def balance(self):
        return self.__balance

a = Account(500)
print(a.balance)           # 500
print(a._Account__balance) # 500 — «приватность» обходится одной строкой
a.balance = 0
  # AttributeError: property 'balance' of 'Account' object has no setter

Двойное подчёркивание придумано не для защиты от программиста, а чтобы имя
случайно не перекрылось в наследнике. Настоящий способ ограничить доступ —
@property без сеттера: атрибут читается, но не присваивается.

Композиция вместо длинных иерархий#

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

class Engine:
    def start(self):
        return "мотор запущен"

class Car:
    def __init__(self):
        self.engine = Engine()      # машина ИМЕЕТ мотор

    def start(self):
        return self.engine.start()

print(Car().start())   # мотор запущен

Простое правило выбора: «является» — наследование (Manager является
Employee), «имеет» — композиция (Car имеет Engine). Множественное
наследование в Python разрешено, порядок поиска методов виден в
Класс.__mro__, но в прикладном коде его почти всегда стоит избегать.

Практика: опиши базовый класс Shape с абстрактным area, наследников
Circle и Rect, сложи их в список и выведи общую площадь через
sum(s.area() for s in shapes). Устройство самого класса — в статье про
классы, а перебор без списка — в статье про
генераторы.

План по этапам

  1. Родитель и наследникНаписать базовый класс с двумя полями и наследника, который добавляет третье через super().__init__.
  2. Переопределить методЗаменить метод в наследнике и вызвать родительскую версию через super().
  3. Список разнородных объектовСложить объекты двух несвязанных классов в список и вызвать у всех одноимённый метод.
  4. Зафиксировать контрактСделать базовый класс через abc и убедиться, что создать его напрямую нельзя.
  5. Переписать на композициюВзять иерархию из трёх уровней и заменить один уровень полем-объектом.

Начать изучать эту тему у себя

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

Начать план

Проверь себя

1.Manager наследует Employee. Что вернёт isinstance(Manager(), Employee)?

2.У Employee.pay() возвращается 100, у Manager.pay() — super().pay() + 50. Что вернёт Manager().pay()?

3.Внутри класса Account атрибут объявлен как self.__balance. Под каким именем он доступен снаружи?

4.Какое отношение лучше описать композицией, а не наследованием: «Car имеет Engine» или «Manager является Employee»?

Источники

Было полезно?

Ещё темы

Программирование и IT Как научиться программировать с нуля Научиться программировать самостоятельно можно без технического образования, если с первой недели писать код руками, а не только смотреть уроки. Ниже — как выбрать первый язык, что установить, в каком порядке идти по темам и как понять, что ты действительно продвинулся. Программирование и IT Как изучить Python с нуля Python хорош для первого знакомства с программированием: код читается почти как текст, а стандартная библиотека закрывает большинство бытовых задач. Этот план ведёт от установки интерпретатора до собственных скриптов, покрытых тестами, — примерно за четыре месяца при занятиях около часа в день. Программирование и IT Python: задачи с решениями Подборка задач по возрастанию сложности — от разворота строки до генератора и декоратора-счётчика. Сначала решай сам и только потом открывай разбор: сравнение своего кода с чужим даёт больше, чем чтение готового. Все решения проверены на Python 3. Программирование и IT Python: срезы Срез вырезает из последовательности кусок по правилу «с какого индекса, до какого, с каким шагом». Запись компактная, но у неё два места, где почти все ошибаются: правая граница не включается, а при отрицательном шаге границы меняются местами. Разберём оба. Программирование и IT Python: генераторы Генератор выдаёт значения по одному и только тогда, когда их просят. Он не хранит всю последовательность в памяти, поэтому подходит для больших файлов и бесконечных потоков. Плата за это — генератор одноразовый: пройти по нему дважды не получится. Программирование и IT Python: декораторы Декоратор — это функция, которая принимает другую функцию и возвращает новую, с добавленным поведением. Знак `@` перед определением — просто короткая запись присваивания. Если держать это в голове, вся тема разбирается за один вечер.

Ещё сценарии