ООП

ООП #

1. Что такое ООП? #

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

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


2. Плюсы и минусы ООП #

Плюсы:

  • Делим программу на «модули»-классы, ОБЪЕКТЫ каждый из которых делает свою часть работы.
  • Код можно повторно использовать в любом месте программы, это экономит время (не нужно писать однотипные функции для разных сущностей).
  • «Более естественная» декомпозиция ПО существенно облегчает его разработку (код легко читается и быстро пишется).
  • Возможность создавать расширяемые системы (extensible systems), именно это отличает ООП от традиционных методов программирования.

Минусы:

  • Снижение производительности, увеличение потребности памяти (Информация распределяется на множество мелких инкапсулированных объектов ⇒ на них растет количество ссылок)


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

Инкапсуляция - объединение данных и методов, работающих с ними в одном классе, а так же сокрытие деталей реализации от пользователя. В Python инкапсуляция реализуется через соглашения об именовании (один подчеркивание _ для защищенных членов, два подчеркивания __ для приватных) и свойства (property декораторы).

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

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

  • Динамическим (duck typing, переопределение методов)
    • Достигается переопределением методов в подклассах
    • Решение, какую версию метода вызвать, принимается во время выполнения программы
    • В Python используется принцип “утиной типизации” - важен не тип объекта, а наличие у него нужного метода
class Animal:
    def sound(self):
        print("Some generic animal sound")

class Dog(Animal):
    def sound(self):  # Переопределение метода
        print("Woof!")

class Cat(Animal):
    def sound(self):  # Переопределение метода
        print("Meow!")

# Демонстрация
def make_sound(animal):
    animal.sound()  # Полиморфный вызов

a1 = Dog()
a2 = Cat()

make_sound(a1)  # Woof!
make_sound(a2)  # Meow!

# Утиная типизация - даже не обязательно наследоваться от Animal
class Car:
    def sound(self):
        print("Vroom!")

make_sound(Car())  # Vroom!

Переменная может ссылаться на объект Dog, Cat или даже Car, а метод sound() будет вызван в соответствии с реальным типом объекта в рантайме.

  • Статическим (перегрузка функций)
    • В Python отсутствует традиционная перегрузка методов как в Java
    • Достигается через параметры по умолчанию, *args, **kwargs или с помощью декораторов (например, @singledispatch)
from functools import singledispatch

# Способ 1: Параметры по умолчанию
class MathUtils:
    def add(self, a, b, c=None):
        if c is not None:
            return a + b + c
        return a + b

# Способ 2: singledispatch для перегрузки по типам
@singledispatch
def process(value):
    return f"Обработка значения: {value}"

@process.register(int)
def _(value):
    return f"Обработка целого числа: {value}"

@process.register(str)
def _(value):
    return f"Обработка строки: {value}"

@process.register(list)
def _(value):
    return f"Обработка списка из {len(value)} элементов"

# Демонстрация
utils = MathUtils()
print(utils.add(2, 3))        # 5
print(utils.add(2, 3, 4))      # 9

print(process(42))             # Обработка целого числа: 42
print(process("hello"))        # Обработка строки: hello
print(process([1, 2, 3]))      # Обработка списка из 3 элементов

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

Абстракция – это способ выделить набор общих характеристик объекта, исключая из рассмотрения частные и незначимые. Соответственно, абстракция – это набор всех таких характеристик. В Python абстракция реализуется через абстрактные базовые классы (ABC) из модуля abc.

from abc import ABC, abstractmethod

class Shape(ABC):  # Абстрактный класс
    @abstractmethod
    def area(self):
        """Абстрактный метод - должен быть реализован в подклассах"""
        pass
    
    @abstractmethod
    def perimeter(self):
        """Абстрактный метод"""
        pass
    
    def description(self):  # Обычный метод
        return f"Это фигура с площадью {self.area()}"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):  # Реализация абстрактного метода
        return self.width * self.height
    
    def perimeter(self):  # Реализация абстрактного метода
        return 2 * (self.width + self.height)

# Использование
rect = Rectangle(5, 3)
print(rect.area())        # 15
print(rect.perimeter())   # 16
print(rect.description()) # Это фигура с площадью 15


4. Класс, объект, интерфейс #

