Интроспекция в Python на реальных примерах: как код узнает сам о себе
В Python код является данными. Функции, классы, модули и даже стек вызовов можно исследовать во время выполнения программы. Этот механизм называется интроспекцией.
Интроспекция активно используется во фреймворках, логировании, тестах, dependency injection контейнерах и отладчиках. При этом многие разработчики пользуются ей неосознанно.
Разберем, что это такое, зачем нужно и как применяется на практике.
Что такое интроспекция
Интроспекция это способность программы получать информацию о своей структуре и объектах во время выполнения.
Python позволяет узнать:
- тип объекта
- его атрибуты и методы
- сигнатуру функции
- параметры вызова
- источник вызова функции
- документацию и аннотации типов
При этом не требуется заранее знать конкретный объект.
Базовые инструменты интроспекции:
1. type() узнать тип объекта:
x = 10
print(type(x)) # <class 'int'>
Объяснить с
Используется при валидации данных, логировании, сериализации.
2. id() идентификатор объекта в памяти:
a = []
b = a
print(id(a) == id(b)) # True
Объяснить с
Помогает определить, ссылаются ли переменные на один и тот же объект.
3. dir() что содержит объект:
print(dir("hello")) # ['__add__', '__class__', '__contains__', ...]
Объяснить с
Возвращает список атрибутов и методов, доступных объекту.
Часто используется в REPL и во время отладки.
Проверка структуры объекта:
hasattr, getattr, setattr:
- hasattr(obj, name)
Проверяет, существует ли у объекта атрибут с указанным именем. ВозвращаетTrueилиFalse. - getattr(obj, name, default)
Возвращает значение атрибута объекта по имени. Если атрибут не найден, может вернуть значение по умолчанию или выброситьAttributeError. - setattr(obj, name, value)
Динамически добавляет новый атрибут объекту или изменяет значение существующего атрибута.
class User:
def __init__(self, name):
self.name = name
u = User("Andrey")
print(hasattr(u, "name")) # True
print(getattr(u, "name")) # Andrey
Объяснить с
Реальный кейс это работа с динамическими объектами, JSON-данными, ORM.
Интроспекция функций:
Имя функции и документация:
- "__name__" позволяет узнать имя функции
- "__doc__" позволяет узнать документацию функции
def calculate(a, b):
"""Складывает два числа"""
return a + b
print(calculate.__name__) # calculate
print(calculate.__doc__) # Складывает два числа
Объяснить с
Применяется при генерации документации и логировании.
Аннотации типов:
- "__annotations__" это служебный атрибут Python, в котором хранятся аннотации типов для функций, методов, классов и переменных.
def process(data: list[str]) -> int:
return len(data)
print(process.__annotations__) # {'data': list[str], 'return': <class 'int'>}
Объяснить с
Фреймворки вроде FastAPI и Pydantic строят свою работу именно на этом механизме.
Модуль inspect: продвинутая интроспекция:
Сигнатура функции:
inspect.signature()анализирует вызываемый объект и возвращает его сигнатуру, то есть формальное описание того, какие аргументы принимает функция и что у них за параметры.
import inspect
def login(user: str, password: str, remember=False):
pass
sig = inspect.signature(login)
print(sig) # (user: str, password: str, remember=False)
Объяснить с
Используется для:
- валидации аргументов
- dependency injection
- автогенерации CLI и API
Получение исходного кода функции
inspect.getsource()возвращает исходный код объекта в виде строки.
import inspect
print(inspect.getsource(login))
'''
def login(user: str, password: str, remember=False):
pass
'''
Объяснить с
Работает только если исходный код доступен и не был скомпилирован.
Интроспекция классов:
Атрибуты и методы класса:
Service.__dict__содержит все атрибуты и методы, определенные в классе, в виде словаря. Ключами являются имена атрибутов, а значениями сами объекты.
class Service:
version = "1.0"
def run(self):
pass
print(Service.__dict__.keys())
# dict_keys(['__module__', '__firstlineno__', 'version', 'run', '__static_attributes__', '__dict__', '__weakref__', '__doc__'])
Объяснить с
Позволяет динамически регистрировать методы, реализовывать плагины и анализировать API.
Интроспекция стека вызовов:
Определение вызывающей функции
import inspect
def who_called_me():
frame = inspect.currentframe()
caller = frame.f_back
print(caller.f_code.co_name)
def test():
who_called_me()
test() # test
Объяснить с
Что здесь вообще происходит?
inspect.currentframe()возвращает объект текущего фрейма выполнения, то есть информацию о том месте, где сейчас выполняется код.frame.f_backсодержит ссылку на предыдущий фрейм в стеке вызовов, то есть на функцию, которая вызвала текущую.caller.f_code.co_nameэто имя функции, связанной с этим фреймом.
В результате функция who_called_me определяет, кто именно ее вызвал, и выводит имя этой функции.
Где интроспекция применяется в реальности:
- FastAPI: анализ сигнатур и аннотаций
- pytest: автоматический поиск тестов
- ORM (Django, SQLAlchemy): анализ моделей
- dependency injection контейнеры
- CLI-генераторы
- отладчики и логгеры
Когда интроспекцию использовать не стоит:
Не рекомендуется применять интроспекцию:
- в горячих циклах из-за накладных расходов
- для избыточной магии без документации
- если задачу можно решить явным кодом
Интроспекция мощный инструмент, но чрезмерное использование ухудшает читаемость и поддержку кода.
Как результат:
Интроспекция в Python:
- делает код гибким
- позволяет создавать фреймворки
- дает доступ к внутреннему устройству runtime
Использовать ее стоит осознанно и точечно.
Источник: https://habr.com/ru/articles/991498/
