Python объекты: внутреннее устройство и приемы работы

Раздел: Продвинутый Python -> Объектная модель

Основы объектной модели Python

В Python любая сущность, будь то число, строка, функция или класс, является объектом. Каждый объект обладает тремя фундаментальными характеристиками: идентификатор (уникальный и неизменный на всём протяжении жизни объекта), тип (определяет набор допустимых операций) и значение (может меняться для изменяемых типов). Для получения этих характеристик используются встроенные функции id(), type() и isinstance().
a = [1, 2, 3]
print(id(a))  # уникальный номер объекта
print(type(a))  # <class 'list'>
print(isinstance(a, (list, tuple)))  # True

объекты языка python (объекты в языке python)

Типичная ошибка: путаница между операторами is и ==. Первый сравнивает идентификаторы, второй - значения. Для небольших целых чисел (от -5 до 256) Python кеширует объекты, поэтому is может дать True, но полагаться на это не стоит. Для больших чисел is вернёт False даже при одинаковых значениях.

Как динамически добавить атрибут объекту?

Атрибуты можно добавлять в любой момент, если класс не использует __slots__. Самый простой способ - прямое присваивание: obj.new_attr = value. Функция setattr() делает то же самое с возможностью динамического имени. Также можно напрямую изменить словарь obj.__dict__.
class Point: pass
p = Point()
p.x = 10  # присваивание
setattr(p, 'y', 20)  # через setattr
p.__dict__['z'] = 30  # напрямую
print(p.__dict__)  # {'x': 10, 'y': 20, 'z': 30}
Проблема: если у класса определён __slots__, то добавление атрибута, отсутствующего в разрешённом списке, вызовет AttributeError. Исключение появляется и при попытке использовать __dict__.

Как ограничить набор допустимых атрибутов экземпляра?

Для этого применяется атрибут класса __slots__. Он задаёт кортеж строк - разрешённых имён атрибутов. Python создаёт для каждого экземпляра дескрипторы для этих имён, а словарь __dict__ не создаётся, что уменьшает потребление памяти.
class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
p = Point(1, 2)
p.z = 3  # AttributeError: 'Point' object has no attribute 'z'
Важно: __slots__ не наследуется автоматически. Если подкласс не определяет свой __slots__, он получит __dict__. Чтобы сохранить ограничение, каждый наследный класс должен явно указать свой __slots__ (возможно, пустой).

Как управлять доступом к атрибутам с проверкой?

Механизм property позволяет перехватывать чтение, запись и удаление атрибута, выполняя произвольный код. Свойство создаётся декоратором @property для геттера, а сеттер и делитер оформляются с помощью @имя.setter и @имя.deleter.
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

t = Temperature(25)
t.celsius = 30  # работает
t.celsius = -300  # ValueError
Ошибка: попытка привязать property напрямую к экземпляру не даёт эффекта - property является дескриптором, который работает только на уровне класса. Присваивание obj.prop = value просто создаст атрибут экземпляра, перекрывающий property.

Как создать собственный тип объекта?

Классы в Python сами являются объектами типа type. Динамически создать класс можно вызовом type('Name', (bases,), dict). Более мощный способ - определить метакласс, потомок type, и переопределить его метод __new__.
# Динамическое создание класса
MyClass = type('MyClass', (object,), {'a': 1})
obj = MyClass()
print(obj.a)  # 1

# Метакласс, добавляющий префикс к именам методов
class PrefixMeta(type):
    def __new__(mcs, name, bases, namespace):
        new_namespace = {}
        for k, v in namespace.items():
            if callable(v) and not k.startswith('_'):
                new_namespace['custom_' + k] = v
            else:
                new_namespace[k] = v
        return super().__new__(mcs, name, bases, new_namespace)

class MyService(metaclass=PrefixMeta):
    def run(self):
        return 'running'

s = MyService()
print(s.custom_run())  # 'running'
Сложность: метаклассы делают код менее прозрачным и могут затруднить отладку. Их следует применять только когда без них невозможно обойтись (например, для ORM, сериализации, регистрации подклассов).

Как проверить принадлежность к классу с учётом иерархии?

Функция isinstance(obj, cls) возвращает True, если объект является экземпляром указанного класса или любого его подкласса. Более строгая проверка - type(obj) is cls - не учитывает наследование. issubclass(cls, base) проверяет отношение наследования между классами.
class Animal: pass
class Dog(Animal): pass
d = Dog()
print(isinstance(d, Animal))  # True
print(type(d) is Animal)      # False
print(issubclass(Dog, Animal))# True
Распространённая ошибка: использование type(obj) == SomeClass для проверки типа. Это игнорирует наследование и может дать ложноотрицательный результат, если объект создан от подкласса. Рекомендуется всегда использовать isinstance.

Расширенные примеры работы с объектами

Дескриптор с валидацией типа

Дескрипторы позволяют перехватывать доступ к атрибутам на уровне класса. Следующий пример показывает дескриптор, который проверяет тип присваиваемого значения.

Пример
class TypedField:
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None  # будет установлен в __set_name__

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Ожидается {self.expected_type.__name__}, получен {type(value).__name__}')
        obj.__dict__[self.name] = value

    def __delete__(self, obj):
        del obj.__dict__[self.name]

class Person:
    name = TypedField(str)
    age = TypedField(int)

    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person('Alice', 30)
print(p.name, p.age)  # Alice 30
p.age = 'тридцать'    # TypeError: Ожидается int, получен str
Alice 30
Traceback (most recent call last):
  ...
TypeError: Ожидается int, получен str

Прокси-объект через __getattr__ и __setattr__

Класс Proxy перенаправляет все обращения к атрибутам на другой объект, сохраняя при этом собственный приватный атрибут.

Пример
class Proxy:
    def __init__(self, target):
        # используем object.__setattr__, чтобы избежать рекурсии
        object.__setattr__(self, '_target', target)

    def __getattr__(self, name):
        return getattr(self._target, name)

    def __setattr__(self, name, value):
        if name == '_target':
            object.__setattr__(self, name, value)
        else:
            setattr(self._target, name, value)

    def __delattr__(self, name):
        delattr(self._target, name)

class Data:
    def __init__(self):
        self.value = 0

d = Data()
proxy = Proxy(d)
proxy.value = 42
print(d.value)          # 42
print(proxy.value)      # 42
42
42

Метакласс для автоматической регистрации подклассов

Метакласс может автоматически записывать все созданные подклассы в словарь, что удобно для плагинов или фабрик.

Пример
class RegistryMeta(type):
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        RegistryMeta.registry[name] = cls
        return cls

class Base(metaclass=RegistryMeta):
    pass

class PluginA(Base):
    pass

class PluginB(Base):
    pass

print(RegistryMeta.registry)
{'Base': <class '__main__.Base'>, 'PluginA': <class '__main__.PluginA'>, 'PluginB': <class '__main__.PluginB'>}

Обратите внимание: сам базовый класс тоже попадает в реестр. Чтобы исключить его, можно добавить условие в __new__, проверяя, что bases не пуст.

Объекты в языке Python - comments

En
объекты языка python (python)