Перейти к содержанию

Лекция 1. Базовые понятия ООП. Инкапсуляция, наследование, полиморфизм (OOP Basics)

История развития ООП

Термины «объектно-» и «ориентированный» в современном смысле появились в MIT в конце 1950-х — начале 1960-х. В среде специалистов по искусственному интеллекту термин «объект» мог относиться к идентифицированным элементам (атомы Lisp) со свойствами. Алан Кэй позже писал, что понимание внутреннего устройства Lisp оказало серьёзное влияние на его мышление в 1966 г. Другой ранний пример ООП в MIT — Sketchpad Ивана Сазерленда (1960–61), где были определены «объект» и «экземпляр» в контексте графического представления.

Объекты как формализованный концепт появились в Simula 67 — модернизированной версии Simula I, языка дискретно-событийного моделирования (Оле-Йохан Даль, Кристен Нюгорд, Норвежский компьютерный центр). Simula включала классы, экземпляры, подклассы, виртуальные методы, сопрограммы, автоматическую сборку мусора. Идеи Simula серьёзно повлияли на Smalltalk, Lisp (CLOS), Object Pascal, C++.

Smalltalk (Алан Кэй, Xerox PARC) фактически навязывал использование «объектов» и «сообщений» как базиса для вычислений. В отличие от Simula, Smalltalk разрабатывался как полностью динамичная система — классы можно создавать и изменять во время выполнения.

В начале и середине 1990-х ООП стало доминирующей методологией программирования: Visual FoxPro, C++, Delphi, позже — Java и C#. Этому способствовал и рост популярности GUI: пример тесной связи библиотеки GUI и ООП-языка — фреймворк Cocoa на macOS, написанный на Objective-C.

Возможности ООП добавлялись и в существующие языки — Ada, BASIC, Fortran, Pascal, что часто приводило к проблемам совместимости. Позже появились языки, поддерживающие и объектно-ориентированный, и процедурный подходы — Python, Ruby. Самые коммерчески успешные ООП-языки — Java, C#, C++.

Современные системные языки, такие как Go (Golang) и Rust, идут другим путём — поддерживают объектно-ориентированные идеи (инкапсуляция, полиморфизм) частично, но отказываются от классического наследования в пользу композиции и интерфейсов.

Базовые понятия: класс, объект, интерфейс

Класс

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

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

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

Атрибуты класса «Автомобиль» — двигатель, подвеска, кузов, четыре колеса. Методы — «открыть дверь», «нажать на педаль газа», «закачать порцию бензина из бензобака в двигатель». Первые два метода доступны другим классам (в частности, классу «Водитель»). Последний описывает внутреннее взаимодействие и не доступен пользователю.

Объект

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

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

Если класс — это абстрактный автомобиль из «мира идей», то объект — конкретный автомобиль, стоящий под окнами.

Интерфейс

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

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

Интерфейсом класса является набор всех его публичных методов в совокупности с публичными атрибутами. Интерфейс специфицирует класс — чётко определяет все возможные действия над ним.

При описании интерфейса важно соблюсти баланс между гибкостью и простотой. Простой интерфейс легко использовать, но не все задачи он позволит решить. Гибкий интерфейс мощнее, но сложнее в освоении и более подвержен ошибкам. Машина с коробкой-автоматом против пассажирского самолёта — иллюстрация этого баланса.

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

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

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

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

Инкапсуляция связана с понятием интерфейса класса: всё то, что не входит в интерфейс, инкапсулируется в классе.

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

Полиморфизм

Любое обучение вождению не имело бы смысла, если человек, научившийся водить ВАЗ 2106, не мог потом водить ВАЗ 2110 или BMW X3. Все автомобили имеют один и тот же интерфейс — водитель, абстрагируясь от сущности автомобиля, работает именно с этим интерфейсом.

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

Если вы читаете данные из файла, в классе файлового потока есть метод read(n: int) -> bytes. Если потом данные нужно считать из сокета — в классе сокета тоже будет метод read. Достаточно заменить объект одного класса на объект другого — и логика работает.

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

Представим, что мы — инженеры автомобильного завода. Задача — разработать новую модель. Мы не проектируем с нуля, а берём за основу предыдущее поколение и вносим изменения: добавляем гидроусилитель руля, противотуманки, изменяем форму кузова. Все три модификации сохраняют большинство свойств предыдущей модели (двигатель, ходовая часть, коробка), но добавляют новую функциональность. Это и есть наследование.

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

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

ООП в Python

Python — типичный представитель ООП-семейства с элегантной и мощной объектной моделью. Даже числа в Python являются объектами.

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

class SomeClass:
    """Базовое определение класса."""

    # атрибуты класса
    attr1 = 42
    attr2 = "Hello, World"

    def method1(self, x):
        """Метод. self — ссылка на объект, в контексте которого вызывается метод."""
        return 2 * x

