Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Пётр Андреев (МФТИ, лектор по курсу Advanced Py...

Пётр Андреев (МФТИ, лектор по курсу Advanced Python). Дебри Python или как работает повседневный Python: что происходит на самом деле

Что происходит, когда вы запускаете CPython-скрипт, и как он запускается на C? Разберём, как код превращается в байткод: все этапы и C-реализация. GIL: обсудим более подробно в сборках с GIL.

Видео: https://moscowpython.ru/meetup/102/python-insides/

Moscow Python: http://moscowpython.ru
Курсы Learn Python: http://learn.python.ru
Moscow Python Podcast: http://podcast.python.ru
Заявки на доклады: https://bit.ly/mp-speaker

Avatar for Moscow Python Meetup

Moscow Python Meetup

June 24, 2025
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. Agenda 2 Дебри Python. Начало. автор tg: @PyotrAndreev • Byte-code

    • Что происходит, когда вы запускаете CPython-скрипт • GIL
  2. Byte-code 3 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 10 # строка 2 b = 20 # строка 3 return a + b # строка 4 Какой-то код “If the implementation is hard to explain, it's a bad idea.” – Zen CPython
  3. Byte-code 4 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 10 # строка 2 b = 20 # строка 3 return a + b # строка 4 Множество CPU: - инструкции - архитектура OS: - реализации Какой-то код x86-64 ARM RISC-V Linux Windows www.python.org
  4. Byte-code 5 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 10 # строка 2 b = 20 # строка 3 return a + b # строка 4 Множество CPU: - инструкции - архитектура OS: - реализации Какой-то код x86-64 ARM RISC-V Linux Windows >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. …
  5. Byte-code 6 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций
  6. Byte-code 7 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций Больше абстракций ***.py ***.pyc Byte-code ***.pyc – отображение исходного кода через ограниченный набор opt.codes (operation code) Хранится в __pycache__
  7. Byte-code 8 Что это? автор tg: @PyotrAndreev def main(): #

    строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций Больше абстракций ***.pyc ***.py ***.pyc – отображение исходного кода через ограниченный набор opt.codes (operation code) Хранится в __pycache__ 3.14.0b2 [GCC 13.3.0] >>> import dis >>> len(dis.opnames) 267 Что за множество? Byte-code
  8. Byte-code 9 Задача преобразования кода автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 ? Какой-то код ***.pyc ***.py Byte-code doc: opt.code GitHub: opt.code
  9. Byte-code 10 Задача преобразования кода автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 ? Какой-то код ***.pyc ***.py Byte-code Вид ***.pyc 3.14.0b2 [GCC 13.3.0] >>> main.__code__ <code object main at 0x76b0916b9c50, … > types.CodeType 3.14.0b2 [GCC 13.3.0] >>> main.__code__.co_code b'\x80\x00^\x05p\x00^\np\x01W\x01,\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00' Последовательность opt.codes с аргументами, за которыми стоит уже С-код doc: opt.code GitHub: opt.code Диспатчинг от PVM: _PyEval_EvalFrameDefault: • читает byte-code • по opt.code находит соответствующий C-код • вызывает его • управляет стеком, лок.переменными … • возвращает результат: PyObject* или NULL
  10. Byte-code 11 Задача преобразования кода автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 ? Какой-то код ***.pyc ***.py Byte-code doc: opt.code GitHub: opt.code В человеко-читабельном виде: >>> from dis import dis >>> dis(main)
  11. Byte-code 12 Задача преобразования кода автор tg: @PyotrAndreev doc: opt.code

    GitHub: opt.code 1 0 RESUME 0 2 2 LOAD_CONST 1 (5) 4 STORE_FAST 0 (a) 3 6 LOAD_CONST 2 (10) 8 STORE_FAST 1 (b) 4 10 LOAD_FAST 0 (a) 12 LOAD_FAST 1 (b) 14 BINARY_OP 0 (+) 18 RETURN_VALUE Инициализация: (>= Python 3.11) При вызове ф-ии _PyInterpreterFrame /+InternalDocs/ создаёт/переиспользует фрейм, инструкция RESUME 0 помогает инициализировать его для opt.codes Загружает константу (5) из таблицы констант ф-и main.__code__.co_consts и помещает ссылку на неё в стек Из вершины стека извлекается значение (ссылка на объект 5) и сохраняется в локальной переменной a. Локальные переменные хранятся в: main.__code__.co_varnames Индекс 0 -> переменная a в списке локальных переменных ф-ии Загружает значение лок.переменной a (ссылка на 5) на стек Бинарная операция над 2 верхними элементами стека (ссылки на 5 и на 10) Результат: новый объект (число 15) созданный в куче Результатом является новый объект (число 15), созданный в куче, ссылка который помещается обратно в стек. Извлекает значение с вершины стека и возвращает его как результат работы функции. номера строк исходного кода # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4
  12. Byte-code 13 Задача преобразования кода автор tg: @PyotrAndreev doc: opt.code

    GitHub: opt.code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 14 BINARY_OP 0 (+) 18 RETURN_VALUE 1. число 15 не создаётся 2. Что такое 0 (+) ?
  13. Byte-code 14 Задача преобразования кода автор tg: @PyotrAndreev doc: opt.code

    GitHub: opt.code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 14 BINARY_OP 0 (+) 18 RETURN_VALUE 1. число 15 не создаётся 2. Что такое 0 (+) ? family(BINARY_OP, INLINE_CACHE_ENTRIES_BINARY_OP) = { BINARY_OP_MULTIPLY_INT, BINARY_OP_ADD_INT, BINARY_OP_SUBTRACT_INT, BINARY_OP_MULTIPLY_FLOAT, BINARY_OP_ADD_FLOAT, ... }; CPython 3.15.0а0 GitHub: версия, семейство BINARY_OP: неспециализированная операция С 3.11 Specializing Adaptive Interpreter: PEP 659 INLINE_CACHE_ENTRIES_BINARY_OP (=5): число нулевых opt.codes (CACHE), которые стоят перед BINARY_OP
  14. Byte-code 15 Задача преобразования кода автор tg: @PyotrAndreev doc: opt.code

    GitHub: opt.code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 14 BINARY_OP 0 (+) 18 RETURN_VALUE 1. число 15 не создаётся 2. Что такое 0 (+) ? family(BINARY_OP, INLINE_CACHE_ENTRIES_BINARY_OP) = { BINARY_OP_MULTIPLY_INT, BINARY_OP_ADD_INT, BINARY_OP_SUBTRACT_INT, BINARY_OP_MULTIPLY_FLOAT, BINARY_OP_ADD_FLOAT, ... }; CPython 3.15.0а0 GitHub: версия, семейство BINARY_OP: неспециализированная операция С 3.11 Specializing Adaptive Interpreter: PEP 659 INLINE_CACHE_ENTRIES_BINARY_OP (=5): число нулевых opt.codes (CACHE), которые стоят перед BINARY_OP 12 CACHE # slot0 = –1 (защитный счётчик) 14 CACHE # slot1 = &PyLong_Type 16 CACHE # slot2 = &PyLong_Type 18 CACHE # slot3 = &fast_int_add 20 CACHE # slot4 = резерв (для будущего) 22 BINARY_OP_ADD_INT 5 12 CACHE # slot0 = 8 (счётчик) 14 CACHE # slot1 = None 16 CACHE # slot2 = None 18 CACHE # slot3 = None 20 CACHE # slot4 – резерв 22 BINARY_OP 5 # PyNumber_Add – медленно При промаха счётчик уменьшается; при пробитии порога → откат Byte-code
  15. Byte-code 16 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4
  16. Byte-code 17 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 1. Lexer токенизирует код: tokenize _PyTokenizer_Get (часть лексера) 2. Parser строит построение AST Abstract Syntax Trees: ast _PyPegen_ASTFromString 3. построение символьной таблицы: PySymtable_BuildObject ◦ обходит AST, помечает имена флагами local/global/cell/free ◦ записи в co_varnames/co_cellvars/co_freevars
  17. Byte-code 18 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 1. Lexer токенизирует код: tokenize 2. Parser строит построение AST Abstract Syntax Trees: ast 3. построение символьной таблицы a = 5 b = 10 c = a + b print(c) Пример
  18. Byte-code 19 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 1. Lexer токенизирует код: tokenize 2. Parser строит построение AST Abstract Syntax Trees: ast 3. построение символьной таблицы a (идентификатор), = (оператор присваивания), 5 (целое число), b (идентификатор), = (оператор присваивания), 10 (целое число), c (идентификатор), = (оператор присваивания), a (идентификатор), + (оператор сложения), b (идентификатор), print (функция), ( (открывающая скобка), c (идентификатор), ) (закрывающая скобка) a = 5 b = 10 c = a + b print(c) Пример Токены
  19. Byte-code 20 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 1. Lexer токенизирует код: tokenize 2. Parser строит построение AST Abstract Syntax Trees: ast 3. построение символьной таблицы a (идентификатор), = (оператор присваивания), 5 (целое число), b (идентификатор), = (оператор присваивания), 10 (целое число), c (идентификатор), = (оператор присваивания), a (идентификатор), + (оператор сложения), b (идентификатор), print (функция), ( (открывающая скобка), c (идентификатор), ) (закрывающая скобка) Assign (присваивание) ├── Name (a) └── Constant (5) Assign (присваивание) ├── Name (b) └── Constant (10) Assign (присваивание) ├── Name (c) └── BinOp (бинарная операция) ├── Name (a) └── Name (b) Call (вызов функции) ├── Name (print) └── Name (c) a = 5 b = 10 c = a + b print(c) Пример Токены Иерархическое дерево: • последовательность выполнения • отношения между объектами
  20. Byte-code 21 Задача преобразования кода автор tg: @PyotrAndreev ? Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4
  21. Byte-code 22 Задача преобразования кода автор tg: @PyotrAndreev ✔ Какой-то

    код ***.pyc ***.py Byte-code # CPython 3.11.13 [GCC 11.4.0] def main(): # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4
  22. Byte-code 23 Задача преобразования кода автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций Больше абстракций ***.py ***.pyc Byte-code
  23. Запуск программы 24 Немного исходников автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций Больше абстракций ***.py ***.pyc Byte-code Этапы запуска CPython
  24. Запуск программы 25 Немного исходников автор tg: @PyotrAndreev def main():

    # строка 1 a = 5 # строка 2 b = 10 # строка 3 return a + b # строка 4 Какой-то код x86-64 ARM RISC-V Linux Windows PVM “Simple is better than complex.” – Zen CPython Больше абстракций Больше абстракций ***.py ***.pyc Byte-code Этапы запуска CPython user@comp:~$ python3 foo.py ... • Unix находит исполняемый файл (ИФ): /usr/bin/python3, который содержит ф-ю main из Programs/python.c ... • ИФ вызывает main, что вызывает цепочку вызовов из Modules/main.c: Py_BytesMain → pymain_main → Py_RunMain → запуск главного цикла выполнения Python‑кода
  25. Запуск программы 26 Немного исходников автор tg: @PyotrAndreev main (Programs/python.c)

    │ └> Py_BytesMain(argc, argv) (Modules/main.c) │ └> pymain_main(&args) │ ├> status = pymain_init(args) # Инициализация интерпретатора │ └── (настройка PyConfig, путей, сигналов и т.д.) │ ├> Проверка статуса (если требуется завершение или ошибка) │ └> Py_RunMain() │ ├> pymain_run_python(&exitcode) │ ├> Обновление конфигурации и настройка путей │ ├> Выбор режима выполнения: │ │ • Если задана команда → pymain_run_command() │ │ • Если задан модуль → pymain_run_module() │ │ • Если задан файл → pymain_run_file() │ │ • Иначе чтение из stdin → pymain_run_stdin() │ └> pymain_repl(config, exitcode) # Запуск REPL (интерактивного режима) │ ├> Py_FinalizeEx() # Завершение интерпретатора │ └> pymain_free() # Освобождение глобальных ресурсов Этапы запуска CPython
  26. Byte-code 27 Задача преобразования кода автор tg: @PyotrAndreev run.py byte-code

    исполнение PVM Компьютер процессы -> потоки Запуск отдельного процесса под PVM исполнение файла Lexer -> токенизация Parser -> AST ... a = 5 b = 10 c = a + b print(c) a (идентификатор), = (оператор присваивания), 5 (целое число), b (идентификатор), = (оператор присваивания), 10 (целое число), c (идентификатор), = (оператор присваивания), a (идентификатор), + (оператор сложения), b (идентификатор), print (функция), ( (открывающая скобка), c (идентификатор),) (закрывающая скобка) Assign (присваивание) ├── Name (a) └── Constant (5) Assign (присваивание) ├── Name (b) └── Constant (10) Assign (присваивание) ├── Name (c) └── BinOp (бинарная операция) ├── Name (a) └── Name (b) Call (вызов функции) ├── Name (print) └── Name (c) b'\x97\x00d\x01}\x00d\x02}\x01|\x00|\x01z\x00\x00 \x00}\x02t\x01\x00\x00\x00\x00\x00\x00\x00\x00|\x 02\xab\x01\x00\x00\x00\x00\x00\x00\x01\x00y\x00' 0 RESUME 2 LOAD_CONST 4 STORE_FAST 6 LOAD_CONST 8 STORE_FAST 10 LOAD_FAST 12 LOAD_FAST 14 BINARY_OP 18 STORE_FAST 20 LOAD_GLOBAL 30 LOAD_FAST 32 CALL 40 POP_TOP 42 RETURN_CONST AST byte-code (.pyc) byte-code foo.py ‘дёргает’ Python C API
  27. Processes: • отдельное адресное пространство (виртуальная память) • изменения в

    памяти одного процесса не влияют на другие • обход GIL: модуль multiprocessing 28 Исполнение кода автор tg: @PyotrAndreev Threads: threading – интерфейс для работы • внутри процесса делят общее адресное пространство, но не стек • создание и переключение быстрее, чем между процессами • GIL threads & processes
  28. 29 автор tg: @PyotrAndreev threads & processes Что хотим? •

    Получить максимальную эффективность от CPU-bound задачах передаваемые данные необходимо сериализовать: дорого Processes: • отдельное адресное пространство (виртуальная память) • изменения в памяти одного процесса не влияют на другие • обход GIL: модуль multiprocessing Коммуникация между • Queues, Pipes, Managers(общие объекты) • Shared Memory – необходимы инструменты синхронизации, чтобы избежать race condition Threads: threading – интерфейс для работы • внутри процесса делят общее адресное пространство, но не стек • создание и переключение быстрее, чем между процессами • GIL Коммуникация между • Lock, RLock: блокировки для защиты критических секций • Condition, Semaphore, Event, Barrier ... pickle Исполнение кода
  29. 30 автор tg: @PyotrAndreev GIL (CPython) “Для … вычислительных задач

    … недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” типы параллелизма: arxiv timemodule.c: repo
  30. автор tg: @PyotrAndreev GIL (CPython) “Для … вычислительных задач …

    недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” 31 типы параллелизма: arxiv • гарантирует, что одновременно выполняется PVM только один поток байт-кода (-> Python C API) -> что обеспечивает потокобезопасность • Защищает race conditions на Python уровне, но не глубже: ◦ C-расширения могут предоставлять параллелизм в процессе timemodule.c: repo
  31. автор tg: @PyotrAndreev GIL (CPython) Как передаётся GIL? макросами •

    Py_BEGIN_ALLOW_THREADS освобождает GIL Py_END_ALLOW_THREADS захватывает GIL • GIL не будет захвачен другими потоками, если им это не требуется “Для … вычислительных задач … недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” 32 типы параллелизма: arxiv • гарантирует, что одновременно выполняется PVM только один поток байт-кода (-> Python C API) -> что обеспечивает потокобезопасность • Защищает race conditions на Python уровне, но не глубже: ◦ C-расширения могут предоставлять параллелизм в процессе time.sleep(10) timemodule.c: repo
  32. автор tg: @PyotrAndreev Пример: • исполняется внутренняя С-функция: time_sleep •

    ... • вычисляется время до, которого спим • используем Py_BEGIN_ALLOW_THREADS -> сохраняет состояние треда: PyEval_SaveThread -> освобождает GIL • вызывается OS блокирующая операция для усыпления потока на заданное время • OS пробуждает поток по истечению времени • другие потоки могут выполнять Python байт-код • используем Py_END_ALLOW_THREADS -> восстанавливаем состояние потока: PyEval_RestoreThread -> захват GIL потоком GIL (CPython) Как передаётся GIL? макросами • Py_BEGIN_ALLOW_THREADS освобождает GIL Py_END_ALLOW_THREADS захватывает GIL • GIL не будет захвачен другими потоками, если им это не требуется “Для … вычислительных задач … недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” 33 типы параллелизма: arxiv • гарантирует, что одновременно выполняется PVM только один поток байт-кода (-> Python C API) -> что обеспечивает потокобезопасность • Защищает race conditions на Python уровне, но не глубже: ◦ C-расширения могут предоставлять параллелизм в процессе time.sleep(10) timemodule.c: repo
  33. автор tg: @PyotrAndreev Пример: • исполняется внутренняя С-функция: time_sleep •

    ... • вычисляется время до, которого спим • используем Py_BEGIN_ALLOW_THREADS -> сохраняет состояние треда: PyEval_SaveThread -> освобождает GIL • вызывается OS блокирующая операция для усыпления потока на заданное время • OS пробуждает поток по истечению времени • другие потоки могут выполнять Python байт-код • используем Py_END_ALLOW_THREADS -> восстанавливаем состояние потока: PyEval_RestoreThread -> захват GIL потоком GIL (CPython) Как передаётся GIL? макросами • Py_BEGIN_ALLOW_THREADS освобождает GIL Py_END_ALLOW_THREADS захватывает GIL • GIL не будет захвачен другими потоками, если им это не требуется “Для … вычислительных задач … недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” 34 типы параллелизма: arxiv • гарантирует, что одновременно выполняется PVM только один поток байт-кода (-> Python C API) -> что обеспечивает потокобезопасность • Защищает race conditions на Python уровне, но не глубже: ◦ C-расширения могут предоставлять параллелизм в процессе time.sleep(10) timemodule.c: repo { Py_BEGIN_ALLOW_THREADS /* your code here */ Py_END_ALLOW_THREADS } Разворачивается в: { PyThreadState *_save; _save = PyEval_SaveThread(); /* your code here */ PyEval_RestoreThread(_save); } что есть:
  34. автор tg: @PyotrAndreev Пример: • исполняется внутренняя С-функция: time_sleep •

    ... • вычисляется время до, которого спим • используем Py_BEGIN_ALLOW_THREADS -> сохраняет состояние треда: PyEval_SaveThread -> освобождает GIL • вызывается OS блокирующая операция для усыпления потока на заданное время • OS пробуждает поток по истечению времени • другие потоки могут выполнять Python байт-код • используем Py_END_ALLOW_THREADS -> восстанавливаем состояние потока: PyEval_RestoreThread -> захват GIL потоком GIL (CPython) Как передаётся GIL? макросами • Py_BEGIN_ALLOW_THREADS освобождает GIL Py_END_ALLOW_THREADS захватывает GIL • GIL не будет захвачен другими потоками, если им это не требуется “Для … вычислительных задач … недостаток параллелизма … является большей проблемой, чем скорость выполнения кода Python…” “The GIL … a global bottleneck” 35 типы параллелизма: arxiv Преимущества GIL: • Более быстрая разработка • Простой подсчёт ссылок через: Py_IncRef, Py_DecRef • ... • гарантирует, что одновременно выполняется PVM только один поток байт-кода (-> Python C API) -> что обеспечивает потокобезопасность • Защищает race conditions на Python уровне, но не глубже: ◦ C-расширения могут предоставлять параллелизм в процессе time.sleep(10) timemodule.c: repo { Py_BEGIN_ALLOW_THREADS /* your code here */ Py_END_ALLOW_THREADS } Разворачивается в: { PyThreadState *_save; _save = PyEval_SaveThread(); /* your code here */ PyEval_RestoreThread(_save); } что есть: