- BrainTools - https://www.braintools.ru -
Вы уже освоили переменные, циклы и функции. Ваши скрипты бодро парсят сайты и перекладывают файлы. Но однажды проект начинает расти.
Вместо одного файла — десять. Переменные «путешествуют» по коду непредсказуемым образом, а попытка исправить один баг рождает два новых. Вы смотрите на редактор и понимаете: это не архитектура, это тарелка со спагетти.
Именно в этот момент на сцену выходит ООП.
Многие новички боятся этой аббревиатуры, представляя скучные университетские лекции и сложные диаграммы. Из-за страха они продолжают писать код «в столбик», лишая себя мощных инструментов разработки.
Хотите системных знаний?
Если текстового формата мало и хочется много практики, я сделал бесплатный курс на Stepik: ООП Python: Часть 1 [1]. Залетайте, там мы раскладываем всё по полочкам с нуля.
А для тех, кто хочет понять саму суть прямо сейчас — добро пожаловать.
Цель этой статьи — объяснить ООП «на пальцах». Мы не будем грузить вас академическими определениями из учебников 90-х. Мы просто возьмем Python и посмотрим, как классы помогают превратить хаос в порядок.
Дисклеймер: Эта статья для тех, кто уже знает базу (как написать
if,forиdef), но впадает в ступор при виде словаclass. Если вы хотите перестать бояться и начать писать чистый, расширяемый код — добро пожаловать под кат.
Давайте сразу к делу. Представьте, что вы разработчик RPG.
Если писать без ООП, то каждый персонаж — это просто куча разрозненных переменных: name_1 = "Arragoornis", hp_1 = 100, name_2 = "Leegoolases", hp_2 = 80. Управлять этим — ад. Здесь нам на помощь приходят Класс и Объект.
Самая простая аналогия:
Класс — это чертеж архитектора. На бумаге написано: «Здесь дверь, здесь окно, высота потолков 3 метра». Жить в чертеже нельзя.
Объект (экземпляр) — это конкретный дом, построенный по этому чертежу. Вы можете построить по одному чертежу целую улицу домов. Они будут похожи структурой, но в одном будут жить котики, а в другом — рок-музыканты.
В нашей игре:
Класс Character — это описание того, что вообще может делать герой (ходить, бить, иметь имя).
Объект hero — это конкретный «Вася», у которого 100 здоровья и меч в руке.
Создадим наш чертеж. В Python это делается ключевым словом class.
Внутри класса есть особая функция (метод) — __init__. Это конструктор. Он запускается автоматически в тот момент, когда вы создаете новый объект. Его задача — задать начальные характеристики («родить» персонажа).
class Character:
# Конструктор: вызывается при создании объекта
def __init__(self, name, power):
self.name = name # Имя конкретного персонажа
self.power = power # Сила конкретного персонажа
self.hp = 100 # Здоровье у всех при рождении 100
# Создаем объекты (строим дома по чертежу)
hero1 = Character("Zonanas", 50)
hero2 = Character("Landalif", 30)
print(hero1.name) # Выведет: Zonanas
print(hero2.name) # Выведет: Landalif
Это главный вопрос всех новичков. Почему он везде?
Смотрите, класс — это шаблон. Когда вы пишете код внутри класса, Python еще не знает, для какого именно объекта этот код будет выполняться. Для Zonanas? Или для Landalif?
self — это ссылка на текущий объект.
Когда мы пишем self.name = name, мы буквально говорим интерпретатору: «Возьми этого конкретного парня (который сейчас создается) и запиши ему в поле name вот это значение».
Простое правило: Читайте
selfкак «Я» или «Мой собственный».
self.hp -= 10означает: «Уменьшить моё собственное здоровье на 10».
В Python есть два типа данных внутри класса, и путать их опасно.
Атрибуты экземпляра (внутри __init__ через self): Уникальные для каждого объекта. У каждого свое имя и свое здоровье.
Атрибуты класса (объявлены прямо в теле класса): Общие для всех. Например, гравитация в игровом мире или IP-адрес сервера.
Классические грабли новичка:
Никогда не создавайте изменяемые объекты (например, списки) как атрибуты класса, если хотите, чтобы они были уникальными.
Посмотрите на этот баг:
class Hero:
# ОШИБКА! Этот рюкзак общий для ВСЕХ героев!
inventory = []
def __init__(self, name):
self.name = name
def pick_item(self, item):
self.inventory.append(item)
# Создаем героев
warrior = Hero("Warrior")
mage = Hero("Mage")
# Воин подбирает меч
warrior.pick_item("Sword")
# ВНЕЗАПНО у мага в кармане тоже появляется меч!
print(mage.inventory) # ['Sword'] -> Ой...
Почему так вышло? Потому что inventory создался один раз при чтении класса интерпретатором. И все герои ссылаются на один и тот же список в памяти [3].
Как правильно? Прятать изменяемые данные внутрь __init__:
class Hero:
def __init__(self, name):
self.name = name
self.inventory = [] # Теперь у каждого свой новый пустой список
Методы — это просто функции, которые живут внутри класса. Единственное отличие — первым аргументом они всегда принимают self.
class Character:
def __init__(self, name, hp):
self.name = name
self.hp = hp
# Метод атаки
def attack(self, other_character):
print(f"{self.name} атакует {other_character.name}!")
other_character.hp -= 10
p1 = Character("Batman", 100)
p2 = Character("Joker", 100)
p1.attack(p2) # Бэтмен бьет Джокера
print(p2.hp) # 90
Обратите внимание [4]: когда мы вызываем p1.attack(p2), мы передаем только один аргумент (p2). А где self? Python подставляет его сам!
Вызов p1.attack(p2) превращается под капотом в Character.attack(p1, p2).
В любом учебнике вам скажут, что ООП держится на трех китах: Инкапсуляция, Наследование и Полиморфизм. Но в Python эти киты имеют свой особый окрас. Начнем с первого.
Инкапсуляция — это защита данных от чужих ручек. В идеале мы хотим скрыть внутреннюю механику класса и дать пользователю только удобный «пульт управления» (публичные методы).
В языках вроде Java или C++ есть строгие ключевые слова private и protected. Если вы пометите переменную как private, компилятор просто ударит вас по рукам при попытке добраться до неё извне.
В Python философия другая: «We are all consenting adults here» (Мы все здесь взрослые люди).
Это «джентльменское соглашение». Если вы видите атрибут или метод, начинающийся с одного подчеркивания (например, _internal_id), это сигнал от разработчика:
«Дружище, это внутренняя переменная. Ты можешь её трогать, интерпретатор тебе не запретит. Но если ты её изменишь и код сломается — это твои проблемы».
class DatabaseConnection:
def __init__(self):
self.is_connected = True
self._port = 5432 # Это "внутренний" атрибут
db = DatabaseConnection()
print(db._port) # 5432 - Python разрешит это сделать
db._port = 9090 # И это тоже
Это не защита, это предупреждение. Как ленточка «Не входить» на стройке.
Многие новички думают: «Ага! Чтобы сделать переменную реально приватной, надо поставить два подчеркивания!»
Это миф.
Двойное подчеркивание включает механизм Name Mangling (искажение имен). Python просто меняет имя переменной под капотом, чтобы вы случайно не переопределили её в дочернем классе.
class Account:
def __init__(self, balance):
self.__balance = balance # Вроде как приватная
acc = Account(1000)
# print(acc.__balance) # Ошибка! AttributeError
# НО! Если очень хочется, то можно:
print(acc._Account__balance) # 1000 - Мы взломали систему!
Зачем это нужно реально? Не для безопасности (пароли так прятать нельзя), а для защиты от конфликтов имен при наследовании. Используйте __ только если вы пишете сложную библиотеку и боитесь, что пользователь создаст наследника с таким же именем атрибута. В 99% случаев вам хватит _.
Представьте, у нас есть класс Person с полем age.
Плохой путь (Java-style):
Писать методы get_age() и set_age(). Это засоряет код. В Python так не принято.
Плохой путь (Naive):
Просто разрешить менять p.age = -100. Возраст не может быть отрицательным!
Путь джедая: Декоратор @property
Мы оставляем удобный синтаксис (через точку), но под капотом добавляем логику [5] проверки.
class Person:
def __init__(self, age):
self._age = age # Храним в защищенной переменной
# Геттер: вызывается, когда мы пишем person.age
@property
def age(self):
return self._age
# Сеттер: вызывается, когда мы пишем person.age = value
@age.setter
def age(self, value):
if value < 0:
print("Нельзя родиться обратно! Возраст будет 0.")
self._age = 0
else:
self._age = value
bob = Person(25)
bob.age = -50 # Сработает сеттер
print(bob.age) # Выведет: 0 (сработал геттер)
Итог: Используйте @property, когда вам нужна валидация данных или когда значение атрибута вычисляется на лету, но вы хотите, чтобы для пользователя это выглядело как обычная переменная.
Программисты — люди ленивые. Если мы написали код один раз, мы не хотим писать его снова. Это принцип DRY (Don’t Repeat Yourself — Не повторяйся).
Представьте, что в нашей RPG есть Warrior (Воин) и Mage (Маг).
У обоих есть имя, здоровье, координаты X и Y.
Если мы создадим два разных класса и скопируем туда одинаковые строки self.name = name, self.hp = hp — мы нарушим принцип DRY. Если потом мы захотим добавить всем героям «уровень», придется править код в двух местах.
Решение: Создать общего родителя Character и унаследовать от него специфичные классы.
class Character:
def __init__(self, name, hp):
self.name = name
self.hp = hp
def move(self):
print(f"{self.name} идет вперед.")
# Наследуемся! В скобках указываем родителя
class Mage(Character):
def cast_spell(self):
print(f"{self.name} кидает огненный шар! Бдыщ!")
# Маг умеет всё, что умеет Character + свое
gandalf = Mage("Landalif", 50)
gandalf.move() # Метод от родителя
gandalf.cast_spell() # Свой метод
Часто нам нужно не просто взять метод родителя, а расширить его.
Например, при рождении Мага мы хотим не только дать ему имя (как у всех), но и начислить ману (чего нет у воинов).
Если мы просто напишем свой __init__ в Маге, он перезапишет (переопределит) родительский, и имя не задастся. Нам нужно вызвать родительскую логику внутри дочерней. Для этого есть super().
class Mage(Character):
def __init__(self, name, hp, mana):
# Эй, родитель! Сделай свою обычную работу (имя и хп)
super().__init__(name, hp)
# А теперь я добавлю кое-что от себя
self.mana = mana
def show_stats(self):
print(f"Имя: {self.name}, Мана: {self.mana}")
merlin = Mage("Landalif", 60, 100)
merlin.show_stats()
super() — это прокси-объект, который говорит: «Вызови этот метод так, как он написан в классе выше по иерархии». Это спасает от дублирования кода инициализации.
Python — один из немногих языков, где у Класса может быть два (и более) других Класса от которых он наследуется. Это мощно, но опасно.
Допустим, у нас есть Pegasus (Пегас). Он — наполовину Horse (Лошадь), наполовину Bird (Птица).
class Horse:
def run(self):
print("Тыгыдык-тыгыдык")
def noise(self):
print("Иго-го!")
class Bird:
def fly(self):
print("Вжух! Я лечу!")
def noise(self):
print("Чик-чирик!")
# Множественное наследование через запятую
class Pegasus(Horse, Bird):
pass
p = Pegasus()
p.run() # От лошади
p.fly() # От птицы
Проблема Алмаза (Diamond Problem):
И Лошадь, и Птица имеют метод noise(). Как будет звучать Пегас: «Иго-го» или «Чик-чирик»?
Здесь в игру вступает MRO (Method Resolution Order) — Порядок Разрешения Методов.
Python составляет список предков в строгом порядке. Правило упрощенно звучит так: «Ищем слева направо, снизу вверх».
Так как мы написали class Pegasus(Horse, Bird), то Horse стоит левее. Значит, он главнее.
p.noise()
# Выведет: "Иго-го!" (потому что Horse первый в списке родителей)
Посмотреть этот порядок можно программно:
print(Pegasus.mro())
# [<class '__main__.Pegasus'>, <class '__main__.Horse'>, <class '__main__.Bird'>, <class 'object'>]
Если вы запутались в наследовании — всегда проверяйте .mro(). Это карта, по которой Python ищет методы. Но лучше не злоупотребляйте множественным наследованием без веской причины — это прямой путь к головной боли [6].
С греческого это переводится как «много форм».
Суть: Один и тот же код может работать с разными типами объектов, если они ведут себя похоже.
В строгих языках (как Java или C++) вы должны предъявить паспорт: «Я — наследник класса Animal».
В Python паспорт не нужен. Нам без разницы, от кого ты наследовался. Нам важно, что ты умеешь делать.
Это называется Утиная типизация (Duck Typing).
«Если нечто ходит как утка, плавает как утка и крякает как утка — то это утка» (Джеймс Уиткомб Райли).
Вернемся к нашей игре. У нас есть Воин, Маг и, скажем, Дракон. Они абсолютно разные внутри. У Воина есть выносливость, у Мага — мана, у Дракона — чешуя. Они даже могут не наследоваться от одного класса.
Но у всех троих есть метод attack().
class Warrior:
def attack(self):
print("Воин бьет мечом: ХРЯСЬ!")
class Mage:
def attack(self):
print("Маг колдует фаербол: ВЖУХ!")
class Dragon:
def attack(self):
print("Дракон дышит огнем: ПФФФ!")
# А теперь — магия полиморфизма
def mass_battle(units):
"""
Этой функции всё равно, кого ей передали.
Главное, чтобы у объекта был метод attack().
"""
for unit in units:
unit.attack()
# Собираем армию
army = [Warrior(), Mage(), Dragon(), Warrior()]
# В БОЙ!
mass_battle(army)
Что здесь произошло?
Функция mass_battle принимает список units. Она не проверяет: «Ты Воин? А ты Маг?». Она просто берет объект и пытается вызвать у него .attack().
Берет первого — это Воин. Есть метод attack? Есть. Вызываем.
Берет второго — это Маг. Есть attack? Есть. Вызываем.
Берет третьего…
Если бы мы подсунули туда объект Chair (Стул), у которого нет метода attack(), Python выбросил бы ошибку [7] AttributeError. Но пока объект поддерживает нужный интерфейс (поведение [8]) — всё работает.
Зачем это нужно?
Вы можете добавить в игру новый класс Robot через год после написания функции mass_battle. Вам не придется переписывать функцию битвы. Робот просто должен иметь метод attack(), и он автоматически «встанет в строй». Это и есть гибкость архитектуры.
Представьте, вы главный архитектор игры. Вы хотите, чтобы любой новый тип врага обязан был уметь атаковать (attack) и умирать (die). Если какой-то программист забудет реализовать метод die, игра упадет в самый неподходящий момент.
Как заставить коллегу-разработчика реализовать эти методы?
На честном слове? На стикерах? В документации?
Нет. Мы используем Абстрактные Базовые Классы (ABC).
Абстракция — это выделение главного. Мы говорим: «Любой персонаж должен иметь эти методы. Но как именно он их реализует — его дело».
В Python для этого есть модуль abc (Abstract Base Classes).
from abc import ABC, abstractmethod
class Enemy(ABC): # Наследуемся от ABC
@abstractmethod
def attack(self):
"""Этот метод ОБЯЗАН быть у всех врагов"""
pass
@abstractmethod
def die(self):
"""И этот тоже"""
pass
def scream(self):
"""А этот метод уже готов и общий для всех"""
print("ААРГХ!!!")
Теперь попробуем схитрить и создать неполноценного врага:
class LazyOrc(Enemy):
def attack(self):
print("Орк лениво машет дубиной")
# А метод die() мы "забыли" написать
# Попытка создать экземпляр
# orc = LazyOrc()
# TypeError: Can't instantiate abstract class LazyOrc with abstract method die
Бам! Python ударил нас по рукам до запуска программы.
Он говорит: «Ты обещал реализовать die, но не сделал этого. Я не дам тебе создать объект этого класса».
Это и есть сила абстракции.
Гарантия интерфейса: Вы уверены, что у любого наследника Enemy точно есть методы attack и die.
Защита от дурака: Нельзя случайно создать экземпляр самого Enemy (ведь это просто идея врага, а не конкретный монстр).
Теперь исправимся:
class ProperOrc(Enemy):
def attack(self):
print("Орк яростно бьет!")
def die(self):
print("Орк падает замертво.")
orc = ProperOrc() # Всё работает!
orc.scream() # И общий метод тоже
Используйте абстрактные классы, когда пишете каркас приложения, который будут расширять другие люди (или вы сами через полгода).
Вы когда-нибудь задумывались, почему 2 + 2 работает? Или почему print(my_list) выводит красивый список [1, 2, 3], а не адрес в памяти?
Это всё — Магические методы (или Dunder-методы, от Double UNDERscore).
Это специальные методы, которые начинаются и заканчиваются двойным подчеркиванием. Они — «крючки», за которые дергает сам интерпретатор Python, когда видит определенный синтаксис.
Вы не вызываете их напрямую. Вы пишете a + b, а Python тихонько вызывает a.__add__(b).
Давайте научим наши классы вести себя как встроенные типы данных.
Создадим простой класс:
class Item:
def __init__(self, name, value):
self.name = name
self.value = value
sword = Item("Меч", 100)
print(sword)
# Вывод: <__main__.Item object at 0x000002...>
Выглядит ужасно. Это адрес в памяти, который нам ни о чем не говорит. Исправим это.
__str__ (String): Для пользователей. Должно быть красиво и понятно.
__repr__ (Representation): Для программистов (отладка). В идеале строка должна быть валидным кодом Python, чтобы воссоздать объект.
class Item:
def __init__(self, name, value):
self.name = name
self.value = value
def __str__(self):
return f"{self.name} (цена: {self.value})"
def __repr__(self):
return f"Item('{self.name}', {self.value})"
sword = Item("Меч", 100)
print(str(sword)) # Меч (цена: 100) — Красиво!
print(repr(sword)) # Item('Меч', 100) — Полезно для логов!
Совет: Если лениво писать оба, напишите только
__repr__. Python будет использовать его как запасной вариант для__str__.
Представьте, что у героев есть кошельки. Мы хотим сложить два кошелька, просто написав wallet1 + wallet2.
class Wallet:
def __init__(self, gold):
self.gold = gold
# Учим класс складываться (+)
def __add__(self, other):
# self - это левый операнд, other - правый
if isinstance(other, Wallet):
return Wallet(self.gold + other.gold)
return NotImplemented
# Учим класс сравниваться (==)
def __eq__(self, other):
return self.gold == other.gold
w1 = Wallet(50)
w2 = Wallet(100)
w3 = Wallet(50)
total = w1 + w2 # Python вызывает w1.__add__(w2)
print(total.gold) # 150
print(w1 == w3) # True (теперь мы сравниваем золото, а не адреса объектов)
Теперь ваши объекты — полноправные граждане языка, с которыми можно проводить математические операции.
Хотите, чтобы ваш объект вел себя как список? Чтобы можно было узнать его длину (len()) или получить элемент по индексу (obj[0])?
Сделаем колоду карт.
class Deck:
def __init__(self):
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = '♠♣♥♦'
# Генерируем список карт: ['2♠', '2♣', ..., 'A♦']
self._cards = [r + s for s in suits for r in ranks]
# Позволяет вызывать len(deck)
def __len__(self):
return len(self._cards)
# Позволяет получать карту по индексу: deck[0]
# АВТОМАТИЧЕСКИ делает объект итерируемым в for!
def __getitem__(self, position):
return self._cards[position]
deck = Deck()
print(len(deck)) # 52
print(deck[0]) # 2♠ (первая карта)
print(deck[-1]) # A♦ (последняя карта)
# Итерация работает "из коробки" благодаря __getitem__
import random
print(random.choice(deck)) # Случайная карта
Мы написали всего два метода, а получили функционал целого списка!
Вы наверняка писали:
with open("file.txt") as f:
data = f.read()
Это менеджер контекста. Он гарантирует, что файл закроется, даже если внутри блока произойдет ошибка.
Мы можем сделать свой аналог для чего угодно. Например, для «безопасного соединения» в игре.
class GameSession:
def __enter__(self):
print("Подключаемся к серверу...")
return self # Это попадет в переменную после as
def play(self):
print("Играем!")
def __exit__(self, exc_type, exc_val, exc_tb):
print("Отключаемся от сервера и сохраняем прогресс.")
# Если была ошибка, здесь можно её обработать
if exc_type:
print(f"Ой, ошибка: {exc_val}")
return True # Подавить ошибку (не дать программе упасть)
# Используем
with GameSession() as session:
session.play()
# Даже если здесь будет ошибка, __exit__ всё равно сработает!
Итог по магии:
Дандер-методы — это не черная магия, а способ встроиться в синтаксис Python. Используйте их, чтобы ваш код был интуитивным (item1 + item2 всегда лучше, чем item1.add_item(item2)).
Вы уже умеете создавать классы, наследовать их и перегружать операторы. Поздравляю, вы знаете ООП лучше 80% новичков. Но есть инструменты, которые отличают «просто код» от «элегантной архитектуры».
Обычные методы принимают первым аргументом self. Им нужен конкретный объект, чтобы работать. Но иногда логика не привязана к конкретному экземпляру.
Иногда функция логически связана с классом (например, утилита), но ей без разницы на состояние объекта (self).
class Calculator:
def __init__(self, version):
self.version = version
@staticmethod
def add(a, b):
# Здесь нет self! Мы не знаем ни версию, ни состояние.
# Это просто функция 2+2.
return a + b
# Можно вызывать без создания объекта!
print(Calculator.add(5, 10)) # 15
Зачем? Чтобы не засорять глобальное пространство имен. Если функция нужна только для работы с Calculator, пусть лежит внутри него.
Этот метод принимает первым аргументом класс (cls), а не объект (self).
Чаще всего это используется для альтернативных конструкторов.
Представьте, что данные приходят к вам в разных форматах.
Стандартный конструктор (__init__) принимает имя и уровень. А что, если данные пришли в виде строки "Geralt-50"?
class Hero:
def __init__(self, name, level):
self.name = name
self.level = level
@classmethod
def from_string(cls, hero_string):
# cls - это ссылка на сам класс Hero
name, level = hero_string.split('-')
# Мы возвращаем НОВЫЙ объект этого класса
return cls(name, int(level))
# Стандартный способ
h1 = Hero("Yennefer", 90)
# Альтернативный способ (через фабричный метод)
h2 = Hero.from_string("Geralt-50")
print(h2.name) # Geralt
Это делает код невероятно чистым. Вместо того чтобы парсить строку снаружи, мы учим класс самому понимать разные форматы данных.
По умре __dict__.
Это гибко: вы можете в любой момент добавиолчанию Python хранит все атрибуты объекта в специальном словать hero.new_attribute = "wow".
Но это жрет память. Словарь — тяжелая структура данных.
Если вы создаете миллион частиц в системе частиц для игры, память кончится мгновенно.
__slots__ говорит Python: «Не создавай словарь __dict__. Выдели память только под эти конкретные атрибуты».
class Pixel:
# Жестко фиксируем атрибуты. Шаг влево, шаг вправо — ошибка.
__slots__ = ('x', 'y', 'color')
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
p = Pixel(10, 20, "red")
# p.z = 30 # AttributeError! Нельзя добавлять новые атрибуты.
Плюсы:
Экономия памяти (в разы!).
Скорость доступа к атрибутам (немного быстрее).
Запрет на создание случайных атрибутов (защита от опечаток).
До Python 3.7 создание простых классов для хранения данных (просто контейнеров) было болью. Нужно было писать __init__, __repr__, __eq__… Это скучно.
Встречайте Dataclasses. Это декоратор, который пишет код за вас.
Было (Старая школа):
class InventoryItem:
def __init__(self, name, weight, price):
self.name = name
self.weight = weight
self.price = price
def __repr__(self):
return f"Item({self.name}, {self.weight}, {self.price})"
def __eq__(self, other):
return (self.name, self.weight, self.price) ==
(other.name, other.weight, other.price)
Стало (Python 3.7+):
from dataclasses import dataclass
@dataclass
class InventoryItem:
name: str
weight: float
price: int = 0 # Можно задавать дефолтные значения!
item1 = InventoryItem("Apple", 0.2, 10)
item2 = InventoryItem("Apple", 0.2, 10)
print(item1) # InventoryItem(name='Apple', weight=0.2, price=10)
print(item1 == item2) # True
Что произошло?
Декоратор @dataclass посмотрел на аннотации типов и сам сгенерировал методы __init__, __repr__, __eq__ и другие. Код стал чище в 3 раза.
Совет: Если ваш класс — это просто «мешок с данными» без сложной логики, всегда используйте
dataclasses.
Знать синтаксис class — это 10% успеха. Остальные 90% — это понимание, как эти классы связывать друг с другом.
На Хабре (да и на любом код-ревью) новичков чаще всего бьют не за то, что они забыли self, а за то, что они построили архитектурного монстра Франкенштейна.
Давайте разберем две главные ошибки, из-за которых ваш код становится неподдерживаемым.
Наследование — это хорошо для новичков. Кажется, что это так удобно: унаследовался, и куча методов досталась бесплатно! Но это ловушка.
Классический пример ошибки:
Вы делаете класс Car (Машина). Машине нужен двигатель.
Новичок думает: «О, у меня уже есть класс Engine с методами start() и stop(). Унаследуюсь-ка я от него!»
# ПЛОХО: Наследование ради кода
class Engine:
def start(self):
print("Врум-врум!")
class Car(Engine): # Машина ЯВЛЯЕТСЯ двигателем? Нет!
def drive(self):
self.start() # Используем метод родителя
print("Поехали!")
Почему это плохо?
Потому что логически Машина НЕ ЯВЛЯЕТСЯ Двигателем.
Если завтра вы захотите добавить в игру Boat (Лодку), которая тоже имеет двигатель, но не имеет колес — у вас начнутся проблемы. А если вы захотите заменить двигатель на электрический? Придется переписывать класс машины?
Правило:
Используйте Наследование, если отношение «Является» (Is-a). (Кошка является Животным).
Используйте Композицию, если отношение «Имеет» (Has-a). (Машина имеет Двигатель).
Как правильно (Композиция):
Мы просто кладем объект двигателя внутрь машины как переменную.
# ХОРОШО: Композиция
class Engine:
def start(self):
pass
class Car:
def __init__(self):
# Машина СОДЕРЖИТ двигатель внутри себя
self.engine = Engine()
def drive(self):
self.engine.start() # Делегируем работу двигателю
# Бонус: Мы можем легко менять детали!
class ElectricEngine:
def start(self):
print("Тишина... Шшшш...")
my_tesla = Car()
my_tesla.engine = ElectricEngine() # Заменили деталь на лету!
my_tesla.drive()
Это делает систему гибкой. Вы можете собирать сложные объекты из простых кубиков (конструктор LEGO), а не лепить одну огромную статую из глины.
SOLID — это 5 принципов здорового ООП. Мы не будем сейчас грузить вас ими всеми (это тема для отдельной статьи), но запомните самый первый и важный — S (Single Responsibility Principle).
Принцип: У класса должна быть только одна причина для изменения.
Или проще: Класс должен делать что-то одно, но делать это хорошо.
Как пишет новичок :
class User:
def __init__(self, name):
self.name = name
def save_to_db(self):
# Логика работы с SQL
pass
def send_email(self, message):
# Логика работы с SMTP
pass
def generate_report(self):
# Логика форматирования PDF
pass
Этот класс User знает слишком много.
Если изменится пароль от базы данных — мы идем править User.
Если мы захотим сменить почтовый сервис — мы идем править User.
Если нужен отчет не в PDF, а в Excel — мы снова правим User.
Это бомба замедленного действия. Ошибка в отправке почты может сломать сохранение в базу.
Как пишет профи (Разделение ответственности):
# 1. Данные (просто хранит инфу)
@dataclass
class User:
name: str
# 2. Работа с БД (отвечает за сохранение)
class UserRepository:
def save(self, user):
print(f"Сохраняю {user.name} в базу...")
# 3. Почта (отвечает за уведомления)
class EmailService:
def send(self, user, msg):
print(f"Письмо для {user.name}: {msg}")
# Использование:
u = User("Neo")
repo = UserRepository()
mailer = EmailService()
repo.save(u)
mailer.send(u, "Wake up!")
Теперь, если вы сломаете отправку почты, сохранение в базу продолжит работать. Код стал надежнее и проще для тестирования.
Фух, выдохнули. Мы пронеслись галопом по темам, на изучение которых в университете уходит семестр.
Что мы поняли?
ООП — это не огромный учебник и не «взрослый способ писать код».
Это инструмент управления сложностью.
Когда ваш скрипт занимает 50 строк — ООП вам не нужно (оно только все усложнит).
Когда строк становится 5000, а над проектом работаете вы и еще три человека — без классов, инкапсуляции и четких интерфейсов вы утонете в хаосе.
Самая частая ошибка новичка после прочтения такой статьи — «Синдром Джависта». Он начинает видеть объекты везде.
Надо сложить два числа? Создам класс Adder!
Надо скачать файл? Создам AbstractFileDownloaderFactory!
Остановитесь.
Python — мультипарадигменный язык. В нем прекрасно живут и функции, и классы.
Если вашу задачу решает одна простая функция def, не нужно оборачивать её в класс со статическим методом.
Лучший код — это код, которого нет. Чем проще решение, тем оно надежнее.
Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе [9]. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.
Вопрос в студию: А как часто вы используете множественное наследование в реальных проектах? Или считаете, что это «зло», которое нужно запретить законодательно? Делитесь в комментариях, похоливарим!
Автор: enamored_poc
Источник [10]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/25809
URLs in this post:
[1] ООП Python: Часть 1: https://stepik.org/course/256643/promo
[2] Image: https://sourcecraft.dev/
[3] памяти: http://www.braintools.ru/article/4140
[4] внимание: http://www.braintools.ru/article/7595
[5] логику: http://www.braintools.ru/article/7640
[6] боли: http://www.braintools.ru/article/9901
[7] ошибку: http://www.braintools.ru/article/4192
[8] поведение: http://www.braintools.ru/article/9372
[9] моём Telegram-сообществе: https://t.me/+NlTdqmVuBkIzMDBi
[10] Источник: https://habr.com/ru/articles/1000378/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1000378
Нажмите здесь для печати.