В современном Python 3 можно опускать (object) после имени класса — все классы автоматически наследуются от object.

Экземпляр класса

obj = SomeClass()       # создание экземпляра
print(obj.method1(6))   # 12
print(obj.attr1)        # 42

Можно создавать экземпляры с заранее заданными параметрами через инициализатор __init__:

class Point:
    def __init__(self, x: float, y: float, z: float):
        self.coord = (x, y, z)

p = Point(13, 14, 15)
print(p.coord)  # (13, 14, 15)

Статические и классовые методы

Статический метод не использует других атрибутов класса. Создаётся декоратором @staticmethod:

class SomeClass:
    @staticmethod
    def hello():
        print("Hello, world")

SomeClass.hello()   # без создания экземпляра
SomeClass().hello() # из экземпляра — тоже работает

Метод класса выполняется в контексте самого класса, а не экземпляра. Требует ссылку на класс (cls):

class SomeClass:
    @classmethod
    def hello(cls):
        print(f"Hello, класс {cls.__name__}")

SomeClass.hello()  # Hello, класс SomeClass

Специальные методы (dunder methods)

Помимо __init__, Python предоставляет метод __new__ — он непосредственно создаёт экземпляр класса. Первым параметром принимает ссылку на класс. Полезен, например, для реализации паттерна Singleton:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)  # True

Деструктор __del__ вызывается при удалении объекта. На практике используется редко — в основном для ресурсов, требующих явного освобождения (например, закрытие файлов).

Объект как функция: __call__

class Multiplier:
    def __call__(self, x, y):
        return x * y

multiply = Multiplier()
print(multiply(19, 19))  # 361

Имитация контейнеров

class Collection:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)

    def __getitem__(self, key):
        return self.items[key]

    def __contains__(self, value):
        return value in self.items

c = Collection([1, 2, 3])
print(len(c))     # 3
print(c[0])       # 1
print(2 in c)     # True

Имитация числовых типов

class Money:
    def __init__(self, value):
        self.value = value

    def __mul__(self, n):
        return Money(self.value * n)

    def __repr__(self):
        return f"Money({self.value})"

m = Money(42)
print(m * 100)  # Money(4200)

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

Все объекты в Python инкапсулируют внутри себя данные и методы, предоставляя публичные интерфейсы. Реального скрытия в Python нет — есть соглашения:

  • _name (одно подчёркивание) — атрибут считается «приватным» (на уровне договорённости).
  • __name (два подчёркивания) — name mangling: атрибут переименуется в _ClassName__name, но к нему всё ещё можно достучаться напрямую.
class SomeClass:
    def __init__(self):
        self.__value = 42  # name-mangled

obj = SomeClass()
# obj.__value           # AttributeError
print(obj._SomeClass__value)  # 42 — но так делать не нужно

Для контролируемого доступа к атрибутам используется декоратор @property:

class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Температура ниже абсолютного нуля")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

t = Temperature(20)
print(t.fahrenheit)  # 68.0
t.celsius = 25       # вызывается setter с проверкой

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

Одиночное наследование:

class Mammal:
    class_name = "Mammal"

class Dog(Mammal):
    species = "Canis lupus"

dog = Dog()
print(dog.class_name)  # Mammal
print(dog.species)     # Canis lupus

Множественное наследование (Python разрешает; MRO — Method Resolution Order — определяет порядок поиска методов):

class Horse:
    is_horse = True

class Donkey:
    is_donkey = True

class Mule(Horse, Donkey):
    pass

m = Mule()
print(m.is_horse, m.is_donkey)  # True True