Класс – это описание ещё не созданного объекта, общий шаблон. Шаблон состоит из:

  • Атрибутов (полей) – переменных, принадлежащих объекту (имя, возраст для человека и т.д.). Определяют состояние объекта.
  • Конструктора (__init__) – специального метода, который автоматически вызывается при создании объекта и инициализирует его начальное состояние.
  • Методов – функций, принадлежащих классу, которые определяют поведение объекта (что он умеет делать).
class Person:
    # Конструктор - инициализация объекта
    def __init__(self, name, age):
        # Атрибуты (поля) объекта
        self.name = name
        self.age = age
    
    # Метод класса
    def introduce(self):
        return f"Привет, меня зовут {self.name}, мне {self.age} лет"
    
    # Ещё один метод
    def have_birthday(self):
        self.age += 1
        print(f"С днём рождения! Теперь {self.name} {self.age} лет")

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

# Создание объектов (экземпляров класса)
person1 = Person("Анна", 25)  # Вызов конструктора
person2 = Person("Иван", 30)

# Каждый объект имеет своё состояние
print(person1.name)  # Анна
print(person2.name)  # Иван

# Вызов методов объектов
print(person1.introduce())  # Привет, меня зовут Анна, мне 25 лет
person2.have_birthday()     # С днём рождения! Теперь Иван 31 лет

Интерфейс — в Python нет строгого понятия интерфейса как в Java, но аналогичная функциональность достигается через:

  • Абстрактные базовые классы (ABC) – определяют контракт (набор методов), который должен реализовать любой класс-наследник
  • Протоколы (Protocol) – неформальные интерфейсы, основанные на утиной типизации (начиная с Python 3.8)
from abc import ABC, abstractmethod
from typing import Protocol

# Способ 1: Абстрактный базовый класс (формальный интерфейс)
class Drawable(ABC):
    @abstractmethod
    def draw(self):
        """Абстрактный метод - должен быть реализован"""
        pass
    
    @abstractmethod
    def get_size(self):
        """Ещё один обязательный метод"""
        pass

# Класс, реализующий интерфейс Drawable
class Circle(Drawable):
    def __init__(self, radius):
        self.radius = radius
    
    def draw(self):  # Обязательная реализация
        print(f"Рисуем круг радиусом {self.radius}")
    
    def get_size(self):  # Обязательная реализация
        return f"Площадь круга: {3.14 * self.radius ** 2}"

# Способ 2: Протокол (утиная типизация)
class Flyable(Protocol):
    def fly(self) -> str:
        """Контракт: любой объект с методом fly() считается Flyable"""
        ...

class Bird:
    def fly(self):
        return "Птица летит"

class Airplane:
    def fly(self):
        return "Самолет летит"
    
    def refuel(self):
        print("Заправка")

# Функция, работающая с любым объектом, реализующим протокол Flyable
def make_it_fly(flying_obj: Flyable):
    print(flying_obj.fly())

# Использование
circle = Circle(5)
circle.draw()           # Рисуем круг радиусом 5
print(circle.get_size()) # Площадь круга: 78.5

bird = Bird()
plane = Airplane()

make_it_fly(bird)  # Птица летит
make_it_fly(plane) # Самолет летит - работает, даже если класс не наследует Flyable

Интерфейсы позволяют задавать общие действия, которые могут выполнять различные классы, не завися от их конкретной реализации. В Python это достигается как через явное наследование от ABC, так и через неявные протоколы (утиную типизацию).


5. Какая модель наследования используется в Python и какие её формы? | Как работает MRO? #

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

В Python 3 все классы фактически являются наследниками object, даже если это не указано явно:

class User:
    pass

# примерно то же самое:
class User(object):
    pass

Основные формы наследования в Python: #

1. Одиночное наследование
Класс наследуется от одного родителя.

class Animal:
    def eat(self):
        print("Animal eats")

class Dog(Animal):
    def bark(self):
        print("Dog barks")

2. Многоуровневое наследование
Цепочка наследования: класс наследуется от класса, который сам уже от кого-то наследуется.

class Animal:
    pass

class Mammal(Animal):
    pass

class Dog(Mammal):
    pass

3. Иерархическое наследование
Несколько классов наследуются от одного базового класса.

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

4. Множественное наследование
Класс наследуется сразу от нескольких родителей.

class Swimmer:
    def move(self):
        print("Swim")

class Flyer:
    def move(self):
        print("Fly")

