Атрибуты и методы: class vs instance, classmethod, staticmethod, property
В прошлом уроке мы создавали простые классы с инстанс-атрибутами. Но Python даёт больше: атрибуты на уровне класса, разные типы методов (instance, class, static) и @property для контролируемого доступа. Понимание этих различий критично для проектирования API.
Class attributes vs instance attributes
class User:
species = "Human" # class attribute
def __init__(self, name):
self.name = name # instance attribute
Class attribute хранится на классе, общий для всех инстансов:
u1 = User("Alice")
u2 = User("Bob")
print(u1.species) # Human
print(u2.species) # Human
User.species = "Cyborg" # изменили на классе
print(u1.species) # Cyborg
print(u2.species) # Cyborg
Instance attribute хранится на инстансе, своё значение у каждого:
u1.name = "Alicia"
print(u1.name) # Alicia
print(u2.name) # Bob - не затронут
Lookup-цепочка
Когда обращаешься к u.attr, Python ищет в порядке:
u.__dict__(instance attributes)User.__dict__(class attributes)- родители (см. урок про наследование и MRO)
AttributeErrorесли нигде нет
class Counter:
count = 0 # class attribute
def increment(self):
self.count += 1 # СЮРПРИЗ - создаёт instance attribute
c1 = Counter()
c2 = Counter()
c1.increment()
print(c1.count) # 1 - instance attribute
print(c2.count) # 0 - всё ещё class attribute!
print(Counter.count) # 0
self.count += 1 это self.count = self.count + 1. Чтение self.count находит class attribute (0), сложение даёт 1, присваивание создаёт instance attribute count=1. После этого на c1 instance attribute заслоняет class attribute.
Это типичная ловушка. Для счётчика на уровне класса:
class Counter:
count = 0
@classmethod
def increment(cls):
cls.count += 1
Counter.increment()
Counter.increment()
print(Counter.count) # 2
Mutable class attributes - опасно
class User:
permissions = [] # ОПАСНО - общий список для всех!
def add_permission(self, perm):
self.permissions.append(perm)
u1 = User()
u2 = User()
u1.add_permission("read")
print(u2.permissions) # ['read'] - неожиданно!
u1.permissions.append(...) не создаёт instance attribute - модифицирует общий class attribute. Все инстансы делят один список. Те же грабли что mutable defaults у функций.
Правильно:
class User:
def __init__(self):
self.permissions = [] # своё у каждого инстанса
Или использовать tuple/frozenset для immutable defaults:
class User:
default_permissions = ("read",) # immutable - безопасно
classmethod - метод класса
@classmethod получает класс (cls) вместо инстанса. Полезен для:
- Alternative constructors (factory methods)
- Методов работающих с классом, не с конкретным инстансом
from datetime import date
class User:
def __init__(self, name, birth_year):
self.name = name
self.birth_year = birth_year
@classmethod
def from_birth_year(cls, name, birth_year):
return cls(name, birth_year)
@classmethod
def from_string(cls, s):
# "Alice|1990"
name, year = s.split("|")
return cls(name, int(year))
@classmethod
def adult_age(cls):
return 18
# Использование
u1 = User("Alice", 1990)
u2 = User.from_birth_year("Bob", 1985)
u3 = User.from_string("Charlie|2000")
Главное преимущество - cls это реальный класс из которого вызывают, не зашитое имя. Это критично для inheritance:
class AdminUser(User):
pass
au = AdminUser.from_string("Dave|1988")
print(type(au)) # AdminUser, не User
Если бы в from_string использовалось User(...) вместо cls(...), alternative constructor возвращал бы User для всех подклассов. С cls каждый подкласс получает свой тип.
staticmethod - функция в namespace класса
@staticmethod не получает ни self, ни cls. Это обычная функция, живущая внутри класса для логической группировки:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@staticmethod
def celsius_to_fahrenheit(c):
return c * 9 / 5 + 32
@staticmethod
def is_valid_celsius(value):
return -273.15 <= value <= 1e9
t = Temperature(25)
Temperature.celsius_to_fahrenheit(100) # 212
t.celsius_to_fahrenheit(100) # тоже работает
Temperature.is_valid_celsius(-300) # False
@staticmethod нужен реже чем @classmethod. Часто проще сделать обычную функцию модуля - она не требует префикса класса.
property - метод как атрибут
@property превращает метод в свойство - доступ как к атрибуту:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.area) # 78.539... - без скобок!
print(c.diameter) # 10
c.area = 100 # AttributeError - read-only без setter
Полезно для:
- Вычисляемых значений (area из radius)
- Lazy computation (с кешированием через
@cached_property) - Контроля над "приватным" атрибутом
property с setter
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Ниже абсолютного нуля")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5 / 9 # вызывает celsius.setter с валидацией
t = Temperature(25)
print(t.fahrenheit) # 77.0
t.celsius = 30
t.fahrenheit = 100 # через setter
print(t.celsius) # 37.77...
t.celsius = -300 # ValueError
Setter получает имя через @<имя_property>.setter. Можно добавлять @property.deleter для контроля удаления.
Используй property когда:
- Нужна валидация при записи
- Значение вычисляется из других атрибутов
- Контролируешь доступ к "приватному" полю
Иначе - обычный атрибут понятнее.
cached_property - lazy + кеш
С Python 3.8 есть @functools.cached_property - вычисляет один раз, кеширует на инстансе. Это обычный декоратор, просто из стандартной библиотеки:
from functools import cached_property
class DataAnalyzer:
def __init__(self, data):
self.data = data
@cached_property
def statistics(self):
# Дорогое вычисление - выполнится один раз
print("Computing...")
return {
"mean": sum(self.data) / len(self.data),
"max": max(self.data),
"min": min(self.data),
}
a = DataAnalyzer([1, 2, 3, 4, 5])
print(a.statistics) # Computing... {'mean': 3.0, ...}
print(a.statistics) # без Computing - из кеша
Кеш живёт на инстансе - удаляется когда инстанс удаляется. Для invalidation - просто del a.statistics.
Приватные атрибуты - конвенция
В Python нет приватных атрибутов в строгом смысле. Есть две конвенции:
class User:
def __init__(self, name):
self.name = name # public - доступен снаружи
self._cache = {} # "protected" - конвенция, не запрещено
self.__secret = "shh" # "private" - name mangling
u = User("Alice")
print(u.name) # OK
print(u._cache) # работает, но "не должен"
print(u.__secret) # AttributeError!
print(u._User__secret) # работает - name mangled
_attr- конвенция: «приватное, не трогай извне». Просто соглашение, доступ не запрещён.__attr- name mangling: Python переименовывает в_ClassName__attr. Это не для приватности, а для избежания конфликтов в иерархии наследования. Использовать редко.
В реальном коде чаще _attr или вообще без подчёркивания (Python community говорит «consenting adults»).
slots - оптимизация памяти
По умолчанию атрибуты хранятся в __dict__ (словарь). Для миллионов объектов это много памяти. __slots__ ограничивает атрибуты заранее объявленным списком, экономит память:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.x = 10 # OK
p.z = 30 # AttributeError - z не в slots
Преимущества:
- Меньше памяти (~40-60% экономии)
- Чуть быстрее доступ к атрибутам
- Защита от опечаток (нельзя случайно создать новый атрибут)
Недостатки:
- Нельзя добавлять атрибуты динамически
- Сложнее наследование (особенно multiple)
- Не работает с
__dict__-based features
Для большинства классов не нужен. Используй в hot path или для большого количества инстансов.
Сравнение типов методов
| Декоратор | Первый параметр | Доступ к | Использование |
|---|---|---|---|
| (нет) | self | инстансу и классу | Обычные методы экземпляра |
@classmethod | cls | только классу | Alternative constructors, factory methods |
@staticmethod | (ничего) | ни тому ни другому | Утилитарные функции в namespace класса |
@property | self | инстансу | Вычисляемые/контролируемые атрибуты |
Мини-задание
- Class attribute vs instance attribute:
class Counter:
total_created = 0 # class attribute
def __init__(self):
Counter.total_created += 1 # явное обращение к классу
self.id = Counter.total_created # своё у каждого
c1 = Counter()
c2 = Counter()
c3 = Counter()
print(c1.id, c2.id, c3.id) # 1 2 3
print(Counter.total_created) # 3
- Alternative constructor через classmethod:
from datetime import datetime
class Event:
def __init__(self, name, timestamp):
self.name = name
self.timestamp = timestamp
@classmethod
def now(cls, name):
return cls(name, datetime.now())
@classmethod
def from_iso(cls, name, iso_string):
return cls(name, datetime.fromisoformat(iso_string))
def __repr__(self):
return f"Event({self.name!r}, {self.timestamp.isoformat()})"
e1 = Event.now("login")
e2 = Event.from_iso("logout", "2026-05-27T15:00:00")
print(e1)
print(e2)
- Property с валидацией:
class Account:
def __init__(self, balance=0):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Баланс не может быть отрицательным")
self._balance = value
a = Account(100)
print(a.balance) # 100
a.balance = 200
print(a.balance) # 200
a.balance = -50 # ValueError
Что дальше
Освоили виды атрибутов и методов. Если класс нужен только чтобы хранить поля, большую часть этого кода заменит @dataclass. В следующем уроке - dunder-методы: str, eq, hash, len, getitem, iter и другие - как сделать свои классы «правильно ведущими себя» в Python.