Множественное наследование позволяет создавать миксины (mixins) — небольшие классы, добавляющие определённую функциональность. Но большинство современных языков (Java, C#, Go) от множественного наследования отказались — оно усложняет MRO и может приводить к «алмазной проблеме».

Композиция (ассоциация) вместо наследования

Кроме наследования есть композиция (агрегация) — один класс является полем другого. В современной разработке часто рекомендуется предпочитать композицию наследованию (принцип «composition over inheritance»):

class Salary:
    def __init__(self, pay):
        self.pay = pay

    def total(self):
        return self.pay * 12

class Employee:
    def __init__(self, pay, bonus):
        self.salary = Salary(pay)  # композиция
        self.bonus = bonus

    def to_pay(self):
        return self.salary.total() + self.bonus

emp = Employee(100, 10)
print(emp.to_pay())  # 1210

Полиморфизм в Python (утиная типизация)

Все методы в Python виртуальны — дочерние классы могут переопределять методы базового:

class Animal:
    def move(self):
        print("Двигается")

class Hare(Animal):
    def move(self):
        print("Прыгает")

class Snake(Animal):
    def move(self):
        print("Ползёт")

for a in [Animal(), Hare(), Snake()]:
    a.move()

Полиморфизм работает и для классов, не связанных наследованием — это утиная типизация (duck typing): «если что-то выглядит как утка, крякает как утка — значит, это утка».

class English:
    def greet(self):
        print("Hello")

class French:
    def greet(self):
        print("Bonjour")

def intro(speaker):
    speaker.greet()  # не важно, какого типа speaker — лишь бы был .greet()

intro(English())  # Hello
intro(French())   # Bonjour

Доступ к методам класса-предка через super():

class Parent:
    def __init__(self):
        print("Parent init")

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

class Child(Parent):
    def __init__(self):
        super().__init__()

    def method(self):
        super().method()
        print("Child addition")

c = Child()      # Parent init
c.method()       # Parent method / Child addition

ООП в Go

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

Тип-структура и методы

package main

import "fmt"

type Point struct {
    X, Y, Z float64
}

// Метод определяется снаружи типа — через receiver.
func (p Point) Distance() float64 {
    return p.X*p.X + p.Y*p.Y + p.Z*p.Z
}

// Метод с указателем как receiver — позволяет менять состояние.
func (p *Point) Scale(k float64) {
    p.X *= k
    p.Y *= k
    p.Z *= k
}

func main() {
    p := Point{X: 1, Y: 2, Z: 3}
    fmt.Println(p.Distance())  // 14
    p.Scale(2)
    fmt.Println(p)             // {2 4 6}
}

Инкапсуляция через регистр имени

В Go доступность определяется первой буквой идентификатора:

  • BigName (с заглавной) — экспортируется (publicly visible) за пределами пакета;
  • smallName (со строчной) — приватно (package-private).
type Account struct {
    Owner   string  // экспортируется
    balance float64 // приватное поле
}

func (a *Account) Deposit(amount float64) {
    a.balance += amount
}

func (a *Account) Balance() float64 {
    return a.balance
}

Композиция через встраивание

Вместо наследования Go использует встраивание (struct embedding):

type Animal struct {
    Name string
}

func (a Animal) Move() {
    fmt.Printf("%s движется\n", a.Name)
}

type Dog struct {
    Animal       // встраивание — поля и методы Animal доступны напрямую
    Breed string
}

func main() {
    d := Dog{Animal: Animal{Name: "Бобик"}, Breed: "Лайка"}
    d.Move()           // Бобик движется — метод Animal "поднялся" в Dog
    fmt.Println(d.Name) // Бобик
}

Полиморфизм через интерфейсы

Интерфейс в Go — набор сигнатур методов. Любой тип, реализующий все методы, неявно удовлетворяет интерфейсу:

type Greeter interface {
    Greet()
}

type English struct{}
func (English) Greet() { fmt.Println("Hello") }

type French struct{}
func (French) Greet() { fmt.Println("Bonjour") }

func intro(g Greeter) {
    g.Greet()
}

func main() {
    intro(English{})  // Hello
    intro(French{})   // Bonjour
}

Это аналог утиной типизации Python, но проверяется во время компиляции.

Пустой интерфейс — any

var x any = 42         // Go 1.18+; в более старых версиях — interface{}
x = "string"
x = []int{1, 2, 3}

any (или interface{} в старом синтаксисе) принимает значения любого типа — близкий аналог object в Python.

Сравнение ООП-подходов: Python vs Go

Аспект Python Go
Классы Есть Нет — есть структуры с методами
Наследование Множественное, через MRO Нет — есть встраивание (композиция)
Полиморфизм Утиная типизация (runtime) Интерфейсы (compile-time)
Инкапсуляция Соглашения (_, __), property Регистр имени (Big / small)
Динамическое изменение Полное (метаклассы, monkey-patching) Невозможно — типы фиксируются на этапе компиляции
Скорость Медленнее (интерпретатор) Быстрее (компиляция в нативный код)
Сборка мусора Подсчёт ссылок + GC Конкурентный GC

Оба подхода жизнеспособны и применяются в проде. Выбор зависит от задач: Python — там, где нужна скорость разработки и гибкость (Data Science, web, скрипты); Go — там, где нужна предсказуемость, скорость выполнения и параллелизм (микросервисы, инфраструктура, CLI).


Контрольные вопросы

  • Что такое класс? Что такое объект? Чем они отличаются?
  • Что такое интерфейс класса?
  • Сформулируйте принципы инкапсуляции, наследования и полиморфизма.
  • В чём различие подхода к ООП в Python и в Go?
  • Что такое утиная типизация и где она применяется?
  • Что такое «композиция вместо наследования» и почему так часто рекомендуют?