class Duck(Swimmer, Flyer):
    pass

5. Ромбовидное наследование / diamond inheritance Ситуация, когда один класс получает общего предка через несколько путей.

class A:
    def method(self):
        print("A")

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

Именно для таких случаев Python использует MRO на основе алгоритма C3 linearization, который корректно обрабатывает diamond inheritance.

Как работает MRO #

MRO — Method Resolution Order. Это порядок, в котором Python ищет методы и атрибуты в иерархии классов.

Когда вызывается:

obj.method()

Python ищет method примерно так:

1. В самом объекте / его классе
2. В первом родительском классе
3. В следующем родительском классе
4. Дальше по MRO
5. В object

Посмотреть MRO можно через:

ClassName.mro()

или:

ClassName.__mro__

Python не идёт просто “вглубь” по первому родителю. Он строит линейный порядок классов так, чтобы:

  1. Дочерний класс всегда шёл раньше родительского.
  2. Порядок родителей в объявлении сохранялся.
  3. Общий предок не вызывался несколько раз без необходимости.
  4. Diamond inheritance разрешался предсказуемо.

Для этого и используется C3 MRO.

Влияние порядка родителей #

Порядок родителей в объявлении класса имеет значение.

class A:
    def method(self):
        print("A")

class B:
    def method(self):
        print("B")

class C(A, B):
    pass

class D(B, A):
    pass

C().method()
D().method()

Результат:

A
B

Потому что:

print(C.mro())
# C -> A -> B -> object

print(D.mro())
# D -> B -> A -> object

Как работает super() #

super() вызывает следующий класс по MRO Пример:

class A:
    def process(self):
        print("A")

class B(A):
    def process(self):
        print("B")
        super().process()

class C(A):
    def process(self):
        print("C")
        super().process()

class D(B, C):
    def process(self):
        print("D")
        super().process()

D().process()
print(D.mro())

Результат:

D
B
C
A
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Здесь super() внутри B вызывает не обязательно A, а следующий класс после B в MRO. Для D следующий после B — это C.

Документация Python описывает super() как механизм для поиска методов в предсказуемом порядке, поддерживающий cooperative multiple inheritance.


6. Что такое MRO и Diamond problem #

Что такое MRO? #

Method resolution order - порядок разрешения методов. Алгоритм, по которому следует искать метод в случае, если у класса два и более родителей.

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

  • Сначала экземпляр
  • Затем его класс
  • Далее все суперклассы его класса с обходом сначала с глубину, а затем слева направо

Эволюция MRO #

Python 2.x: Классические классы

В классических классах использовался алгоритм DFS (Depth-First Search) с обходом в глубину слева направо: text
A → B → D → C → E

Python 3.x: Современные классы
В Python 3 все классы наследуются от object и используют алгоритм C3 Linearization, который обеспечивает более предсказуемый порядок.

Алгоритм C3 Linearization
Основные правила:

  • Порядок наследования сохраняется
  • Дочерний класс имеет приоритет над родительским
  • Порядок в списке родителей сохраняется

Формула C3:

L[C] = C + merge(L[B1], L[B2], ..., L[Bn], B1B2...Bn)

Примеры MRO: Простое множественное наследование:

class A:
    def method(self):
        print("A")

class B(A):
    def method(self):
        print("B")

class C(A):
    def method(self):
        print("C")

class D(B, C):
    pass

print(D.__mro__)
# Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

“Ромбовидное” наследование (Diamond Problem):

class A:
    def method(self):
        print("A")

class B(A):
    def method(self):
        print("B")

class C(A):
    def method(self):
        print("C")

class D(B, C):
    pass

# MRO: D → B → C → A → object


7. Что можешь рассказать про принцип программирования SOLID? #

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

S Single Responsibility Principle – принцип единственной ответственности.Каждый класс должен иметь только одну зону ответственности.

O Open closed Principle – принцип открытости-закрытости.Классы должны быть открыты для расширения, но закрыты для изменения.

L Liskov substitution Principle – принцип подстановки Барбары Лисков. Должна быть возможность вместо базового (родительского) типа класса подставить любой его подтип (класс-наследник), при этом работа программы не должна измениться.

I Interface Segregation Principle – это принцип разделения интерфейсов. Данный принцип обозначает, что не нужно заставлять клиента (класс) реализовывать интерфейс, который не имеет к нему отношения.

