super() и порядок MRO: как Python ищет методы
super() вызывает не «метод родителя», а следующий метод в порядке MRO — порядке, в котором Python ищет атрибуты по иерархии. Разбираем __mro__, расширение методов и кооперативные цепочки вызовов.
Редакция Питоники
В прошлом уроке вы расширяли классы наследованием и трогали super() в __init__. Осталось два вопроса: куда именно смотрит Python, когда ищет метод, и почему super() нельзя понимать как «вызов родителя». Оба ответа спрятаны в механизме MRO (Method Resolution Order) — он определяет и поиск атрибутов, и поведение наследования в иерархиях любой сложности.
Куда смотрит Python, когда вы вызываете метод
Вызов manager.work() запускает поиск атрибута. Сначала Python смотрит в самом объекте, затем в его классе, затем — в родителях. Порядок обхода родителей зафиксирован в атрибуте __mro__ (Method Resolution Order): это кортеж классов, выстроенных так, что каждый класс стоит раньше своих родителей. Линию строит алгоритм C3, и главное его свойство — детерминированность: порядок одинаков при каждом запуске.
class Employee:
def work(self):
return "работаю по общему регламенту"
class Manager(Employee):
def work(self):
return "управляю проектом"
manager = Manager()
print(manager.work())
print([cls.__name__ for cls in Manager.__mro__])
управляю проектом ['Manager', 'Employee', 'object']
- Python ищет
workв самом объектеmanager— среди его атрибутов; - затем в классе
Manager— метод найден, поиск закончен; - если бы метода там не было, поиск пошёл бы по
__mro__дальше:Employee, потомobject; - если имя не нашлось нигде —
AttributeError.
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def describe(self):
return f"Счёт {self.owner}: {self.balance} руб."
class SavingsAccount(Account):
def __init__(self, owner, balance=0, rate=0.05):
super().__init__(owner, balance)
self.rate = rate
def describe(self):
base = super().describe()
return base + f" Ставка: {self.rate:.0%}"
account = SavingsAccount("Анна", 10000, 0.07)
print(account.describe())
Счёт Анна: 10000 руб. Ставка: 7%
super() — не родитель, а следующий в MRO
Распространённое заблуждение — читать super() как «пойди к родителю». super() без аргументов возвращает прокси к следующему классу в порядке MRO относительно класса, где он написан, — даже если это не родитель, а «брат». super() — не «родительский класс», а следующий класс в порядке MRO: цепочка вызовов доходит до каждого класса и не зацикливается. Проверим на иерархии с двумя родителями.
class Base:
def tag(self):
return "Base"
class Left(Base):
def tag(self):
return "Left -> " + super().tag()
class Right(Base):
def tag(self):
return "Right -> " + super().tag()
class Child(Left, Right):
def tag(self):
return "Child -> " + super().tag()
print(Child().tag())
print([cls.__name__ for cls in Child.__mro__])
Child -> Left -> Right -> Base ['Child', 'Left', 'Right', 'Base', 'object']
class Cart:
def __init__(self, total):
self.total = total
def final_total(self):
print("базовая сумма:", self.total)
return self.total
class DiscountCart(Cart):
def final_total(self):
total = super().final_total()
total = round(total * 0.9, 2)
print("скидка 10%:", total)
return total
class PromoCart(DiscountCart):
def final_total(self):
total = super().final_total()
total -= 100
print("промокод минус 100:", total)
return total
order = PromoCart(1000)
print("к оплате:", order.final_total())
базовая сумма: 1000 скидка 10%: 900.0 промокод минус 100: 800.0 к оплате: 800.0
Сначала предскажи ответ в голове — это главный навык программиста.
class Greeter:
def greet(self):
return "привет"
class ShoutingGreeter(Greeter):
def greet(self):
return super().greet().upper()
print(ShoutingGreeter().greet())
class Account:
def __init__(self, owner):
self.owner = owner
class Card(Account):
def __init__(self, owner, number):
super().__init__(owner)
self.number = number
card = Card("Игорь", "4276")
print(card.owner, card.number)
class A:
def where(self):
return "из A"
class B(A):
def where(self):
return "из B"
class C(B):
pass
print(C().where())
1. Что возвращает super() без аргументов?
2. В каком порядке Python ищет метод при вызове obj.method()?
3. Что напечатает [cls.__name__ for cls in SavingsAccount.__mro__] при SavingsAccount(Account)?
4. Что случится, если потомок объявит __init__ без вызова super().__init__()?
5. Почему кооперативная цепочка super() не зацикливается?
Создайте класс Employee с полем name и методом describe(), возвращающим строку «Сотрудник: имя». Создайте потомка Developer с дополнительным полем language: его __init__ расширяет родительский через super(), а describe() добавляет к родительской строке «, пишет на язык». Создайте Developer("Пётр", "Python") и выведите describe().
Что делает super() в Python?
super() возвращает объект-прокси, который передаёт вызов следующему классу в порядке MRO. Чаще всего его используют в __init__ потомка, чтобы инициализировать родительскую часть, и в переопределённых методах, чтобы расширить поведение, а не копировать код родителя.
Урок 7: наследованиеКак посмотреть порядок MRO?
У любого класса есть атрибут __mro__ (кортеж классов) и метод mro() (список). Например, print(SavingsAccount.__mro__) покажет линию от самого класса до object — именно в этом порядке Python ищет атрибуты.
Почему super() не всегда вызывает непосредственного родителя?
При множественном наследовании следующий класс в MRO может быть не родителем, а классом-«братом»: в паре Child(Left, Right) super() внутри Left ведёт к Right, и только от него — к Base. Это позволяет каждой классу цепочки внести свою часть работы ровно один раз.
Можно ли вызвать метод родителя напрямую, без super()?
Можно: Employee.describe(self). Но такие вызовы ломают кооперативные цепочки (класс посередине будет пропущен) и хрупки при изменении иерархии. super() всегда следует актуальному порядку MRO, поэтому в новых классах используют его.
Понравился урок? Сошлитесь на него
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
ООП · Урок 7
Наследование: расширяем классы
Наследование — способ расширять классы без переписывания: потомок получает всё от родителя, переопределяет и дополняет через super(). Родство проверяет isinstance.
ООП · Урок 9
Полиморфизм и утиная типизация в Python
Полиморфизм — когда разные классы отвечают на один и тот же вызов каждый по-своему, а ваш код не интересуется, чей метод выполнится. Разбираем один интерфейс, утиную типизацию и функции над разнородными объектами.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 1
Что такое API и REST: введение в FastAPI для начинающих
Понять, что такое API, REST, HTTP-методы и JSON — и подготовиться к первому приложению на FastAPI.
api простыми словамиhttp методы
aiogram · Урок 7
Асинхронность в aiogram: asyncio для бота без страха
Разбираем на живом коде, зачем aiogram асинхронный: async и await, asyncio.sleep против time.sleep и gather для параллельных чатов.
async await python простыми словамиблокирующий вызов python
ООП · Урок 2
__init__ и self: как рождается объект
__init__ задаёт объекту стартовое состояние, а self — это сам объект внутри его методов. Разбираем механику вызова hero.heal(30) до последней детали.
__init__ self pythoninit python