Объектно-ориентированное программирование. Классы и объекты
Сегодня мы поговорим об объектно-ориентированном программировании и о его применении в python.
Объектно-ориентированное программирование (ООП) — парадигма программирования, в которой основными концепциями являются понятия объектов и классов.
Класс — тип, описывающий устройство объектов. Объект — это экземпляр класса. Класс можно сравнить с чертежом, по которому создаются объекты.
Python соответствует принципам объектно-ориентированного программирования. В python всё является объектами — и строки, и списки, и словари, и всё остальное.
Но возможности ООП в python этим не ограничены. Программист может написать свой тип данных (класс), определить в нём свои методы.
Это не является обязательным — мы можем пользоваться только встроенными объектами. Однако ООП полезно при долгосрочной разработке программы несколькими людьми, так как упрощает понимание кода.
Приступим теперь собственно к написанию своих классов на python. Попробуем определить собственный класс:
Теперь мы можем создать несколько экземпляров этого класса:
File
Классу возможно задать собственные методы:
И напоследок еще один пример:
Для вставки кода на Python в комментарий заключайте его в теги
- Модуль csv - чтение и запись CSV файлов
- Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
- Онлайн-обучение Python: сравнение популярных программ
- Книги о Python
- GUI (графический интерфейс пользователя)
- Курсы Python
- Модули
- Новости мира Python
- NumPy
- Обработка данных
- Основы программирования
- Примеры программ
- Типы данных в Python
- Видео
- Python для Web
- Работа для Python-программистов
- Сделай свой вклад в развитие сайта!
- Самоучитель Python
- Карта сайта
- Отзывы на книги по Python
- Реклама на сайте
Объектно-ориентированное программирование в Python для чайников

