2026, Jan 09 00:04

Почему def _() в Python выполняется: роль декоратора и условного запуска

Разбираем, почему функция с именем def _() выполняется в Python: декоратор получает объект функции и вызывает его по условию (jax.lax.cond). С примерами.

Имена из одного нижнего подчёркивания на первый взгляд могут сбить с толку, особенно в декорированных местах вызова. Видите def _(): — и сразу возникает вопрос, как эта функция вообще запускается. Короткий ответ: по имени к ней обращаться не предполагается, но она всё равно выполняется, потому что декоратор хранит объект функции и вызывает его напрямую.

Пример, который вызывает вопрос

В шаблоне ниже функция определяется внутри другой функции и получает имя _. Реализация опирается на декоратор, который решает, запускать ли обёрнутый вызываемый объект.

import functools
import jax
from jax._src.pallas import helpers as pl_helpers
def execute_on_primary_core(axis_label: str):
  """Runs a function on the first core of the given axis."""
  core_count = jax.lax.axis_size(axis_label)
  if core_count == 1:
    return lambda g: g()
  def invoker(g):
    idx = jax.lax.axis_index(axis_label)
    @pl_helpers.when(idx == 0)
    @functools.wraps(g)
    def _():  # Как это вызывается?
      return g()
  return invoker

Что на самом деле происходит

Имя, состоящее только из _, — это не то же самое, что имя с префиксом _. Использование _ как полного имени — это условность: «имя требуется синтаксисом, но использоваться не будет». В этой ситуации имя не играет роли, потому что объект функции передаётся прямо в декоратор, и именно декоратор отвечает за вызов при выполнении условия.

Реализация соответствующего декоратора наглядно показывает механизм: он получает объект функции, а не её имя, и вызывает его при выполнении условия.

import jax
def when_conditionally(predicate):
  def wrap(fn):
    if isinstance(predicate, bool):
      if predicate:
        fn()
    else:
      jax.lax.cond(predicate, fn, lambda: None)
  return wrap

Декоратор захватывает вызываемый объект как fn и решает, запускать его сразу или провести через jax.lax.cond. Ни на одном этапе ему не требуется искать функцию по идентификатору. Поэтому инструменты, которые ищут прямые обращения к _, не найдут места вызова: вызов происходит через замыкание декоратора.

Как рассуждать о «починке»

В управлении потоком исполнения исправлять нечего. Важно понять, что именно декоратор управляет запуском. Если вам удобнее видеть ту же идею с именами, которые подчёркивают замысел, вот эквивалентная перепись без изменения поведения.

import functools
import jax
# Семантика та же, что у упомянутого декоратора
def run_if(predicate):
  def attach(callable_obj):
    if isinstance(predicate, bool):
      if predicate:
        callable_obj()
    else:
      jax.lax.cond(predicate, callable_obj, lambda: None)
  return attach
# Та же структура, что и в предыдущем примере, отличаются лишь имена
def apply_on_first(axis_token: str):
  total = jax.lax.axis_size(axis_token)
  if total == 1:
    return lambda op: op()
  def binder(op):
    position = jax.lax.axis_index(axis_token)
    @run_if(position == 0)
    @functools.wraps(op)
    def _():
      return op()
  return binder

Почему это важно

Понимание конвенции с одинарным подчёркиванием избавляет от «охоты за привидениями» при поиске по коду или в ходе аудита. Если вы сканируете вызовы _, вы их не найдёте, потому что путь вызова уже контролируется декоратором. Это помогает рассуждать о порядке выполнения, побочных эффектах и характеристиках производительности в проектах, которые полагаются на декораторы и условное выполнение.

Выводы

Если в современном Python встречаете def _():, воспринимайте это как «вызываемый объект, чьё имя сознательно несущественно». Сосредоточьтесь на декораторе, который его принимает: именно там принимаются решения о вызове и управлении потоком — либо прямым вызовом объекта функции, либо через условный примитив. При аудите сначала отслеживайте декораторы, а не текстовые упоминания имени функции.