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, получен strAlice 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) # 4242 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)