ООП в Python имеет много особенностей, что делает его непохожим на другие языки. С одной стороны Python очень простой язык и делать на нём программы в ООП-стиле легко, но с другой стороны — в нём нет привычных вещей, которые сейчас являются «стандартным набором» любого ООП-подхода. Если вы новичок в программировании, то эта статья для вас.
Понятие объекта и класса
Объектно-ориентированное программирование — это такой способ программирования, где вместо манипулирования функциями и переменными, используется объект, который содержит в себе функции и переменные. Такой подход позволяет упростить в первую очередь поддержку кода: если в программе сотни функций и переменных, то легко что-то «сломать». Если же сгруппировать их по объектам по функциональности, то это упростит сам код и его структуру.
Для того, чтобы создать объект, следует определить его тип. Тип переменной для объектов называется классом. Рассмотрим такой пример:
# объявим класс class A(): # метод класса def out(self, t): print(t) # создание экземпляра класса a = A() # использование метода класса a.out('Hello!') # Hello!
Класс объявляется с помощью ключевого слова class . Всё что в его блоке будет относиться к нему. Внутри класса мы можем определить произвольные функции, которые называются методами. Для методов используется привычное def , но первым аргументом должен указываться self — это ссылка на текущий объект (об этом ниже). Если метод имеет другие аргументы, то они указываются после self как обычно.
Сам по себе класс — это тип данных, который объявлен пользователем. Он точно такой же как и стандартные типы, вроде строк или чисел. Для того, чтобы использовать класс, как и любой другой тип данных, следует создать переменную.
В нашем примере переменная a и есть объект класса A .
В других языках для создания объектов часто используются специальное ключевое слово, например new , но в Python создание объекта синтаксически оформляется точно также, как и обычных функций — со скобками.
После того, как объект создан, можно его использовать. В примере используется вызов метода out() . Обратите внимание, что хотя в классе первым аргументом стоит self , при вызове методов его не нужно указывать, то есть как будто бы его нет.
Это также особенность Python, которую следует запомнить.
Класс формирует свою область видимости, поэтому чтобы вызвать метод, нужно вначале указать его класс. Для больших программ, где много функций, их группировка по классам, упростит жизнь многих программистов.
Кроме методов, класс может хранить и собственные переменные, которые называются полями . рассмотрим ещё один пример:
class Fig(): def set(self, x, y): self.x = x self.y = y def square(self): print(self.x * self.y) rect = Fig() rect.set(10, 20) rect.square() # 200
Метод set() принимает два аргумента (не считая self), которые присваиваются полям класса. Поля класса задаются через self . Назначение self очень простое — она указывает на текущий объект. То есть когда мы объявляем переменную объекта, то self принимает на него ссылку. Поскольку объектов одного класса может быть множество, то self будет гарантировать данные только текущего объекта.
Строго говоря имя self может быть произвольным. Просто так принято в Python.
Соответственно, когда нам нужно получить значение поля, мы опять же его получаем через self .
Понятие инкапсуляции
Слово инкапсуляция переводится in capsula — «в капсуле», то есть внутри. Сокрытие внутри класса/объекта методов и полей — это и есть инкапсуляция — одного из ключевых понятий объектно-ориентированного программирования.
Всё работает автоматом, поэтому нам не о чем беспокоиться.
Однако следует заметить, что в понятие инкапсуляции также входит ограничение области видимости. Например объект может содержать методы или поля, которые не следует использовать явно. В других языках для этого используются специальные ключевые слова, например private, public, protected, но в Python такой возможности нет. Вместо этого используется соглашение в именовании методов и полей, которое указывает на то, что они скрытые и их не следует использовать.
Например в последнем примере мы используем rect.set(10, 20) для self.x и self.x . Но никто не мешает нам напрямую обратиться к этим полям:
rect = Fig() rect.x = 10 rect.y = 30 rect.square() # 300
В таких случаях говорят о том, что поля объекта x и y являются публичными, а значит могут изменяться вне класса. Если бы мы хотели их спрятать, то их следует переименовать в _x и _y . такая нотация будет означать, что поля защищены и не предназначены для изменения извне.
На самом деле к _x и _y можно будет обратиться напрямую, поскольку Python никак не проверяет видимость переменной — это всего лишь соглашение между программистами. Если бы мы хотели ещё больше скрыть поля и методы, то их следует именовать с двойного подчеркивания — в этом случае Python выкинет ошибку.
class Fig(): def set(self, x, y): self.__x = x self.__y = y def square(self): print(self.__x * self.__y) rect = Fig() # при прямом обращении будет ошибка # rect.__x = 10 # AttributeError: 'Fig' object has no attribute '_Fig__x' # rect.__y = 30 # AttributeError: 'Fig' object has no attribute '_Fig__y' # а так можно rect.set(10, 20) rect.square() # 200
Конструктор и «магические методы»
В Python есть методы, которые начинаются и заканчиваются с двойного подчеркивания — это т.н. магические методы, которые имеют специальное назначение. Например есть метод __init__() , который выполняется в момент инициализации (создания) объекта и называется конструктор.
В конструкторе как правило размещают начальные значения полей, а также выполняют какие-то начальные действия при создании объекта.
В нашем примере в классе Fig нужно вначале вызвать set() , чтобы задать начальные значения переменных. Если мы попробуем сразу вызвать rect.square() , то получим ошибку Python, поскольку поля объекта ещё не определены.
Поэтому можно сделать некое дефолтное значение полей в конструкторе.
class Fig1(): def __init__(self): self._x = 0 self._y = 0 def set(self, x, y): self._x = x self._y = y def square(self): print(self._x * self._y) rect = Fig1() rect.square() # 0
При вызове rect.square() мы получим значение с дефолтными значениям и ошибки не будет.
Однако часто используют немного другой приём — можно сразу указать аргументы при создании объекта. Делается это в конструкторе.
class Fig2(): def __init__(self, x = 0, y = 0): self._x = x self._y = y def set(self, x, y): self._x = x self._y = y def square(self): print(self._x * self._y) rect = Fig2(20, 40) rect.square() # 800 rect.set(10, 20) rect.square() # 200
Такой подход позволяет указывать аргументы при создании объекта, а при необходимости изменить их с помощью отдельного метода (называется «сеттер» — от set).
В Python также есть другие магические методы, но они достаточно специфичные, поэтому сейчас не рассматриваем.
Наследование
Наследование ещё одна фундаментальная концепция ООП. Она позволяет создавать классы на основе других. Например есть некий класс, которые нужно расширить и добавить новые методы. Вместо того, чтобы изменять исходный класс (а если это вообще чужой модуль?), можно просто воспользоваться наследованием.
class X(): def out(self, s): print(s) class Y(X): pass # любые другие методы y = Y() y.out('hello!') # hello!
Класс Y создан на основе класса X — это указывается в скобках после названия класса. То есть класс X будет родительским, а Y — дочерним. При этом в классе Y автоматически будут доступны все методы и поля класса X .
Такой механизм и называется наследованием.
Python поддерживает множественное наследование — это когда можно указать сразу несколько родительских классов. В программировании множественное наследование не особо приветствуется из-за возможных коллизий, но в Python такая возможность есть.
Полиморфизм
Полиморфизм — ещё одна концепция программирования, которая переводится как «много форм» — это возможность функции работать с разными типами данных. Существует несколько видов полиморфизма и в разных языках используются разные реализации.
Например функция print() может работать с разными типами:
print(120) print([1, 2, 3]) print('hello') print(1.234)
То есть когда функция может принимать аргументы разных типов — она считается полиморфной. По большому счёту есть два основных вида полиморфизма: Ad-hoc и Параметрический.
- Ad-hoc полиморфизм — функция ведет себя по разному для аргументов разных типов. Например если функции передать число, то она посчитает его квадрат, а если строку, то сделает её реверс. Разное поведение в зависимости от входящего типа данных.
- Параметрический полиморфизм — функция ведет себя одинаково для аргументов разных типов. Например print() выводит данные разных типов на экран.
Также полиморфной может быть переменная, но в Python динамическая типизация, поэтому переменные могут быть любого типа.
Полиморфизм может быть частью языка, либо нет. Например в Pascal можно объявить несколько одноименных функций, но с разными аргументами и компилятор сам определит какая функция будет выполняться, в зависимости от входящих аргументов.
Но в Python такой возможности нет, поэтому реализация полиморфной функции ложится на плечи программиста. К счастью такие задачи редки, но ради полноты изложения покажу пример функции, которая может вести себя по разному в зависимости от типа входящего аргумента. Для проверки типа используется функция isinstance() .
def add(x): if isinstance(x, str): print('Работаем со строками') elif isinstance(x, int): print('Работаем с целыми числами') elif isinstance(x, float): print('Работаем с дробными числами') add(10) # Работаем с целыми числами add('hello') # Работаем со строками add(1.3) # Работаем с дробными числами
Теперь перейдём к ООП.
Вызов метода — это по сути обычный вывод функции, поэтому как она себе будет вести зависит только от неё. Но в Python (да и в других языках) под полиморфизмом часто понимают приём, когда переписываются родительские методы.
class A(): def out(self): print('class A') class B(A): def out(self): print('class B') my = B() my.out() # class B
Обратите внимание, что метод out() был переписан в дочернем классе. Таким образом, в зависимости от потребности мы можем использовать заданный метод, но в разных классах.
Такое «переписывание» методов возможно из-за того, что в Python все методы виртуальные.
Более сложный пример, когда используется некий интерфейс в виде отдельной функции:
class A(): def out(self): print('class A') class B(): def out(self): print('class B') def my(t): t.out() a = A() b = B() my(a) # class A my(b) # class B
Здесь два объекта, которые имею одинаковые методы (или только метод). Функция my() в качестве аргумента принимает какой-то из этих объектов и выполняет его метод. Таким образом в функции my() будет выполнять разные задачи в зависимости от входящего объекта.
Строго говоря, такое поведение не самое идеальное, но это из-за того, что в Python нет возможности создавать полноценные интерфейсы и выполнять какой-то контроль типов данных, как это делается в других языках. Поэтому, когда мы говорим о полиморфизме в Python, то ограничиваемся такими примитивами.
Статичные методы
Иногда стоит задача использовать методы класса без создания объекта. Например класс — это набор однотипных функций и можно упростить их вызов. В таких случаях говорят о статичных методах. В Python для этого используется декоратор @staticmethod .
class MyClass: @staticmethod def static1(x): print('hello', x) MyClass.static1(42) # hello 42
Заметьте, что мы сразу вызываем метод без создания объекта. Именно поэтому в методе static1() нет первого аргумента self — просто нет объекта и не на что ссылаться.
Однако, если всё-таки в классе нужно использовать какие-то поля, либо получить доступ к другим методам этого же класса, то используется другой декоратор @classmethod .
class MyRound: pi = 3.14 @classmethod def go(cls, x): print(cls.pi * x) MyRound.go(10) # 31.400000000000002
То есть метод фактически статичный, но при этом есть доступ к самому классу.
В других языках статичные методы объявляются с помощью специального ключевого слова (static), но в Python такой возможности нет, поэтому используются декораторы в качестве «заменителей».
Поля класса и поля объекта
Python поддерживает динамическое создание полей класса. Например:
class D(): pass d = D() d.id = 42 print(d.id) # 42
Обратите внимание, что в классе D нет поля id , но мы можем его создать после создания объекта и после использовать как ни в чём не бывало.
class L(): pass L.id = 64 print(L.id) # 64
Здесь для класса L даже не создаётся объект, но мы можем создать поле класса.
Эти примеры показывают, что Python разделяет понятия поле класса и поле объекта (экземпляра). Когда мы создаём объект, то его поля доступны через self , а вот поля класса создаются как обычные переменные.
Вот пример для изучения:
class A(): y = 10 def __init__(self, x): self.x = x A.y = 200 print(A.y) # 200 b = A(30) print(b.x) # 30 print(b.y) # 200
В классе A поле y объявлено как поле класса, поэтому у него нет self . И поэтому мы можем обратиться к этому полю даже без создания объекта.
После создания объекта b мы можем обратиться не только к self.x , который является полем экземпляра, но и к y (поле класса).
Такая возможность Python проистекает из особенностей его интерпретатора и скорее является побочным эффектом. С точки зрения программирования динамическое создание полей, является плохой практикой, поэтому её следует избегать. Но знать об этой особенности Python всё-таки следует.
Паттерны проектирования классов на Python
Хорошей практикой программирования считается использование композиции, а не наследования. Это когда объекты используют другие объекты в разных вариантах. Ради интереса посмотрите несколько шаблонов, которые я делал для PHP. Все эти паттерны являются «стандартными» в программировании и их можно повторить на Python. Другой вопрос — нужно ли?
Дело в том, что такие паттерны нужны больше для теоретического изучения основ ООП и языка, где много завязано на «классический» ООП, за который можно взять язык Java. Но в Python отсутствуют огромные пласты возможностей, поэтому изучение теории объектно-ориентированного программирования на Python, на мой взгляд, лишено смысла.
Подход Python — это краткость и практичность. И если стоит задача написать программу, то его ООП хватит с головой. Да, такой код будет сильно отличаться от «теоретического», но зато будет простым и компактным.
Объектно-ориентированное программирование в Python
Python — мультипарадигмальный язык программирования. Он поддерживает разные подходы к программированию.
Один из популярных подходов к решению проблем — создание объектов. Это называется объектно-ориентированным программированием (ООП).
У объекта есть две характеристики:
Рассмотрим пример. Допустим, наш объект — это попугай. У попугая есть такие свойства:
- Имя, возраст, цвет. Это атрибуты.
- То, как попугай поет и танцует. Это поведение.
ООП предлагает писать код, который можно использовать повторно. Такой принцип называется DRY (don’t repeat yourself, «не повторяйся»).
Класс
Класс — это шаблон объекта.
Вернемся к нашему попугаю. Если мы схематично нарисуем его на бумаге, такой набросок будет являться классом. По нему можно сделать, например, чучело попугая.
Давайте создадим класс, который описывает попугая:
class Parrot: pass
Для объявления класса Parrot мы использовали ключевое слово class . Из классов мы получаем экземпляры, созданные по подобию этого класса.
Объект
Объект — это экземпляр класса. Объявленный класс — это лишь описание объекта: ему не выделяется память.
Например, экземпляра класса Parrot будет выглядеть так:
# obj — экземпляр класса Parrot obj = Parrot()
Теперь разберемся, как написать класс и его объекты.
# Создаем класс и его объекты class Parrot: # атрибуты класса species = "птица" # атрибуты экземпляра def __init__(self, name, age): self.name = name self.age = age # создаем экземпляра класса kesha = Parrot("Кеша", 10) cookie = Parrot("Куки", 15) # получаем доступ к атрибутам класса print("Кеша — <>".format(kesha.__class__.species)) print("Куки тоже <>".format(cookie.__class__.species)) # получаем доступ к атрибутам экземпляра print("<> — <>-летний попугай".format(kesha.name, kesha.age)) print("<> — <> летний попугай".format(cookie.name, cookie.age))
Вывод:
Кеша — птица
Куки тоже птица
Кеша — 10-летний попугай Куки — 15-летний попугай
Мы создали класс Parrot . После этого мы объявили атрибуты — характеристики объекта.
Атрибуты объявлены внутри класса — в методе __init__ . Это метод-инициализатор, который запускается сразу же после создания объекта.
После этого мы создаем экземпляры класса Parrot . kesha и cookie — ссылки на (значения) наши новые объекты.
Получить доступ к атрибуту класса можно так — __class__.species . Атрибуты класса для всех экземпляров класса одинаковы. Точно так же мы можем получить доступ к атрибутам экземпляра — kesha.name и kesha.age . Но вот атрибуты каждого экземпляра класса уникальны.
Подробнее о классах и объектах.
Методы
Методы — функции, объявленные внутри тела класса. Они определяют поведения объекта.
# Создаем метод class Parrot: # атрибуты экземпляра def __init__(self, name, age): self.name = name self.age = age # метод экземпляра def sing(self, song): return "<> поет <>".format(self.name, song) def dance(self): return "<> танцует".format(self.name) # создаем экземпляр класса kesha = Parrot("Кеша", 10) # вызываем методы экземпляра print(kesha.sing("песенки")) print(kesha.dance())
Вывод:
Кеша поет песенки
Кеша танцует
В этой программе мы объявили два метода: sing() и dance() . Они являются методами экземпляра, потому что они вызываются объектами — например, kesha .
Наследование
Наследование — способ создания класса. Его суть заключается в том, что функциональность нового класса наследуются от уже существующего класса. Новый класс называется производным (дочерним). Существующий — базовым (родительским).
# родительский класс class Bird: def __init__(self): print("Птица готова") def whoisThis(self): print("Птица") def swim(self): print("Плывет быстрее") # дочерний класс class Penguin(Bird): def __init__(self): # вызов функции super() super().__init__() print("Пингвин готов") def whoisThis(self): print("Пингвин") def run(self): print("Бежит быстрее") peggy = Penguin() peggy.whoisThis() peggy.swim() peggy.run()
Вывод:
Птица готова
Пингвин готов
Пингвин
Плывет быстрее
Бежит быстрее
В этой программе мы создаем два класса — Bird (родительский) и Penguin (дочерний). Дочерний класс наследует функции родительского. Это вы можете заметить по методу swim() .
Но и дочерний класс изменяет функциональность родительского. Это можно заметить по методу whoisThis() . Более того, мы расширяем функциональность родительского класса — создаем метод run() .
Также мы используем функцию super() внутри метода __init__() . Это позволяет запускать метод __init__() родительского класса внутри дочернего.
Инкапсуляция
Мы можем ограничить доступ к методам и переменным, что предотвратит модификацию данных — это и есть инкапсуляция. Приватные атрибуты выделяются нижним подчеркиванием: одинарным _ или двойным __ .
# Используем инкапсуляцию данных class Computer: def __init__(self): self.__maxprice = 900 def sell(self): print("Цена продажи: <>".format(self.__maxprice)) def setMaxPrice(self, price): self.__maxprice = price c = Computer() c.sell() # изменение цены c.__maxprice = 1000 c.sell() # используем функцию изменения цены c.setMaxPrice(1000) c.sell()
Вывод:
Цена продажи: 900
Цена продажи: 900
Цена продажи: 1000
Мы объявили класс Computer .
Затем мы использовали метод __init__() для хранения максимальной цены компьютера. Затем мы попытались изменить цену — безуспешно: Python воспринимает __maxprice как приватный атрибут.
Как видите, для изменения цены нам нужно использовать специальную функцию — setMaxPrice() , которая принимает цену в качестве параметра.
Полиморфизм
Полиморфизм — особенность ООП, позволяющая использовать одну функцию для разных форм (типов данных).
Допустим, нам нужно закрасить фигуру. Их форма может быть любой: прямоугольник, квадрат, круг. Одним и тем же методом мы можем раскрасить любую фигуру. Такой принцип и называется полиморфизмом.
# Используем полиморфизм class Parrot: def fly(self): print("Попугай умеет летать") def swim(self): print("Попугай не умеет плавать") class Penguin: def fly(self): print("Пингвин не умеет летать") def swim(self): print("Пингвин умеет плавать") # общий интерфейс def flying_test(bird): bird.fly() # создаем экземпляров класса kesha = Parrot() peggy = Penguin() # передача объектов в качестве аргумента flying_test(kesha) flying_test(peggy)
Вывод:
Попугай умеет летать
Пингвин не умеет летать
В этой программе мы объявили два класса: Parrot и Penguin . В каждом из них описан общий метод fly() . Но функции у них разные.
Для использование полиморфизма мы создали общий интерфейс — функцию flying_test() . В качестве аргумента она принимает любой объект, после чего происходит вызов его собственного метода fly() .
Что нужно запомнить:
- Объектно-ориентированное программирование упрощает понимание программы, делает ее эффективнее.
- Все классы являются общими — код можно использовать повторно.
- Все данные находятся в сохранности и защищены благодаря абстракции.
- Полиморфизм предоставляет общий интерфейс разным объектам, что делает код эффективнее.
СodeСhick.io - простой и эффективный способ изучения программирования.
2023 © ООО "Алгоритмы и практика"
Почему в Python плохой ООП
В Python ужасный ООП. Кликбейтный тезис, который имеет право на существование. Есть много языков, где ООП представлен не в самом классическом виде, ну или так принято считать. Прототипные JavaScript и Lua, Golang с прикреплением методов и прочие. Но «не такой как все» всегда ли синоним слова «неправильный»? С чего мы вообще вязли, что ООП в Python не такой каким должен быть ООП? Что вообще является точкой отсчёта «правильности» ООП? Smalltalk или Simula 67? Вроде бы объектно-ориентированное программирование – это просто парадигма.. или уже догма?
В этом статье мы попробуем понять:
- что не так с ООП в Python;
- чем его ООП отличается от языков с эталонной по мнению многих реализацией ООП: Java, C# и С++;
- почему разработчики языка Python решили сделать всё именно так.
Реализует этот текст автор YouTube-канала PyLounge Макс. Поехали!
Дисклеймер: В этой статье я хочу высказать свои "рассуждения на тему" и подчёркиваю, что не обладаю монополией на истину. Буду рад осудить альтернативное мнение в комментариях.

Для начала необходимо понять. Чем ООП в Python отличается от классической концепции и реализации в других ЯП.
Парадигма ООП появилась ещё 60-70-х годах XX века. ООП или Объектно-ориентированное программирование — это методология программирования, которая основана представлении программы в виде набора взаимодействующих объектов, каждый из которых является экземпляром класса, а классы образуют иерархию наследования.
Ключевыми особенностями ООП является понятия:
- абстракция;
- инкапсуляция;
- наследование;
- полиморфизм.
Алан Кэй, создателя языка Smalltalk, одним из «отцов-основателей» ООП, говорил, что ООП подход заключается в следующем наборе принципов:
- Всё является объектом.
- Вычисления осуществляются путём взаимодействия (обмена данными) между объектами, при котором один объект требует, чтобы другой объект выполнил некоторое действие.
- Каждый объект имеет независимую память, которая состоит из других объектов.
- Каждый объект является представителем класса, который выражает общие свойства объектов (таких, как целые числа или списки).
- В классе задаётся поведение (функциональность) объекта. Тем самым все объекты, которые являются экземплярами одного класса, могут выполнять одни и те же действия.
- Классы организованы в единую древовидную структуру с общим корнем, называемую иерархией наследования. Память и поведение, связанное с экземплярами определённого класса, автоматически доступны любому классу, расположенному ниже в иерархическом дереве.
«ООП для меня означает лишь обмен сообщениями, локальное сохранение, и защита, и скрытие состояния, и крайне позднее связывание». (c) Алан Кэй
Другими словами, в соответствии с идеями Алана Кэя, самыми важными ингредиентами ООП является:
- Передача сообщений (то есть взаимодействие).
- Инкапсуляция.
- Динамическое связывание.
Интересно, что указывается именно термин связывание, а терминов наследование и полиморфизм нет. Ведь полиморфизм бывает статический (раннее связывание) – это перегрузки и дженерики (шаблоны). То есть Кэй, человек, который считается изобретателем термина «ООП» не считал важными частями ООП наследование и полиморфизм. Получается пропорции условны, а границы размыты.
Ключевая идея ООП состоит в том, чтобы разделить проблему на подзадачи, которые можно решить с помощью отдельных объектов, взаимодействующих друг с другом. Это означает, что они сохраняют свой статус внутри, и они связаны с определенным множеством функций (методов) для работы с внутренним статусом и для связи с другими объектами.
Все остальное же было определено, когда появились объектно-ориентированные языки. Языки OO были разработаны, чтобы упростить подход к программированию. И они реализовали инструменты и функции для поддержки ООП — классы были одним из таких инструментов.
Под инкапсуляцией стали подразумевать возможность классов содержать данные и методы в себе, которые непосредственно связаны с этим классом по смыслу. При этом одни языки соотносят инкапсуляцию с сокрытием этой информации, а другие (Smalltalk, Eiffel, OCaml) различают эти понятия.
Например, в Java можно определить поле как приватное, и тогда оно будет видно только членам этого класса. Также работает и С++, однако там есть концепция друзей (friend), которые могут видеть приватные поля других классов, что сильно критикуется.
Наследование — свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствованной функциональностью.
Полиморфизм — это возможность обработки разных типов данных, т. е. принадлежащих к разным классам, с помощью "одной и той же" функции, или метода. На самом деле одинаковым является только имя метода, его исходный код зависит от класса. Поэтому в данном контексте под полиморфизмом понимается множество форм одного и того же слова – имени метода.
Абстрагирование (абстракция данных) означает выделение значимой информации и исключение из рассмотрения незначимой. В ООП рассматривают абстракцию данных, подразумевая набор наиболее значимых характеристик объекта, доступных остальной программе.
Класс — универсальный, комплексный тип данных, состоящий из тематически единого набора «полей» (переменных более элементарных типов) и «методов» (функций для работы с этими полями), то есть он является моделью информационной сущности с внутренним и внешним интерфейсами для оперирования своим содержимым (значениями полей).
В центре ООП находится понятие объекта. Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Объект — это экземпляр класса. Данные объекта содержатся в объекте, а не просто лежат внутри программы. Инкапсуляция включает в себя сокрытие (но им не является!).
ООП пытается сделать программное обеспечение похожим на «реальный мир», как его может понять обычный человек.
Современная идея ООП — это синтез всех их идей, а также идей Голдберга, Барбары Лисков, Дэвида Парнаса, Бертрана Мейера, Гюля Ага и других. Но никто из них не может утверждать, что же такое «ООП». Термины развиваются, как и задачи, которые изначально эти инструменты должны были решать.
А что же касаемо Python. Python полностью удовлетворяет всем перечисленным выше требования, а значит является «полностью объектно-ориентированным». ООП – просто стиль.
Однако дополнительные фишки ООП, которые стали ассоциироваться с ООП за счёт их реализации в других объектных языках, несколько отличаются тем, как они реализованы в Python.
Отсутствие модификаторов доступа
В Python отсутствует деление на публичные, защищённые, приватные свойства и методы. Многие вещи в Python основаны на соглашениях. Сокрытие данных реализуется чисто конвенционально. За счёт соглашения использовать подчёркивание у свойств и методов (защищённые члены). Да, можно использовать двойное подчёркивание, так называемый манглинг. Чисто технически это запрещает прямой доступ к данным и равносильно модификатору приват, но это скорее придуманный адептами классического ООП «грязный хак». Таким образом, в Python нет классического разделения на группы доступа, потому что Python доверяет разработчику. В этом плане Python ближе к С++.

«Да, я знаю, что ты можешь выстрелить себе в ногу, но я верю, что ты этого не сделаешь. Ведь не даром ты столько узнал, прежде чем приступить к написанию кода». (с) Python
Мне кажется, инкапсуляция не так полезна в языке с динамической типизацией. Выскажу непопулярное мнение – это не добавляет никакой безопасности, она просто дает ощущение безопасности. Если вы грамотный программист, то так или иначе сделаете всё как надо.
Но почему разработчики языка не добавили такой привычный «предохранитель»? Ответ кроется в философии Python. Гвидо не любит что-то скрывать. Как он выразился в одном интервью: «мы все здесь взрослые по обоюдному согласию. Python доверяет вам. Он говорит: «Эй, если хочешь чтобы ковыряться в темных местах, я надеюсь, что у тебя есть уважительная причина, и вы не создаете проблем». Этого тезиса мы ещё коснёмся ниже. Пока просто запомните.
Вообще инкапсуляция – это не совсем про сокрытие. Инкапсуляция определяется как «процесс объединения элементов данных и функций в единое целое, называемое классом» или «отделение реализации от описания». Таким образом, номинально в Python всё соблюдается более чем верно.
Отсутствие интерфейсов
В языке Python нет как таковой конструкции как интерфейс (interface). К слову в С++ их тоже нет. Но что в Python, что в С++, есть механизмы, позволяющие так или иначе использовать интерфейсы. Абстрактные классы – это хоть и немного другое, но функционалу отвечает и допускает некоторое упрощение концепции. На мой взгляд, отсутствие интерфейсов искусственный механизм избежания неоднозначности. Вот у тебя есть абстрактные классы, вот их и используй. С помощью абстрактных классов можно сделать всё тоже что и с интерфейсами, но не надо заморачиваться. Ведь Python идёт по пути простоты и убирает всё лишнее. Создатели языка даже конструкцию switch case выкинули, дабы "место не занимала".
Множественное наследование
Многие современные языки отказываются от множественного наследования, так как оно многое усложняет. Однако Python хоть и идёт по пути упрощения, но старается выкидывать избыточность, а не функциональность, ведь любое упрощение — это потеря гибкости + см. пункт про доверие своему разработчику. Python думает, что разработчик, который его использует достаточно умён, чтобы не плодить гигантскую иерархию и победить проблему ромба. Не доверился он разве что, при создании GIL. Но спишем это на ошибки молодости. Кстати, С++ также поддерживает множественное наследование. Так что с этим пунктом всё тоже в рамках закона.
Утиная типизация
Она, конечно, к теме относится косвенно. Но, тем не менее, рядом с Python всегда всплывает понятие утиной типизации.
Если что-то выглядит как утка, плавает как утка и крякает как утка, это наверняка и есть утка.
Утиная типизация заключается в том, что вместо проверки типа чего-либо в Python мы склонны проверять, какое поведение оно поддерживает, зачастую пытаясь использовать это поведение и перехватывая исключение, если оно не работает.
Тут во всей красе демонстрируется один из главных принципов Дзена Python — «явное лучше, чем неявное». Если что-то выглядит как утка и крякает, то это утка, к чему погружаться в экзистенциальные копания и вопросы самоопределения? Будь проще и посмотри пример.

Поскольку Duck и Human это разные классы, Python повторно вызывает функцию fly_quack() для экземпляра класса Human . И хотя класс Human имеет похожие методы quack и fly , типы объектов были разными и поэтому все работает правильно и вызываются верные методы.
Константные методы
Нет способов предотвратить изменение состояния класса методами класса (константные методы), снова всё полагается на добрую волю программиста.
Вообще докопаться ещё можно много до чего. Например, не совсем стандартное описание статических методов и свойств, параметр self, MRO и многое многое другое.
Но Python отвечает всем требованиям парадигмы ООП. Просто многие моменты выполнены не так как у всех. Но на то есть причины. Гвидо ван Россум при разработке дизайна языка мотивировался выработанным им Дзеном Python, где простое лучше, чем сложное, явное лучше не явного и т.д. Через эту философию красной нитью проходит структура всего языка Python.

Python задуман как гибрид. Вы можете писать в объектно-ориентированном или функциональном стилях. Отличительными чертами объектной ориентации являются абстракция, инкапсуляция, наследование и полиморфизм. Что из этого отсутствует в Python?
По мнению многих Smalltalk — самый чистый ООП язык, но что даёт и какова цена этой чистоты? Можно написать очень хороший объектно-ориентированный код как на Smalltalk, так и на Python.
Python прагматичен. Вводятся концепции, представляющие ценность для разработчика, без особого внимания к теологическим концепциям, таким как «правильный объектно-ориентированный дизайн» и прочее. Это язык для людей, которые хотят сделать свою работу быстро и просто, а как там оно «концептуально» верно, отходит на второй план.
Есть языки, которые идут по одному из двух векторов развития: доверяют разработчику, дают средства и возможности, за что он может заплатить неправильностью своих решений. И языки, которые по максимуму всё запрещают, чтобы писать было просто и топорно. Все решения давно приняты за тебя, всем известно как делать правильно, например, Golang. С такой точки зрения «Почти все «фичи» — это сахарная кола, а программирование — это толстяк с диабетом»».
Python старается оставаться максимально гибким и давать разработчику свободу действий, сохраняя максимум функционала. Потому Python использует отступы, динамически типизирован и имеет не самые очевидные идеи в дизайне ООП. Но глупо отрицать, что такая стратегия может привести к плохим решениям.
Python похож на ту маму, которая позволит вам тусоваться с плохими детьми поздно ночью, если вы осознаете последствия.
Он дает вам свободный доступ к свойствам класса, даже если они должны быть частными или константными, потому что вы уже взрослый.
Это извечная дилемма: что лучше авторитарная стабильность или нестабильная свобода? Каждый человек отвечает на этот вопрос сам. Так же, как и выбирает подходящий для себя инструмент – язык программирования.
ООП в Python не лучше и не хуже, чем в других языка. Он другой. Такой каким концептуально его видел главный разработчик языка Гвидо ван Россум. ООП в Python это часть Дзена Python. Философии, для которой язык и был разработан.
Проблема в том, что люди пытаются перенять подходы из других языков, а не учатся использовать уникальные сильные стороны Python. У Python довольно надежная объектная модель, но это объектная модель Python, а не C++, Java или…кого-то другого.
Не динамическая типизация, корявый ООП и большое количество библиотек порождают говонокод. Говнокод порождают люди. Люди, которых не интересуют инженерные практики, которые лезут в профессию исключительно за деньгами и не видят в программировании своего ремесла, и, безусловно, те люди, которых привлекает низкий порог вхождения.
А Python просто сейчас очень популярен. Он своего рода фронтмен, а тот кто на передовой, того обычно и критикуют. И да, я понимаю, что Python стремится быть как можно более простым, как завещал Эйнштейн: «все должно быть настолько простым, насколько это возможно, но не проще». Однако иногда Python всё же попадает в это «проще» чем надо, что может выливаться в проблемы.
Python получает важные обновления по ходу игры, потому что изначально задумывался как простенький язык сценариев (а там чем меньше церемоний, тем лучше).
Дизайн языка потихоньку меняется. Аннотации типов, разного рода оптимизации говорят о том, что язык и сообщество взрослеют и зреют. Python со времён 2 версии уже сильно изменился и будет продолжать меняться. Как молодой бунтарь контркультуртурщик под призмой времени превращается в консерватора и прагматика, также и Python превратится просто в стабильный качественный инструмент. А на смену ему придёт новая рок-звезда, которая будет вертеть устои инженерной культуры и привлекать школьников.
То, что код превращается в беспорядок, — это ваша вина, а не вина языка. И именно таким и должен быть хороший язык: инструментом, помогающим решать ваши проблемы, а не диктатором, который покровительствует вам, что-то запрещает, командует вами.
Если мы напишем язык, который смогут использовать идиоты, в конце концов, только идиоты и будут его использовать. И да, это цена, которую придётся заплатить.
Закончу мысль довольно известной фразой: «Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует».