D Dependency Inversion Principle – это принцип инверсии зависимостей. Абстракции НЕ должны зависеть от деталей. Детали должны зависеть от абстракций. Модули верхнего уровня НЕ должны зависеть от модулей нижнего уровня, НО должны зависеть от абстракции.


8. Какие принципы SOLID чаще всего используют на практике? #

На практике чаще всего реально применяются:

  • SRP — Single Responsibility Principle
  • DIP — Dependency Inversion Principle
  • ISP — Interface Segregation Principle


9. Почему SOLID не всегда соблюдают в реальных проектах? #

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

Microsoft в документации описывает SOLID как принципы проектирования внутренних слоёв приложения и способ уменьшать связанность зависимостей, а не как формальное требование к каждому классу.

SOLID не всегда соблюдают, потому что:

  1. иногда это избыточно и излишне усложняет код;
  2. сроки важнее идеальной архитектуры;
  3. не всегда очевидны границы ответственности;
  4. механическое применение ухудшает код;
  5. legacy-код сложно быстро исправить;
  6. простое решение иногда лучше абстрактного;
  7. в Python не всегда нужны тяжёлые ООП-структуры.

SOLID нужно применять там, где он реально снижает сложность, а не ради того, чтобы каждый класс выглядел “архитектурно правильно”.


10. Что такое миксин (mixin) и для чего он применяется в объектно-ориентированном программировании? #

В Python миксины (mixins) - это классы, предназначенные для добавления функциональности в другие классы через множественное наследование, не являясь при этом самостоятельными классами для создания экземпляров. Они предоставляют набор методов, которые можно “подмешать” к другим классам для расширения их возможностей.

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

  • Особенности: Миксины обычно не содержат данных (атрибутов), а только методы, определяющие поведение. Они предназначены для использования в качестве “строительных блоков”, которые можно комбинировать для создания более сложных классов.

  • Использование: Миксины включаются в другие классы через множественное наследование. Например, если у вас есть класс User и вы хотите добавить функциональность логирования, вы можете создать миксин LoggingMixin с методами логирования и унаследовать от него класс User.

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

  • Гибкость: Они обеспечивают гибкость при проектировании классов, позволяя легко добавлять или изменять функциональность.

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

Пример:

class LoggingMixin:
    def log(self, message):
        print(f"Logging: {message}")


class User(LoggingMixin):
    def __init__(self, name):
        self.name = name
        self.log(f"User {self.name} created")


def say_hello(self):
    self.log(f"Hello, my name is {self.name}")

user = User("Alice")
user.say_hello()

В этом примере, LoggingMixin предоставляет метод log, который используется классом User. Класс User при этом несет ответственность только за свою логику, а логирование делегируется LoggingMixin. Миксины - это полезный инструмент в Python для создания гибкого, повторно используемого и поддерживаемого кода, позволяющий избегать сложных иерархий наследования и дублирования.


11. Может ли абстрактный класс содержать реализацию методов? #

Да, может
Абстрактный класс в Python может содержать реализацию методов.
Более того, даже абстрактный метод может иметь реализацию.
Пример:

from abc import ABC, abstractmethod


class BaseParser(ABC):
    @abstractmethod
    def parse(self, data: str):
        print("Общая подготовка данных")


class JsonParser(BaseParser):
    def parse(self, data: str):
        super().parse(data)
        print("Парсим JSON")

Использование:

parser = JsonParser()
parser.parse("{}")

Важный момент Метод parse() в BaseParser остаётся абстрактным, даже если внутри него есть код:

@abstractmethod
def parse(self, data: str):
    print("Общая логика")


12. Чем абстрактные классы отличаются от протоколов (интерфейсов) и обычных классов #

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

В Python нет специального ключевого слова interface, как в Java или C#. Однако интерфейсы реализуются через:

1. Абстрактные классы с одними абстрактными методами

from abc import ABC, abstractmethod

