В 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

Использовать ее стоит осознанно и точечно.

Дополнительные бесплатные материалы и практические уроки по Python доступны в моем образовательном пространстве на Stepik.