Deep Engineering

ЗАМЕР

bench/oop/basics.py

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/python/classes-and-oop
Как запустить
python3.11 bench/oop/basics.py
python3.12 bench/oop/basics.py
python3.13 bench/oop/basics.py
python3.14 bench/oop/basics.py

Запись прогона

Замеры для урока «Классы и ООП»

Скрипт Что делает
basics.py наблюдения без времени: self и связанный метод, атрибут класса против атрибута экземпляра, три вида методов и фабрики стандартной библиотеки через наследника, super().__init__(), _x и __x, __new__/__init__, __eq__ без __hash__, хеш у датаклассов, __slots__ и словарь экземпляра (раздел 7)
practice.py источник ответов двух практических задач урока
python3.11 bench/oop/basics.py
python3.12 bench/oop/basics.py
python3.13 bench/oop/basics.py
python3.14 bench/oop/basics.py

Записи в runs/: без суффикса — CPython 3.13.7, остальные версии — с суффиксом (3.11.15, 3.12.3, 3.14.7); linux/amd64. Перезапущены 14.10.2026 после добавления раздела 7 — разделы 1–6 не изменились.

Цена вызова метода здесь не меряется: она снята в bench/attr-lookup/ и разобрана в статье о поиске атрибута. Урок ссылается туда, а не повторяет числа.

Что получилось

self — обычный первый аргумент. a.hello() и Greeter.hello(a) дали одно и то же. a.hello — объект типа method, и каждый раз новый: a.hello is a.hello — False, тысяча обращений — тысяча разных объектов. Метод, объявленный без self, падает при вызове через экземпляр: takes 0 positional arguments but 1 was given.

Изменяемый атрибут класса — общий. Три экземпляра добавили по метке в tags, объявленный в теле класса, — каждый видит все три, а в словаре экземпляра (vars(c2)) пусто. Присваивание через экземпляр (c1.total = 5) создаёт свой атрибут и затеняет классовый, класс не меняется.

Три вида методов. @classmethod получает класс, через который вызван: Dog.create() вернул Dog, а не Animal; так же dict.fromkeys и datetime.fromtimestamp, вызванные через наследников, вернули наследников. @staticmethod не получает ничего лишнего.

Забытый super().__init__() — объект создан, но атрибута родителя нет; AttributeError возникает при первом чтении, а не в конструкторе.

__x переименовывается в _Класс__x. Доступ по исходному имени — AttributeError, по переименованному — пожалуйста. Наследник с тем же __pin получает своё имя и родительское не задевает.

__eq__ без __hash__ делает объект нехешируемым: Point.__hash__ — None, ни элементом множества, ни ключом словаря он быть не может. У @dataclass хеш по умолчанию тоже None; с frozen=True он генерируется. Текст исключения в 3.14 изменился и называет роль объекта: cannot use 'Point' as a set element (…) и cannot use 'Point' as a dict key (…) вместо unhashable type: 'Point' на 3.11–3.13. Остальной вывод на четырёх версиях совпадает.

__slots__ убирает словарь экземпляра не всегда (раздел 7, добавлен 14.10.2026 по внешнему аудиту, замечание PY-OOP-01). OnlySlots со __slots__ = ("x",) и базой object — hasattr(o, '__dict__') даёт False, присвоить o.extra нельзя. Но Child(Base) со __slots__ = (), где Base — обычный класс, принимает c.extra = 1: vars(c) — {'extra': 1}. И "__dict__", перечисленный в slots, словарь возвращает. Текст AttributeError на 3.13 и 3.14 длиннее: … has no attribute 'extra' and no __dict__ for setting new attributes; на 3.11 и 3.12 — без хвоста.

Скрипт

