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 _():, воспринимайте это как «вызываемый объект, чьё имя сознательно несущественно». Сосредоточьтесь на декораторе, который его принимает: именно там принимаются решения о вызове и управлении потоком — либо прямым вызовом объекта функции, либо через условный примитив. При аудите сначала отслеживайте декораторы, а не текстовые упоминания имени функции.