class PaymentInterface(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

    @abstractmethod
    def refund_payment(self, amount):
        pass

    @abstractmethod
    def get_status(self):
        pass

2. Протоколы (с Python 3.8+)

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...
    def scale(self, factor: float) -> None: ...

# Любой класс с методами draw() и scale() считается Drawable
class Circle:
    def draw(self) -> None:
        print("Рисуем круг")

    def scale(self, factor: float) -> None:
        print(f"Масштабируем круг в {factor} раз")

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

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

class EmailSender:
    def send(self, message: str) -> None:
        print(f"Отправляем email: {message}")


sender = EmailSender()
sender.send("Hello")

Интерфейс vs Абстрактный класс
Интерфейс:

  • ✅ Только объявления методов (что делать)
  • ❌ Никакой реализации
  • ✅ Множественное наследование (в Python)
  • ✅ Определяет только контракт
  • ✅ Все методы абстрактные

Абстрактный класс:

  • ✅ Объявления методов + реализация
  • ✅ Готовая бизнес-логика (как делать)
  • ❌ Одиночное наследование (в Python)
  • ✅ Сочетает контракт + готовую функциональность
  • ✅ Смесь абстрактных и обычных методов Обычный класс:
  • ✅ Конкретная реализация.
  • ✅ Отвечает на вопросы что делать и как делать.
  • ✅ Можно создать объект напрямую
  • ❌Задаёт контракт
  • ❌Требует явного наследования

Интерфейс (только контракт):

class DatabaseInterface(ABC):
    @abstractmethod
    def connect(self): pass

    @abstractmethod
    def execute_query(self, query): pass

    @abstractmethod
    def disconnect(self): pass

Абстрактный класс (контракт + логика):

class DatabaseConnector(ABC):
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.connection = None

    @abstractmethod
    def connect(self): pass  # Должны реализовать

    def execute_query(self, query):  # Готовая логика
        if not self.connection:
            raise ConnectionError("Нет подключения")
        return f"Выполняем: {query}"

    @abstractmethod
    def disconnect(self): pass

Используйте Интерфейс когда:

  • Нужно определить только контракт
  • Классы из разных иерархий должны иметь общее API
  • Нужно множественное наследование поведения

Используйте Абстрактный класс когда:

  • Есть общая логика для наследования
  • Классы тесно связаны по смыслу
  • Нужно предоставить готовые реализации части методов

Используйте Обычный класс когда:

  • Нужна конкретная реализация.
  • Когда нет необходимости заставлять наследников соблюдать общий контракт.

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


13. Как обратиться к protected (Защищенному) методу? #

Обратиться к нему технически можно напрямую:

class User:
    def _validate_password(self, password):
        return len(password) >= 8


user = User()

print(user._validate_password("12345678"))

Результат:

True

Но важно: в Python это не настоящее ограничение доступа. Один _ означает: «этот метод внутренний, не используй его снаружи без необходимости». В официальной документации Python сказано, что настоящих private-переменных, доступных только внутри объекта, в Python нет; используется соглашение между разработчиками.

Правильнее обращаться к protected-методу из самого класса или из дочернего класса:

class BaseService:
    def _connect(self):
        print("Подключение к базе данных")


class UserService(BaseService):
    def get_user(self):
        self._connect()
        print("Получаем пользователя")


service = UserService()
service.get_user()

Результат:

Подключение к базе данных
Получаем пользователя


14. Есть ли в Python полноценная инкапсуляция как в Java или C++? | Почему в Python нет строгих модификаторов доступа private, protected, public? #

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

В Python объект можно исследовать и изменять во время выполнения:

obj.name
obj.__dict__
getattr(obj, "name")
setattr(obj, "name", value)

Классы и объекты в Python хранят атрибуты в пространствах имён, часто в словарях. Документация Python описывает, что у класса есть namespace, реализованный через dictionary, а обращение к атрибуту класса переводится в поиск по этому namespace и по базовым классам.

Из-за такой модели строгие модификаторы доступа плохо вписываются в язык.

Главная идея Python
В Python действует подход:

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

Почему не сделали как в Java/C++ В Java/C++ доступ проверяется языком или компилятором, в Python это не основной механизм защиты. Python больше опирается на:

@property
методы класса
соглашения об именовании
документированный публичный API
тесты
линтеры
code review

Почему __name — не настоящий private
Пример:

class User:
    def __init__(self):
        self.__token = "secret"

Python переименует __token примерно так:

_User__token

Поэтому прямой доступ не сработает:

user.__token

Но технически можно обратиться так:

user._User__token

Это называется name mangling. Его цель — не абсолютная приватность, а защита от случайного конфликта имён, особенно при наследовании. Документация Python описывает этот механизм в разделе про private variables.