ЗАМЕР
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))