328 строк
"""Классы и ООП в Python: что на самом деле происходит в семи местах, о которых спрашивают первыми.

Времени здесь нет — только наблюдения: что напечатал интерпретатор, какие
объекты совпали, какой текст у исключения. Цена вызова метода снята в
bench/attr-lookup/ и разобрана в статье о поиске атрибута.

СЕМЬ СЮЖЕТОВ:

1. SELF — ОБЫЧНЫЙ ПЕРВЫЙ АРГУМЕНТ. `a.hello()` и `Greeter.hello(a)` — один и
   тот же вызов. `a.hello` — связанный метод: новый объект при каждом
   обращении, который помнит функцию и экземпляр. Метод без `self` в
   объявлении падает при первом же вызове через экземпляр.

2. АТРИБУТ КЛАССА ПРОТИВ АТРИБУТА ЭКЗЕМПЛЯРА. Чтение ищет сначала в
   экземпляре, потом в классе; присваивание через экземпляр создаёт свой
   атрибут и затеняет классовый. Изменяемый атрибут класса при этом общий
   у всех экземпляров — классическая ловушка.

3. ТРИ ВИДА МЕТОДОВ. Что получает первым аргументом обычный метод,
   `@classmethod` и `@staticmethod`, и почему фабрика на `@classmethod` у
   подкласса создаёт подкласс — так же ведут себя фабрики стандартной
   библиотеки `dict.fromkeys` и `datetime.fromtimestamp`.

4. НАСЛЕДОВАНИЕ. Переопределённый `__init__` без `super().__init__()` —
   атрибута родителя нет, и ошибка всплывает не там, где допущена.
   `isinstance` против `type(...) is`.

5. ИНКАПСУЛЯЦИЯ ПО СОГЛАШЕНИЮ. `_x` доступен всем; `__x` переименовывается
   в `_Класс__x` — это защита от случайного совпадения имён в наследнике, а
   не от доступа.

6. МАГИЧЕСКИЕ МЕТОДЫ. `__init__` — не конструктор: объект уже создан
   `__new__`. Без `__eq__` два одинаковых объекта не равны. С `__eq__` без
   `__hash__` объект перестаёт быть хешируемым.

7. __slots__ И СЛОВАРЬ ЭКЗЕМПЛЯРА. Класс со `__slots__`, у которого ни один
   базовый класс не даёт словаря и в slots нет "__dict__", — экземпляр без
   `__dict__`, новый атрибут не присвоить. Но наследник ОБЫЧНОГО класса со
   `__slots__ = ()` словарь получает от родителя, и `c.extra = 1` проходит;
   так же — если "__dict__" перечислен в slots. Раздел добавлен в конец
   14.10.2026 по внешнему аудиту (PY-OOP-01), чтобы не сдвигать разделы 1–6.

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import sys
from dataclasses import dataclass
from datetime import datetime

print("PY", sys.version.split()[0])
print()


def err(fn):
    """Текст исключения дословно: «Тип: сообщение»."""
    try:
        fn()
    except Exception as e:  # noqa: BLE001 — нам нужен именно текст
        return f"{type(e).__name__}: {e}"
    return "исключения нет"


# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) self — обычный первый аргумент")


class Greeter:
    def __init__(self, name):
        self.name = name

    def hello(self):
        return f"привет, {self.name}"

    def broken():  # self забыт намеренно
        return "не дойдём"


a = Greeter("Аня")
print("   a.hello()          ->", a.hello())
print("   Greeter.hello(a)   ->", Greeter.hello(a))
print("   type(a.hello)      ->", type(a.hello).__name__)
print("   a.hello is a.hello ->", a.hello is a.hello)
print("   a.hello == a.hello ->", a.hello == a.hello)
print("   a.hello.__self__ is a            ->", a.hello.__self__ is a)
print("   a.hello.__func__ is Greeter.hello ->", a.hello.__func__ is Greeter.hello)
print("   Greeter.hello()    ->", err(lambda: Greeter.hello()))
print("   a.broken()         ->", err(lambda: a.broken()))
bound = [a.hello for _ in range(1000)]
print("   1000 обращений a.hello, разных объектов:", len({id(m) for m in bound}))
del bound
print()

# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) атрибут класса против атрибута экземпляра")


class Counter:
    total = 0
    tags = []


c1, c2 = Counter(), Counter()
print("   до присваивания: vars(c1) =", vars(c1), "| c1.total =", c1.total)
c1.total = 5
print("   после c1.total = 5: vars(c1) =", vars(c1))
print("   c1.total =", c1.total, "| c2.total =", c2.total, "| Counter.total =", Counter.total)
for i, c in enumerate([c1, c2, Counter()]):
    c.tags.append(f"t{i}")
print("   три экземпляра добавили по метке в tags класса:")
print("   c1.tags =", c1.tags, "| c2.tags =", c2.tags)
print("   c1.tags is c2.tags ->", c1.tags is c2.tags, "| vars(c2) =", vars(c2))


class FixedCounter:
    def __init__(self):
        self.tags = []


f1, f2 = FixedCounter(), FixedCounter()
f1.tags.append("t0")
print("   список в __init__: f1.tags =", f1.tags, "| f2.tags =", f2.tags)
print()

# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) три вида методов: что приходит первым аргументом")


class Animal:
    sound = "..."

    def describe(self):
        return f"describe получил экземпляр {type(self).__name__}"

    @classmethod
    def create(cls):
        return cls()

    @classmethod
    def who(cls):
        return f"who получил класс {cls.__name__}"

    @staticmethod
    def helper(x):
        return f"helper получил только x = {x}"


class Dog(Animal):
    sound = "гав"


d = Dog()
print("  ", d.describe())
print("  ", Dog.who(), "|", d.who())
print("  ", Dog.helper(1), "|", d.helper(2))
print("   Animal.create() ->", type(Animal.create()).__name__)
print("   Dog.create()    ->", type(Dog.create()).__name__)
print("   Animal.describe() без экземпляра ->", err(lambda: Animal.describe()))


class MyDict(dict):
    pass


class MyDateTime(datetime):
    pass


print("   MyDict.fromkeys('ab')         ->", type(MyDict.fromkeys("ab")).__name__)
print("   MyDateTime.fromtimestamp(0)   ->", type(MyDateTime.fromtimestamp(0)).__name__)
print()

# ── 4 ─────────────────────────────────────────────────────────────────────
print("4) наследование и super().__init__()")


class Account:
    def __init__(self, owner):
        self.owner = owner
        self.balance = 0


class SavingsForgot(Account):
    def __init__(self, owner, rate):
        self.rate = rate  # super().__init__ забыт


class Savings(Account):
    def __init__(self, owner, rate):
        super().__init__(owner)
        self.rate = rate


s1 = SavingsForgot("Ира", 0.05)
print("   без super(): объект создан, vars =", vars(s1))
print("   s1.balance ->", err(lambda: s1.balance))
s2 = Savings("Ира", 0.05)
print("   с super():   vars =", vars(s2))
print("   isinstance(s2, Account) ->", isinstance(s2, Account))
print("   type(s2) is Account     ->", type(s2) is Account)
print("   Savings.__mro__ ->", [c.__name__ for c in Savings.__mro__])
print()

# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) _x и __x: соглашение и переименование")


class Wallet:
    def __init__(self):
        self._cash = 10
        self.__pin = 1234

    def pin_ok(self, value):
        return value == self.__pin


w = Wallet()
print("   vars(w) =", vars(w))
print("   w._cash ->", w._cash)
print("   w.__pin ->", err(lambda: w.__pin))
print("   w._Wallet__pin ->", w._Wallet__pin)


class StrictWallet(Wallet):
    def __init__(self):
        super().__init__()
        self.__pin = 0  # своё имя, родительское не задето


sw = StrictWallet()
print("   наследник с тем же __pin: vars =", vars(sw))
print("   sw.pin_ok(1234) ->", sw.pin_ok(1234))
print()

# ── 6 ─────────────────────────────────────────────────────────────────────
print("6) магические методы: __new__, __init__, __eq__, __hash__")


class Traced:
    def __new__(cls, *args):
        print("   __new__ создаёт объект")
        return super().__new__(cls)

    def __init__(self, x):
        print("   __init__ заполняет уже созданный объект, x =", x)
        self.x = x


Traced(1)


class Plain:
    def __init__(self, x, y):
        self.x, self.y = x, y


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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)


class HPoint(Point):
    def __hash__(self):
        return hash((self.x, self.y))


print("   Plain(1, 2) == Plain(1, 2) ->", Plain(1, 2) == Plain(1, 2))
print("   Point(1, 2) == Point(1, 2) ->", Point(1, 2) == Point(1, 2))
print("   Point.__hash__ is None     ->", Point.__hash__ is None)
print("   {Point(1, 2)}              ->", err(lambda: {Point(1, 2)}))
print("   {Point(1, 2): 'x'}         ->", err(lambda: {Point(1, 2): "x"}))
print("   len({HPoint(1, 2), HPoint(1, 2)}) ->", len({HPoint(1, 2), HPoint(1, 2)}))
print("   repr(Plain(1, 2)) начинается с ->", repr(Plain(1, 2)).split(" at ")[0])


@dataclass
class DPoint:
    x: int
    y: int


@dataclass(frozen=True)
class FPoint:
    x: int
    y: int


print("   @dataclass: DPoint.__hash__ is None ->", DPoint.__hash__ is None)
print("   @dataclass(frozen=True): len({FPoint(1, 2), FPoint(1, 2)}) ->", len({FPoint(1, 2), FPoint(1, 2)}))


# ── 7 ─────────────────────────────────────────────────────────────────────
print()
print("7) __slots__: когда у экземпляра нет __dict__, а когда есть")


class OnlySlots:  # базовый — только object, "__dict__" в slots нет
    __slots__ = ("x",)


o = OnlySlots()
print("   OnlySlots: hasattr(o, '__dict__') ->", hasattr(o, "__dict__"))
print("   OnlySlots: o.extra = 1 ->", err(lambda: setattr(o, "extra", 1)))


class Base:  # обычный класс: его экземпляры получают __dict__
    pass


class Child(Base):
    __slots__ = ()


ch = Child()
ch.extra = 1
print("   Child(Base), __slots__ = (): c.extra = 1 -> vars(c) =", vars(ch))
print("   Child(Base): hasattr(c, '__dict__') ->", hasattr(ch, "__dict__"))


class SlotsWithDict:
    __slots__ = ("x", "__dict__")


sd = SlotsWithDict()
sd.extra = 1
print("   __slots__ = ('x', '__dict__'): s.extra = 1 -> vars(s) =", vars(sd))