Атрибуты функций: хранение данных и настройка поведения

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

Основные способы управления атрибутами функций

Функции в Python являются полноправными объектами, поэтому им можно присваивать произвольные атрибуты. Это позволяет хранить метаданные, настраивать поведение без создания отдельного класса или глобальных переменных. В данном разделе рассмотрены основные приёмы добавления, чтения и удаления атрибутов, а также типичные ошибки.

Прямое присваивание атрибута

Наиболее простой и быстрый способ – присвоить значение непосредственно через точечную нотацию:

def hello():
    print("Привет")
hello.lang = "Russian"
print(hello.lang)

атрибуты функции python (атрибуты функции в python)

Russian

Такой код напрямую добавляет запись в словарь __dict__ функции. Метод эффективен и интуитивно понятен. После присваивания атрибут можно читать, изменять или удалять оператором del.

Типичные ошибки и их решения:

  • Переопределение встроенных атрибутов – если присвоить значение атрибуту __name__, __doc__ или __code__, интерпретатор не выдаст ошибку, но такое вмешательство способно нарушить работу интроспекции и декораторов. Рекомендуется использовать уникальные имена без двойных подчёркиваний.
  • Попытка обратиться к несуществующему атрибуту – вызовет AttributeError. Для безопасного чтения применяют getattr(func, 'attr', default).
  • Модификация атрибута во время выполнения – если функция используется в нескольких местах, все они увидят изменение атрибута. Для изолированного состояния рекомендуется замыкание или класс.

Как создать счётчик вызовов без глобальной переменной?

Атрибут функции отлично подходит для хранения числа выполненных вызовов:

def counter():
    counter.calls += 1
    print(f"Вызов номер {counter.calls}")
counter.calls = 0
counter()
counter()
Вызов номер 1
Вызов номер 2

Обратите внимание: инициализация атрибута выполняется вне функции. Если забыть это сделать, при первом вызове возникнет AttributeError. Альтернатива – проверять существование атрибута внутри функции с помощью hasattr.

Проблема: при повторном импорте модуля счётчик сбросится. Для долгоживущих приложений атрибут функции может оказаться недостаточно устойчивым; в таких случаях лучше использовать замыкание или атрибут объекта более высокого уровня.

Как получить все атрибуты функции?

Пользовательские атрибуты хранятся в __dict__ функции. Встроенные атрибуты (например, __name__) в этом словаре не отображаются. Для их просмотра служит функция dir:

def example():
    pass
example.custom_attr = 42
print(example.__dict__)  # {'custom_attr': 42}
print([name for name in dir(example) if not name.startswith('__')])
{'custom_attr': 42}
['custom_attr']

При получении атрибутов через dir следует помнить, что список включает также методы, унаследованные от object.

Как добавить атрибут с помощью setattr?

Функция setattr делает код более динамичным, позволяя задавать имя атрибута строкой:

def work():
    pass
setattr(work, 'version', 2.0)
print(getattr(work, 'version'))  # 2.0
2.0

Это удобно, когда имя атрибута формируется программно. Для массового копирования атрибутов можно использовать цикл по словарю.

Цели использования атрибутов функций:

  • Хранение конфигурационных данных (приоритет, тайм-аут, язык).
  • Сбор статистики (количество вызовов, время выполнения).
  • Реализация паттерна «функция-диспетчер» с регистрацией обработчиков.
  • Передача метаинформации декораторам и фреймворкам.

Практические примеры с атрибутами функций

В этом разделе приведены более сложные сценарии, демонстрирующие гибкость атрибутов функций. Каждый пример сопровождается кодом и выводом.

Декоратор для подсчёта вызовов с сохранением атрибута

Часто требуется обернуть функцию так, чтобы она автоматически подсчитывала количество вызовов. Атрибут calls присваивается обёртке:

Пример
def count_calls(func):
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def say_hello(name):
    print(f"Привет, {name}")

say_hello("Анна")
say_hello("Иван")
print(f"Вызовов: {say_hello.calls}")
Привет, Анна
Привет, Иван
Вызовов: 2

Обратите внимание: wrapper возвращается как новая функция, поэтому её атрибут calls изолирован от исходной функции. Если нужно сохранить и другие атрибуты оригинала, используйте functools.wraps.

Кэширование результатов (мемоизация) с помощью атрибута cache

Атрибут может хранить словарь уже вычисленных значений. Это ускоряет повторные вызовы с теми же аргументами:

Пример
def factorial(n):
    if not hasattr(factorial, 'cache'):
        factorial.cache = {}
    if n in factorial.cache:
        return factorial.cache[n]
    if n == 0:
        result = 1
    else:
        result = n * factorial(n - 1)
    factorial.cache[n] = result
    return result

print(factorial(5))
print(factorial(5))
print(factorial.cache)
120
120
{0: 1, 1: 1, 2: 2, 3: 6, 4: 24, 5: 120}

Кэш инициализируется при первом вызове. Метод подходит для чистых функций без побочных эффектов.

Установка атрибутов через декоратор для настройки поведения

Декоратор может добавлять функции метаданные, влияющие на логику работы приложения:

Пример
def set_priority(priority):
    def decorator(func):
        func.priority = priority
        return func
    return decorator

@set_priority(1)
def process_a():
    pass

@set_priority(2)
def process_b():
    pass

print(process_a.priority)
print(process_b.priority)
1
2

В коде, определяющем порядок выполнения, можно отфильтровать функции по значению атрибута priority.

Функция-диспетчер с регистрацией подфункций

Атрибуты позволяют централизованно собирать обработчики событий:

Пример
def dispatcher():
    pass

dispatcher.handlers = {}

def register(event):
    def decorator(func):
        dispatcher.handlers[event] = func
        return func
    return decorator

@register('start')
def start_handler():
    print("Старт")

@register('stop')
def stop_handler():
    print("Стоп")

print(dispatcher.handlers)
dispatcher.handlers['start']()
{'start': , 'stop': }
Старт

Атрибут handlers живёт на уровне самого диспетчера, что удобно для расширяемых систем.

Хранение метаинформации с помощью __annotations__

Аннотации типов тоже являются атрибутами функции. Их можно дополнить пользовательскими данными:

Пример
def calculate(a: int, b: int) -> int:
    return a + b

calculate.info = "Сложение чисел"
print(calculate.__annotations__)
print(calculate.info)
{'a': int, 'b': int, 'return': int}
Сложение чисел

Комбинация встроенных и пользовательских атрибутов даёт полную картину о назначении функции.

Сохранение атрибутов при использовании functools.wraps

Декораторы, возвращающие обёртку, часто теряют атрибуты исходной функции. Функция wraps копирует их автоматически:

Пример
from functools import wraps

def add_metadata(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    wrapper.extra = "metadata"
    return wrapper

@add_metadata
def example():
    """Documentation."""
    pass

print(example.__name__)   # example, не wrapper
print(example.__doc__)    # Documentation.
print(example.extra)      # metadata
example
Documentation.
metadata

Без wraps пришлось бы вручную копировать __name__, __doc__ и другие атрибуты.

Динамическое изменение атрибута функции из другого модуля

Атрибуты доступны глобально, если импортировать функцию:

Пример
# module_a.py
def task():
    print(f"Config: {task.config}")

task.config = "default"

# module_b.py
import module_a
module_a.task.config = "custom"
module_a.task()

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

Атрибуты функции в Python - comments

En
атрибуты функции python (python)