<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Python - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/tag/python/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/tag/python/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Sun, 25 Jan 2026 15:03:01 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>Python - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/tag/python/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Подготовка к собеседованию Python</title>
		<link>https://datatalks.ru/python-interview/</link>
					<comments>https://datatalks.ru/python-interview/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Tue, 20 Jan 2026 19:07:45 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Python Interview]]></category>
		<category><![CDATA[Python Интервью]]></category>
		<category><![CDATA[Python Собеседование]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2823</guid>

					<description><![CDATA[<p>Одним из основных hard skill дата инженера является знание Python, так как это достаточно удобный язык для реализации различного функционала. В этой статье будет подборка материалов для подготовки к интервью по Python. YouTube Ну НАСТОЯЩИЙ Senior! 10 лет опыта и экспертиза абсолютно во ВСЁМ? / Техсобес Senior Python Developer ТОП 70 ВОПРОСОВ НА СОБЕСЕДОВАНИИ SENIOR [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/python-interview/">Подготовка к собеседованию Python</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Одним из основных hard skill дата инженера является знание Python, так как это достаточно удобный язык для реализации различного функционала. В этой статье будет подборка материалов для подготовки к интервью по Python.</p>
<h1>YouTube</h1>
<ul>
<li><a href="https://www.youtube.com/watch?v=2J1Zxps_GIc" target="_blank" rel="noopener">Ну НАСТОЯЩИЙ Senior! 10 лет опыта и экспертиза абсолютно во ВСЁМ? / Техсобес Senior Python Developer</a></li>
<li><a href="https://www.youtube.com/watch?v=j2My4dt6mLk" target="_blank" rel="noopener">ТОП 70 ВОПРОСОВ НА СОБЕСЕДОВАНИИ SENIOR PYTHON DEVELOPER</a></li>
<li><a href="https://www.youtube.com/watch?v=QQD-l8Leqog" target="_blank" rel="noopener">#33 Собеседование Python 2025 в BIG TECH компанию | Разбор вопросов</a></li>
<li><a href="https://www.youtube.com/watch?v=bFcbnBGkBLY" target="_blank" rel="noopener">Python разработчик | Собеседование с задачей из Яндекса. Максим Никулин</a></li>
<li><a href="https://www.youtube.com/watch?v=6BioMYL56po" target="_blank" rel="noopener">«Техническое собеседование python-разработчика уровня мидл»</a></li>
</ul>
<h1>Рекомендации по подготовке к интервью в разных компаниях</h1>
<ul>
<li><a href="https://www.tbank.ru/career/it/interview/python/" target="_blank" rel="noopener">ТБанк: Как проходит интервью по Python</a></li>
<li><strong>Подготовка к алгоритмической секции (Яндекс):</strong>
<ul>
<li><a href="https://leetcode.com/explore/featured/card/top-interview-questions-easy/127/strings/885/" target="_blank" rel="noopener">https://leetcode.com/explore/featured/card/top-interview-questions-easy/127/strings/885/</a></li>
<li><a href="https://leetcode.com/problems/kth-smallest-element-in-a-bst/" target="_blank" rel="noopener">https://leetcode.com/problems/kth-smallest-element-in-a-bst/</a></li>
<li><a href="https://leetcode.com/problems/group-anagrams/" target="_blank" rel="noopener">https://leetcode.com/problems/group-anagrams/</a></li>
<li><a href="https://leetcode.com/problems/decode-ways/" target="_blank" rel="noopener">https://leetcode.com/problems/decode-ways/</a></li>
<li><a href="https://leetcode.com/problems/word-break/" target="_blank" rel="noopener">https://leetcode.com/problems/word-break/</a></li>
<li><a href="https://leetcode.com/problems/integer-to-roman/" target="_blank" rel="noopener">https://leetcode.com/problems/integer-to-roman/</a></li>
<li>Строки, массивы</li>
<li>Хеш-таблицы, словари</li>
<li>Обход двоичного дерева</li>
<li>Реализация различных примитивов: счётчиков, кешей и т. д.</li>
</ul>
</li>
</ul>
<h1>Подборки задач (Leetcode и другие)</h1>
<ul>
<li><a href="https://leetcode.com/discuss/post/460599/blind-75-leetcode-questions-by-krishnade-9xev/" target="_blank" rel="noopener">Blind 75 LeetCode Questions</a></li>
<li><a href="https://neetcode.io/roadmap" target="_blank" rel="noopener">neetcode.io</a></li>
<li><a href="https://leetcopilot.dev/blog/best-free-coding-interview-prep-resources-2026" target="_blank" rel="noopener">Best Free Coding Interview Prep Resources in 2026 (Complete List)</a></li>
<li><a href="https://github.com/umitkacar/awesome-interview" target="_blank" rel="noopener">Coding Interview Prep (awesome interview)</a></li>
<li><strong>Моя подборка задач Easy уровня на Leetcode:</strong>
<ul>
<li><a href="https://leetcode.com/problem-list/wpdwlphj/" target="_blank" rel="noopener">1. Arrays Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd040lr/" target="_blank" rel="noopener">2. String Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd0crwh/" target="_blank" rel="noopener">3. Sorting Easy</a></li>
<li><a href="https://leetcode.com/problem-list/wpd7hzr5/" target="_blank" rel="noopener">4. Counting Easy</a></li>
</ul>
</li>
<li>Решение задач по алгоритмам <a href="https://github.com/ivanshamaev/python-algorithms-data-engineer/tree/main/%D0%9F%D0%BE%D0%B4%D0%B3%D0%BE%D1%82%D0%BE%D0%B2%D0%BA%D0%B0%20%D0%BA%20%D1%81%D0%BE%D0%B1%D0%B5%D1%81%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%D0%BC%20(%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B8)" target="_blank" rel="noopener">&#171;Подготовка к собеседованиям (задачи)&#187;</a></li>
</ul>
<p>Сообщение <a href="https://datatalks.ru/python-interview/">Подготовка к собеседованию Python</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/python-interview/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Python &#8212; Многозадачность, конкурентность и асинхронность</title>
		<link>https://datatalks.ru/python-threading-multiprocessing-asyncio/</link>
					<comments>https://datatalks.ru/python-threading-multiprocessing-asyncio/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Fri, 26 Dec 2025 19:05:32 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[asyncio]]></category>
		<category><![CDATA[concurrent.futures]]></category>
		<category><![CDATA[multiprocessing]]></category>
		<category><![CDATA[threading]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=2596</guid>

					<description><![CDATA[<p>Подборка материалов для освоения темы многозадачности в Python YouTube ролики Как работает GIL в Python. Многопоточность. Многопроцессность. IO/CPU-Bound Yandex for Developers &#8212; 01. Устройство CPython – Егор Овчаренко [ZProger] Многопоточность и Многопроцессорность Python. Threading &#38; Multiprocessing Python Асинхронность, многопоточность, многопроцессность в python &#124; Библиотека asyncio и асинхронный код Threading. Кратко про Python Плейлист Асинхронность в [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/python-threading-multiprocessing-asyncio/">Python &#8212; Многозадачность, конкурентность и асинхронность</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Подборка материалов для освоения темы многозадачности в Python</h1>
<h2>YouTube ролики</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=hkWmKQqLT4k" target="_blank" rel="noopener">Как работает GIL в Python. Многопоточность. Многопроцессность. IO/CPU-Bound</a></li>
<li><a href="https://www.youtube.com/watch?v=PxIqLgjtQ5Y" target="_blank" rel="noopener">Yandex for Developers &#8212; 01. Устройство CPython – Егор Овчаренко</a></li>
<li><a href="https://www.youtube.com/playlist?list=PL6plRXMq5RAAb9gwGqmgAoA-KIr-7CMuz" target="_blank" rel="noopener">[ZProger] Многопоточность и Многопроцессорность Python. Threading &amp; Multiprocessing Python</a></li>
<li><a href="https://www.youtube.com/watch?v=_4QY1nGFRY8" target="_blank" rel="noopener">Асинхронность, многопоточность, многопроцессность в python | Библиотека asyncio и асинхронный код</a></li>
<li><a href="https://www.youtube.com/watch?v=_JCV3deaFvE" target="_blank" rel="noopener">Threading. Кратко про Python</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLlWXhlUMyooawilqK4lPXRvxtbYiw34S8" target="_blank" rel="noopener">Плейлист Асинхронность в Python</a></li>
<li><a href="https://www.youtube.com/live/sCPMwXPggis" target="_blank" rel="noopener">Young&amp;&amp;Yandex ШБР 2023 — Асинхронное программирование (Python)</a> &#8212; полный плейлист <a href="https://www.youtube.com/playlist?list=PLZvfMc-lVSSPZ_VYTK8XEkZ_S_bCfyu8C" target="_blank" rel="noopener">Python ШБР 2023</a></li>
<li><a href="https://www.youtube.com/watch?v=DvVhG8-HMSQ" target="_blank" rel="noopener">Особенности asyncio.wait_for() в асинхронном Python. Как работает таймаут для корутины</a></li>
<li><a href="https://www.youtube.com/playlist?list=PLz8SX0iNPyAIHH3xtwrcxI5UWleLB5el_" target="_blank" rel="noopener">Плейлист Асинхронность в Python</a></li>
<li><a href="https://www.youtube.com/watch?v=o_COfPdWAPw" target="_blank" rel="noopener">Асинхронное программирование на примере Python / asyncio</a></li>
<li><a href="https://www.youtube.com/watch?v=BoazgBZ4D7k" target="_blank" rel="noopener">Собеседование Python. Разбор вопросов</a></li>
<li><a href="https://www.youtube.com/watch?v=dvfnYkEHmdA" target="_blank" rel="noopener">Денис Аникин. Вновь ускоряем cpu-bound задачи</a></li>
<li><a href="https://www.youtube.com/watch?v=G2EG-eCHOiI" target="_blank" rel="noopener">Python: Threads, GIL, asyncio</a></li>
<li><a href="https://www.youtube.com/watch?v=QitEF7Qvi4w" target="_blank" rel="noopener">Лекция Тимофей Хирьянов &#8212; Параллельное программирование на Python</a></li>
<li>Yandex Developer (плейлист <a href="https://www.youtube.com/playlist?list=PLQC2_0cDcSKBHamFYA6ncnc_fYuEQUy0s" target="_blank" rel="noopener">Школа бэкенд-разработки 2019</a>) &#8212; Асинхронное программирование &#8212; <a href="https://www.youtube.com/watch?v=AXkOli6BsBY" target="_blank" rel="noopener">Лекция 1</a>, <a href="https://www.youtube.com/watch?v=IB4bJqmfjI0" target="_blank" rel="noopener">Лекция 2</a>, <a href="https://www.youtube.com/watch?v=FFUYf8FHDlY" target="_blank" rel="noopener">Лекция 3</a>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PLQC2_0cDcSKCMKnywAS8eI_EgCcE3yx0r" target="_blank" rel="noopener">Плейлист Школа бэкенд-разработки 2021</a></li>
</ul>
</li>
<li><a href="https://www.youtube.com/playlist?list=PLlKID9PnOE5ibKy6U7XaCA2Nqk_R1d5CJ" target="_blank" rel="noopener">Плейлист &#171;Конкурентность в Python&#187;</a></li>
<li><a href="https://www.youtube.com/watch?v=AWX4JnAnjBE" target="_blank" rel="noopener">GIL в Python: зачем он нужен и как с этим жить</a></li>
<li><a href="https://www.youtube.com/watch?v=z7WIm0iZcOU" target="_blank" rel="noopener">Асинхронный Python-код медленнее обычного кода! Ааа!!1один. Aiohttp VS синхронные фреймворки</a></li>
</ul>
<p><strong>YouTube English:</strong></p>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PLhNSoGM2ik6SIkVGXWBwerucXjgP1rHmB" target="_blank" rel="noopener">PlayList: Воспроизвести все import asyncio: Learn Python&#8217;s AsyncIO</a></li>
<li><a href="https://www.youtube.com/watch?v=Wsv07g4ml8I" target="_blank" rel="noopener">CPU Bound vs. I/O Bound | Computer Basics</a></li>
<li><a href="https://www.youtube.com/watch?v=AZnGRKFUU0c" target="_blank" rel="noopener">threading vs multiprocessing in python</a></li>
<li><a href="https://www.youtube.com/watch?v=XbBFKco43aw" target="_blank" rel="noopener">I/OBound vs CPU Bound Code</a></li>
</ul>
<h2>Статьи</h2>
<ul>
<li><a href="https://habr.com/ru/companies/otus/articles/960206/" target="_blank" rel="noopener">CPython простыми словами: всё, что нужно знать начинающему</a></li>
<li><a href="https://habr.com/ru/companies/otus/articles/769448/" target="_blank" rel="noopener">Как устроен GIL (Global Interpreter Lock) в Python: влияние на многозадачность и производительность</a></li>
<li><a href="https://habr.com/ru/articles/84629/" target="_blank" rel="noopener">Как устроен GIL в Python</a></li>
<li><a href="https://habr.com/ru/companies/wunderfund/articles/586360/" target="_blank" rel="noopener">Глобальная блокировка интерпретатора (GIL) и её воздействие на многопоточность в Python</a></li>
<li><a href="https://habr.com/ru/articles/417215/" target="_blank" rel="noopener">Всё, что нужно знать о сборщике мусора в Python</a></li>
<li><a href="https://habr.com/ru/companies/ntechlab/articles/946098/" target="_blank" rel="noopener">Визуализация управления памятью в Python: что творится внутри?</a></li>
</ul>
<h1>Введение в Python</h1>
<h2>Исходный глоссарий</h2>
<h3>Виртуальное адресное пространство</h3>
<p><strong>Виртуальное адресное пространство</strong> — это абстракция, предоставляемая ОС, в рамках которой каждый процесс видит собственную непрерывную адресную память, не зная о реальном физическом расположении данных.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2627" src="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg" alt="" width="660" height="452" srcset="https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space.jpg 660w, https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space-300x205.jpg 300w, https://datatalks.ru/wp-content/uploads/2025/12/Virtual_address_space-450x308.jpg 450w" sizes="(max-width: 660px) 100vw, 660px" /></a></p>
<p><strong>Структура виртуального адресного пространства</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Высокие адреса
┌─────────────────────────┐
│ Kernel space (отображ.) │  ← недоступен напрямую
├─────────────────────────┤
│ Stack                   │  ← стек потоков
├─────────────────────────┤
│ Heap                    │  ← объекты Python
├─────────────────────────┤
│ Data / BSS              │  ← глобальные переменные
├─────────────────────────┤
│ Code (text segment)     │  ← байткод + C-расширения
└─────────────────────────┘
Низкие адреса</pre><p>Python не управляет адресным пространством напрямую — он запрашивает память у ОС через <code>malloc</code>, <code>mmap</code>, <code>brk</code>.</p>
<h3>Heap &amp; Stack</h3>
<p><strong>Стек(stack)</strong> и <strong>куча(heap)</strong> – области в оперативной памяти (ОЗУ, RAM), в которых хранятся данные приложения во время его выполнения. Управление оперативной памятью для приложения Python осуществляется с помощью <strong>Python memory manager</strong>.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg"><img decoding="async" class="aligncenter size-full wp-image-2628" src="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg" alt="" width="764" height="529" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management.jpeg 764w, https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management-300x208.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_memory_management-450x312.jpeg 450w" sizes="(max-width: 764px) 100vw, 764px" /></a></p>
<p><strong>В управлении памятью (Python memory management)</strong> существует <strong>механизм учёта ссылок (reference counting)</strong>, который ведет внутренний журнал того, как много ссылок ссылается на объект в куче. Когда на объект не ссылается ни одна ссылка <strong>сборщик мусора (Garbage collector)</strong> автоматически освобождает память выделенную ранее для этого объекта.</p>
<p><strong>Heap</strong> — область памяти процесса, предназначенная для динамического выделения памяти во время выполнения.</p>
<p><strong>В Python:</strong></p>
<ul>
<li>все объекты Python живут в heap</li>
<li><code>int</code>, <code>list</code>, <code>dict</code>, <code>class</code>, <code>function</code> — всё heap</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg"><img decoding="async" class="aligncenter size-full wp-image-2629" src="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg" alt="" width="971" height="515" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack.jpeg 971w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-300x159.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-768x407.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-450x239.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_heap_stack-780x414.jpeg 780w" sizes="(max-width: 971px) 100vw, 971px" /></a></p>
<p><strong>Stack</strong> — область памяти, используемая для хранения локальных переменных, адресов возврата, аргументов функций. Каждый поток имеет собственный стек.</p>
<p><strong>Python stack</strong> — это логическая абстракция, а не «настоящий» stack ОС.</p>
<h3>Дескрипторы ресурсов (File Descriptors)</h3>
<p><strong>File descriptor</strong> — это целое число, которое операционная система даёт твоей программе, когда она открывает файл или другое устройство ввода-вывода (например, сокет, pipe). Это как минимальный идентификатор ресурса: Python использует его для низкоуровневых операций с файлами.</p>
<ul>
<li>Это не объект Python, это число, под которым ОС видит открытый файл/ресурс.</li>
<li>С помощью FD можно делать низкоуровневые операции (чтение, запись, дупликация, перемещение позиции и т.п.).</li>
<li>Отличие от обычного open() в том, что FD используют функции модуля os, а не методы объекта файла.</li>
</ul>
<p>Каждый <strong>FD</strong> — ограниченный ресурс. Если ты открыл много файлов или сокетов и не закрыл их, система закончится и новые операции упадут с ошибками вроде Too many open files. Это особенно критично для серверов, которые держат много соединений одновременно.</p>
<p>В Unix-системах всё представляется как файл. Стандартные дескрипторы:</p>
<ul>
<li><code>0</code> — stdin</li>
<li><code>1</code> — stdout</li>
<li><code>2</code> — stderr</li>
</ul>
<p>Ты можешь перенаправлять их (например, в скриптах bash или в приложениях), и это тоже работает через FD.</p>
<p><strong>Примеры ресурсов:</strong> файлы, сокеты, pipe, eventfd, epoll/kqueue</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2630" src="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg" alt="" width="862" height="591" srcset="https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux.jpeg 862w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-300x206.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-768x527.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-450x309.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/file_descriptor_linux-780x535.jpeg 780w" sizes="(max-width: 862px) 100vw, 862px" /></a></p>
<h3>Глобальные переменные в Python</h3>
<p>Глобальные переменные — это имена, привязанные в namespace модуля.</p>
<p>В реальности:</p>
<ul>
<li>имя <code>x</code> → указатель</li>
<li>объект <code>10</code> → heap</li>
<li>namespace модуля → dict в heap</li>
</ul>
<h3>Регистры CPU</h3>
<p><strong>Регистры CPU</strong> — сверхбыстрая память внутри процессора.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2631" src="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png" alt="" width="627" height="368" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry.png 627w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry-300x176.png 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_regestry-450x264.png 450w" sizes="(max-width: 627px) 100vw, 627px" /></a></p>
<p><strong>Хранят: </strong>указатель инструкции (<strong>IP</strong>), указатель стека (<strong>SP</strong>), флаги, временные значения.</p>
<p>Python не управляет регистрами напрямую. Но при <strong>context switch ОС</strong> сохраняет регистры, при переключении потоков Python → регистры меняются. Это основная стоимость <strong>context switch</strong>.</p>
<h3>User Space</h3>
<p><strong>User space</strong> — режим выполнения с ограниченными правами.</p>
<p>Python-код выполняется исключительно в user space.</p>
<p><strong>Запрещено:</strong></p>
<ul>
<li>прямой доступ к устройствам</li>
<li>управление памятью</li>
<li>прерывания</li>
</ul>
<h3>Kernel Space</h3>
<p><strong>Kernel space</strong> — привилегированный режим выполнения.</p>
<p><strong>Ядро:</strong></p>
<ul>
<li>управляет памятью</li>
<li>планирует процессы</li>
<li>обрабатывает I/O</li>
<li>управляет сетевым стеком</li>
</ul>
<h2>Что такое процесс, поток, системный вызов и context switch?</h2>
<p><strong>Процесс</strong> — это изолированное выполняемое окружение, предоставляемое ОС. Каждый процесс имеет собственное виртуальное адресное пространство, heap, stack, дескрипторы ресурсов (файлы, сокеты).</p>
<p><strong>Поток (Thread)</strong> &#8212; это единица выполнения внутри процесса. Потоки разделяют одно адресное пространство процесса, каждый поток имеет собственный stack, выполняются псевдопараллельно внутри 1 процесса. Общее у потоков heap, глобальные переменные, объекты Python. Раздельное &#8212; stack, регистры CPU.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2635" src="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg" alt="" width="772" height="959" srcset="https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading.jpeg 772w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-242x300.jpeg 242w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-768x954.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/multiprocessing_threading-450x559.jpeg 450w" sizes="(max-width: 772px) 100vw, 772px" /></a></p>
<p><strong>Системный вызов</strong> — это контролируемый переход из <strong>user space</strong> в <strong>kernel space</strong>.</p>
<p>Python-код не может напрямую:</p>
<ul>
<li>читать диск</li>
<li>писать в сокет</li>
<li>создавать процесс</li>
<li>спать</li>
</ul>
<p><strong>Что происходит при системном вызове:</strong></p>
<ul>
<li>Python вызывает <strong>C-функцию</strong></li>
<li><strong>C-функция</strong> делает <code>syscall</code></li>
<li>ОС выполняет операцию</li>
<li>Поток блокируется, пока ОС не закончит</li>
</ul>
<p><strong>В этот момент:</strong></p>
<ul>
<li><strong>GIL</strong> может быть освобождён</li>
<li>другой поток может выполняться</li>
</ul>
<p><strong>Context switch</strong> — это переключение CPU с одной задачи на другую.</p>
<p><strong>Бывает:</strong></p>
<ul>
<li>между потоками</li>
<li>между процессами</li>
</ul>
<p>Что сохраняется:</p>
<ul>
<li>регистры CPU</li>
<li>указатель стека</li>
<li>состояние планировщика</li>
</ul>
<h2>Архитектура CPython</h2>
<p><strong>CPython</strong> — это эталонная реализация языка программирования Python. Это версия Python по умолчанию, наиболее широко используемая и оригинальная реализация, написанная преимущественно на языке C.</p>
<p>Иными словами <strong>CPython</strong> &#8212; это программа, которая принимает ваш <strong>код на Python</strong> и выполняет его, преобразуя в понятные машине действия.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2659" src="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg" alt="" width="590" height="307" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpython.jpeg 590w, https://datatalks.ru/wp-content/uploads/2025/12/cpython-300x156.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpython-450x234.jpeg 450w" sizes="(max-width: 590px) 100vw, 590px" /></a></p>
<ol>
<li><strong>Исходный код (Source code)</strong> – mymodule.py преобразуется в <strong>байт-код</strong> с помощью компилятора (compiler) Python</li>
<li><strong>Байт-код (Byte code)</strong> сохраняется в определенном формате (.pyc, .pyo, .pyd) – mymodule.pyc</li>
<li><strong>Виртуальная машина Python (или PVM)</strong> получает байт-код и с помощью интерпретатора преобразует его в бинарный код.</li>
<li>Бинарный или машинный код (Binary code)</li>
<li>Компьютер читает бинарный код и выполняет программу</li>
</ol>
<p><strong>Важно понимать разницу между языком Python и интерпретатором CPython.</strong> Язык Python — это набор правил и синтаксиса (описанных в документации), а CPython конкретная программа, исполняющая код на этом языке.</p>
<hr />
<blockquote><p><strong>Python</strong> &#8212; язык, а <strong>CPython</strong> &#8212; его основной движок.</p></blockquote>
<hr />
<p><strong>Что такое PVM?</strong></p>
<p>Мы знаем, что компьютеры понимают только машинный код, состоящий из нулей и единиц. Поскольку компьютер понимает исключительно машинный код, любой программный код перед выполнением должен быть преобразован в машинный код. Для этого используется компилятор. Обычно компилятор преобразует исходный код программы непосредственно в машинный код.</p>
<p>Компилятор Python выполняет ту же задачу, но несколько иным образом. Он преобразует исходный код программы в другой вид кода, называемый байт-кодом. Каждая инструкция программы на Python преобразуется в набор инструкций байт-кода.</p>
<p><strong>Виртуальная машина Python (Python Virtual Machine, PVM)</strong> принимает этот байт-код и преобразует его в машинный код, чтобы компьютер мог выполнить соответствующие инструкции и вывести итоговый результат. Для выполнения этого преобразования PVM оснащена интерпретатором. Интерпретатор преобразует байт-код в машинный код и передаёт этот машинный код процессору компьютера для выполнения. Поскольку именно интерпретатор играет ключевую роль, виртуальную машину Python часто также называют интерпретатором.</p>
<h3>Альтернативные реализации</h3>
<p>Хотя <strong>CPython</strong> является стандартной реализацией, существуют и другие реализации Python, созданные для конкретных задач, таких как повышение производительности или интеграция с другими платформами:</p>
<ul>
<li><strong>PyPy</strong> — использует компиляцию <strong>Just-In-Time (JIT)</strong>, что позволяет во многих случаях выполнять Python-код значительно быстрее, чем в CPython.</li>
<li><strong>Jython</strong> — написан на <strong>Java</strong> и компилирует Python-код в байткод Java, что позволяет запускать Python на виртуальной машине Java (<strong>JVM</strong>) и взаимодействовать с библиотеками Java.</li>
<li><strong>IronPython</strong> — реализован для <strong>Common Language Infrastructure (CLI)</strong>, благодаря чему может работать на платформе <code>.NET</code>.</li>
<li><strong>MicroPython / CircuitPython</strong> — оптимизированные реализации, предназначенные для микроконтроллеров и встраиваемых систем.</li>
</ul>
<h3>Производительность</h3>
<p>Те, кто имеют опыт работы с компилирующими языками программирования, такими как C и C++, могут заметить несколько отличий в модели выполнения Python.</p>
<ul>
<li><strong>Первое</strong>, что бросается в глаза, – это отсутствие этапа сборки, или вызова утилиты «make»: программный код может запускаться сразу же, как только будет написан.</li>
<li><strong>Второе</strong> отличие: байт код не является двоичным машинным кодом (например, инструкциями для микропроцессора Intel). Байт код – это внутреннее представление программ на языке Python.</li>
</ul>
<p>По этой причине программный код на языке Python не может выполняться так же быстро, как программный код на языке C или C++. <strong>Обход инструкций выполняет виртуальная машина</strong>, а не микропроцессор, и <strong>чтобы выполнить байт код, необходима дополнительная интерпретация</strong>, инструкции которого требуют на выполнение больше времени, чем машинные инструкции микропроцессора. С другой стороны, в отличие от классических интерпретаторов, здесь присутствует дополнительный этап компиляции – интерпретатору не требуется всякий раз снова и снова анализировать инструкции исходного текста. В результате Python способен обеспечить скорость выполнения где то между традиционными компилирующими и традиционными интерпретирующими языками программирования.</p>
<h2>GIL (Global Interpreter Lock)</h2>
<p><strong>GIL (Global Interpreter Lock)</strong> &#8212; интерпретатор Python однопоточный в том смысле, что в каждый момент времени может выполняться только <strong>один участок байт-кода</strong>, даже если в процессе работает несколько потоков. <strong>Глобальная блокировка интерпретатора не позволяет выполнять несколько потоков одновременно.</strong></p>
<p>Python может освободить GIL на время выполнения <strong>операций ввода-вывода (I/O Bound)</strong>, потому что для выполнения ввода-вывода вызывается низкоуровневая функция операционной системы. Эти функции работают за пределами интерпретатора, т. е. никак не могут повредить его внутренние структуры, от чего и призвана защитить GIL.</p>
<p><strong>GIL</strong> был введён для упрощения управления памятью в Python, поскольку многие внутренние операции, такие как создание объектов, по умолчанию не являются потокобезопасными. Без <strong>GIL</strong> нескольким потокам, одновременно обращающимся к общим ресурсам, потребовались бы сложные механизмы блокировок или синхронизации для предотвращения гонок данных и повреждения состояния.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2647" src="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg" alt="" width="910" height="348" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_gil.jpeg 910w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-300x115.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-768x294.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-450x172.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_gil-780x298.jpeg 780w" sizes="(max-width: 910px) 100vw, 910px" /></a></p>
<p><strong>Когда GIL становится узким местом?</strong></p>
<ul>
<li>В однопоточных программах <strong>GIL</strong> не имеет значения, так как поток обладает эксклюзивным доступом к интерпретатору Python.</li>
<li>В многопоточных <strong>I/O-bound</strong> программах влияние <strong>GIL</strong> менее заметно, поскольку потоки освобождают GIL во время ожидания операций ввода-вывода.</li>
<li>В многопоточных <strong>CPU-bound</strong> задачах <strong>GIL</strong> становится серьёзным узким местом. Несколько потоков, конкурируя за GIL, вынуждены по очереди выполнять байткод Python.</li>
</ul>
<p>Интересный случай, на который стоит обратить внимание, — использование time.sleep. Python фактически рассматривает <code>time.sleep</code> как <strong>I/O-операцию</strong>. Функция <code>time.sleep</code> не является <strong>CPU-bound</strong>, поскольку во время сна не происходит активных вычислений или выполнения байткода Python. Вместо этого ответственность за отслеживание прошедшего времени передаётся операционной системе. В течение этого времени поток освобождает GIL, позволяя другим потокам выполняться и использовать интерпретатор.</p>
<h3><strong>Когда GIL может освобождать поток?</strong></h3>
<table>
<thead>
<tr>
<th>Ситуация</th>
<th>GIL</th>
</tr>
</thead>
<tbody>
<tr>
<td><code inline="">time.sleep()</code></td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>I/O</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td><code inline="">lock.acquire()</code> (ожидание)</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>C-расширение без Python API</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> отпущен</td>
</tr>
<tr>
<td>Чистый Python CPU-код</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td>Работа с Python-объектами</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td>
</tr>
<tr>
<td>Переключение по таймеру</td>
<td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> временно</td>
</tr>
</tbody>
</table>
<p>Рассмотрим подробно каждую ситуацию.</p>
<p><strong>1 кейс &#8212; блокирующие операции (I/O, sleep, lock wait):</strong> Когда поток заходит в операцию, которая может надолго заблокироваться, CPython отпускает GIL.</p>
<p>time.sleep()</p>
<p><strong>I/O:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">sock.recv()
sock.send()
open().read()
requests.get()</pre><p>Ожидание примитивов синхронизации</p><pre class="urvanov-syntax-highlighter-plain-tag">lock.acquire()      # если lock уже занят
event.wait()
condition.wait()
queue.get()         # если очередь пуста</pre><p><strong>Кейс 2 &#8212; выполнение C-кода, который отпускает GIL:</strong> Если поток заходит в C-расширение, где внутри есть:</p><pre class="urvanov-syntax-highlighter-plain-tag">Py_BEGIN_ALLOW_THREADS
// тяжёлая работа без Python-объектов
Py_END_ALLOW_THREADS</pre><p>текущий поток временно теряет GIL.</p>
<p>Примеры библиотек:</p>
<ul>
<li>numpy</li>
<li>hashlib</li>
<li>zlib</li>
<li>Pillow</li>
</ul>
<p><strong>C-расширение без Python API:</strong> C-код, который во время выполнения не создаёт, не читает и не изменяет Python-объекты (PyObject*).</p>
<p><strong>Пример Python API в C:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">PyLong_FromLong(10);      // создаёт Python-объект
PyList_Append(list, x);  // меняет Python-объект
Py_INCREF(obj);          // меняет refcount
PyObject_CallObject(f);  // вызывает Python-функцию
PyErr_SetString(...);    // трогает исключения</pre><p></p>
<h2>CPU-bound vs I/O-bound задачи</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2639" src="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg" alt="" width="706" height="353" srcset="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound.jpeg 706w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound-300x150.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound_cpu_inbound-450x225.jpeg 450w" sizes="(max-width: 706px) 100vw, 706px" /></a></p>
<h3>I/O-bound</h3>
<p><strong>I/O-bound задача</strong> — это задача, выполнение которой блокируется ожиданием операций ввода-вывода (I/O), например сетевых запросов, чтения/записи на диск или работы с внешними устройствами, и поэтому большая часть времени тратится не на вычисления, а на ожидание завершения этих операций.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2642" src="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg" alt="" width="1069" height="329" srcset="https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound.jpeg 1069w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-300x92.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-1024x315.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-768x236.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-450x138.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/i_o_inbound-780x240.jpeg 780w" sizes="(max-width: 1069px) 100vw, 1069px" /></a></p>
<h3>CPU-bound</h3>
<p><strong>CPU-bound задача</strong> — это задача, выполнение которой ограничено мощностью центрального процессора (CPU), а не ожиданием ввода-вывода. Время её выполнения определяется главным образом количеством вычислительных операций, которые нужно выполнить CPU, а не тем, сколько времени тратится на ожидание данных из внешних источников.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2643" src="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg" alt="" width="1068" height="297" srcset="https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound.jpeg 1068w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-300x83.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-1024x285.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-768x214.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-450x125.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/cpu_inbound-780x217.jpeg 780w" sizes="(max-width: 1068px) 100vw, 1068px" /></a></p>
<h1>Многозадачность в Python</h1>
<h2>Concurrency vs Parallelism</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2654" src="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg" alt="" width="1251" height="504" srcset="https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python.jpeg 1251w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-300x121.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-1024x413.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-768x309.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-450x181.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/concarency_parallelism_python-780x314.jpeg 780w" sizes="(max-width: 1251px) 100vw, 1251px" /></a></p>
<ul>
<li><strong>Concurrency</strong> — это управление несколькими задачами в одно и то же время, но не обязательно их одновременное выполнение. Задачи могут выполняться по очереди, создавая иллюзию многозадачности.</li>
<li><strong>Parallelism</strong> — это одновременное выполнение нескольких задач, как правило за счёт использования нескольких ядер CPU.</li>
</ul>
<h2>Критерии выбора подхода &#8212; Multithreading, Multiprocessing или Asyncio</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2651" src="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg" alt="" width="861" height="489" srcset="https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio.jpeg 861w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-300x170.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-768x436.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-450x256.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/12/python_threading_multiprocessing_asyncio-780x443.jpeg 780w" sizes="(max-width: 861px) 100vw, 861px" /></a></p>
<p><strong>Multiprocessing (многопроцессность)</strong></p>
<ul>
<li>Лучше всего подходит для CPU-bound задач, требующих интенсивных вычислений.</li>
<li>Используется, когда необходимо обойти GIL — каждый процесс имеет собственный интерпретатор Python, что позволяет достичь настоящего параллелизма.</li>
</ul>
<p><strong>Multithreading (многопоточность)</strong></p>
<ul>
<li>Лучше всего подходит для быстрых I/O-bound задач, так как уменьшается частота переключений контекста, и интерпретатор Python дольше остаётся в одном потоке.</li>
<li>Не подходит для CPU-bound задач из-за ограничений GIL.</li>
</ul>
<p><strong>Asyncio (асинхронность)</strong></p>
<ul>
<li>Идеально подходит для медленных I/O-bound задач, таких как длительные сетевые запросы или обращения к базе данных, поскольку эффективно обрабатывает ожидание и хорошо масштабируется.</li>
<li>Не подходит для CPU-bound задач, если вычисления не выносятся в другие процессы.</li>
</ul>
<h1>threading</h1>
<blockquote><p><span style="color: #ff6600;"><strong>threading</strong></span> в CPython — это инструмент для <span style="color: #ff6600;">I/O</span>-параллелизма.</p></blockquote>
<hr />
<h2>Начальный пример Threading</h2>
<p><strong>Модуль threading</strong> предоставляет способ запуска нескольких потоков (меньших единиц процесса) конкурентно внутри одного процесса. Он позволяет создавать и управлять потоками, делая возможным параллельное выполнение задач с разделяемым адресным пространством памяти. Потоки особенно полезны, когда задачи являются <strong>I/O-bound</strong>, например при работе с файлами или выполнении сетевых запросов, где значительная часть времени тратится на ожидание внешних ресурсов.</p>
<p>Типичный сценарий использования <strong>threading</strong> — управление пулом рабочих потоков, которые могут конкурентно обрабатывать несколько задач. Ниже приведён базовый пример создания и запуска потоков с использованием <strong>Thread</strong>:</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random
from datetime import datetime

def crawl(link):
    print(f"crawl запустился для ссылки {link}. Время вызова: {datetime.now()}")
    time.sleep(random.randint(1, 11))  # Блокирующий I/O (имитация сетевого запроса)
    print(f"crawl завершен для {link}. Время вызова: {datetime.now()}")

links = [
    "https://python.org",
    "https://docs.python.org",
    "https://peps.python.org",
]

# Создаём потоки для каждой ссылки
threads = []
for i, link in enumerate(links):
    # Используем `args` для позиционных аргументов и `kwargs` для именованных
    t = threading.Thread(target=crawl, args=(link,), name=f"Thread-{i+1}")
    threads.append(t)

# Запускаем каждый поток
for t in threads:
    t.start()
    print(f'Поток {t} запущен в {datetime.now()}')

# Ожидаем завершения всех потоков
for t in threads:
    t.join()
    print(f'{t} завершен в {datetime.now()}')</pre><p><strong>Результат:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">crawl запустился для ссылки https://python.org. Время вызова: 2025-12-27 13:38:45.574122
Поток &lt;Thread(Thread-1, started 138519994431168)&gt; запущен в 2025-12-27 13:38:45.574331
crawl запустился для ссылки https://docs.python.org. Время вызова: 2025-12-27 13:38:45.574500
Поток &lt;Thread(Thread-2, started 138519986038464)&gt; запущен в 2025-12-27 13:38:45.574558
crawl запустился для ссылки https://peps.python.org. Время вызова: 2025-12-27 13:38:45.574701
Поток &lt;Thread(Thread-3, started 138519977645760)&gt; запущен в 2025-12-27 13:38:45.574758
crawl завершен для https://python.org. Время вызова: 2025-12-27 13:38:46.574254
&lt;Thread(Thread-1, stopped 138519994431168)&gt; завершен в 2025-12-27 13:38:46.574417
crawl завершен для https://docs.python.org. Время вызова: 2025-12-27 13:38:47.574610
&lt;Thread(Thread-2, stopped 138519986038464)&gt; завершен в 2025-12-27 13:38:47.574773
crawl завершен для https://peps.python.org. Время вызова: 2025-12-27 13:38:47.574813
&lt;Thread(Thread-3, stopped 138519977645760)&gt; завершен в 2025-12-27 13:38:47.574895</pre><p><strong>Результат второго запуска:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">crawl запустился для ссылки https://python.org. Время вызова: 2025-12-27 13:44:50.159111
Поток &lt;Thread(Thread-1, started 126693911033536)&gt; запущен в 2025-12-27 13:44:50.159203
crawl запустился для ссылки https://docs.python.org. Время вызова: 2025-12-27 13:44:50.159412
Поток &lt;Thread(Thread-2, started 126693902640832)&gt; запущен в 2025-12-27 13:44:50.159460
crawl запустился для ссылки https://peps.python.org. Время вызова: 2025-12-27 13:44:50.159612
Поток &lt;Thread(Thread-3, started 126693894248128)&gt; запущен в 2025-12-27 13:44:50.159679
crawl завершен для https://docs.python.org. Время вызова: 2025-12-27 13:44:57.159525
crawl завершен для https://peps.python.org. Время вызова: 2025-12-27 13:44:58.159735
crawl завершен для https://python.org. Время вызова: 2025-12-27 13:44:59.159292
&lt;Thread(Thread-1, stopped 126693911033536)&gt; завершен в 2025-12-27 13:44:59.159502
&lt;Thread(Thread-2, stopped 126693902640832)&gt; завершен в 2025-12-27 13:44:59.159545
&lt;Thread(Thread-3, stopped 126693894248128)&gt; завершен в 2025-12-27 13:44:59.159565</pre><p><strong>Общая схема start и join в threading:</strong></p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2690" src="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg" alt="" width="745" height="626" srcset="https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join.jpeg 745w, https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join-300x252.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/threading_start_join-450x378.jpeg 450w" sizes="(max-width: 745px) 100vw, 745px" /></a></p>
<h3>Деталь реализации CPython</h3>
<p>В CPython из-за <strong>глобальной блокировки интерпретатора (GIL)</strong> только один поток может выполнять Python-код в каждый момент времени (хотя некоторые ориентированные на производительность библиотеки могут обходить это ограничение). Если требуется более эффективно использовать вычислительные ресурсы многоядерных машин, рекомендуется использовать <code>multiprocessing</code> или <code>concurrent.futures.ProcessPoolExecutor</code>. Тем не менее, <strong>threading</strong> остаётся подходящей моделью, если нужно одновременно выполнять несколько <strong>I/O-bound задач</strong>.</p>
<h3>GIL и вопросы производительности</h3>
<p>В отличие от модуля <strong>multiprocessing</strong>, который использует отдельные процессы для обхода GIL, модуль <strong>threading</strong> работает внутри одного процесса, а значит все потоки разделяют одно и то же адресное пространство памяти. Однако GIL ограничивает прирост производительности при работе с CPU-bound задачами, поскольку только один поток может выполнять байткод Python одновременно. Несмотря на это, потоки остаются полезным инструментом для достижения конкурентности во многих сценариях.</p>
<p><strong>Начиная с Python 3.13, существуют <code>free-threaded</code> сборки</strong>, в которых GIL может быть отключён, что позволяет добиться настоящего параллельного выполнения потоков. Однако по умолчанию эта возможность недоступна (см. <strong>PEP 703</strong>).</p>
<h2>Жизненный цикл потока</h2>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2679" src="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg" alt="" width="779" height="379" srcset="https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python.jpeg 779w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-300x146.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-768x374.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/12/thread_lifecycle_python-450x219.jpeg 450w" sizes="(max-width: 779px) 100vw, 779px" /></a></p>
<p><strong>Жизненным циклом потоков можно управлять с помощью следующих методов:</strong></p>
<ul>
<li><code>start()</code> &#8212; Дает потоку жизнь.</li>
<li><code>run()</code> &#8212; Этот метод представляет действия, которые должны быть выполнены в<br />
потоке.</li>
<li><code>join([timeout])</code> &#8212; Поток, который вызывает этот метод, приостанавливается, ожидая завершения потока, чей метод вызван. Параметр <code>timeout</code> (число с плавающей точкой) позволяет указать время ожидания (в секундах), по истечении которого приостановленный поток продолжает свою работу независимо от завершения потока, чей метод <code>join</code> был вызван. Вызывать <code>join()</code> некоторого потока можно много раз. Поток не может вызвать метод <code>join()</code> самого себя. Также нельзя ожидать завершения еще не запущенного потока. Слово &#171;join&#187; в переводе с английского означает &#171;присоединить&#187;, то есть, метод, вызвавший <code>join()</code>, желает, чтобы поток по завершении присоединился к вызывающему <strong>метод потоку</strong>.</li>
<li><code>getName() </code>&#8212; Возвращает имя потока. Для главного потока это &#171;<code>MainThread</code>&#171;.</li>
<li><code>setName(name)</code> &#8212; Присваивает потоку имя <strong>name</strong>.</li>
<li><code>isAlive()</code> &#8212; Возвращает истину, если поток работает (метод <code>run()</code> уже вызван, но еще не завершился).</li>
<li><code>isDaemon()</code> &#8212; Возвращает истину, если поток имеет <strong>признак демона</strong>. Программа на Python завершается по завершении всех потоков, не являющихся демонами. Главный поток демоном не является.</li>
<li><code>setDaemon(daemonic)</code> &#8212; Устанавливает признак <strong>daemonic</strong> того, что поток является демоном. Начальное значение этого признака заимствуется у потока, запустившего данный. Признак можно изменять только для потоков, которые еще не запущены.</li>
</ul>
<p><strong>Атрибуты потока:</strong></p>
<ul>
<li><code>t.name</code> &#8212; имя потока</li>
<li><code>t.ident</code> &#8212; Уникальный идентификатор потока (ID) &#8212; None, если поток ещё не запущен</li>
<li><code>threading.current_thread()</code> &#8212; Возвращает объект текущего потока</li>
<li><code>t.daemon = True</code> &#8212; Демон-потоки убиваются при завершении главного потока. Используются для фоновых задач, логирования, heartbeat-потоков. daemon нужно задавать до start()</li>
<li><code>threading.active_count()</code> &#8212; Количество активных потоков</li>
<li><code>threading.enumerate()</code> &#8212; Список всех живых потоков</li>
</ul>
<h2>Реализация потокобезопасной записи результатов с Lock, чтобы избежать race condition</h2>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random
from datetime import datetime

def crawl(link, results, lock):
    print(f"Поток {threading.current_thread().name} запущен. Время вызова: {datetime.now()}")

    # Имитация сетевого запроса
    delay = random.randint(1, 10)
    time.sleep(delay)

    # Имитация полученного JSON
    response = {
        "url": link,
        "status": 200,
        "data": {
            "title": f"Данные с {link}",
            "value": random.randint(1, 100),
        },
        "fetched_at": datetime.now().isoformat(),
        "thread": threading.current_thread().name,
    }

    # Потокобезопасная запись результата
    with lock:
        results[link] = response

    print(f"Поток {threading.current_thread().name} завершен. Запрос длился {delay} секунд. Время завершения: {datetime.now()}")


links = [
    "https://python.org",
    "https://docs.python.org",
    "https://peps.python.org",
]

# Общее хранилище результатов
results = {}

# Lock для синхронизации доступа к results
lock = threading.Lock()

# Создаём потоки
threads = []
for i, link in enumerate(links):
    t = threading.Thread(
        target=crawl,
        args=(link, results, lock),
        name=f"Thread-{i + 1}",
    )
    threads.append(t)

# Запускаем потоки
for t in threads:
    t.start()

# Ждём завершения
for t in threads:
    t.join()

# Итоговый объединённый результат
print("\nИТОГОВЫЙ РЕЗУЛЬТАТ:")
for url, data in results.items():
    print(f"{url} → {data}")</pre><p><strong>Что выполняется в коде:</strong></p>
<ul>
<li><code>t.start()</code> &#8212; Создаёт реальный системный поток. Вызывает <strong>crawl(&#8230;)</strong> в новом потоке. Нельзя вызывать <strong>start()</strong> дважды для одного и того же объекта.</li>
<li><code>t.join()</code> &#8212; Блокирует главный поток и ждёт, пока поток t завершится. Гарантирует, что все данные собраны.</li>
<li><code>threading.current_thread().name</code> &#8212; Позволяет узнать, какой поток сейчас выполняется. Используется для логирования и отладки</li>
</ul>
<table>
<thead>
<tr>
<th>Команда</th>
<th>Где используется</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Thread(...)</code></td>
<td>создание потоков</td>
<td>описание задачи</td>
</tr>
<tr>
<td><code>start()</code></td>
<td>запуск</td>
<td>старт выполнения</td>
</tr>
<tr>
<td><code>join()</code></td>
<td>ожидание</td>
<td>синхронизация</td>
</tr>
<tr>
<td><code>current_thread()</code></td>
<td>внутри <code>crawl</code></td>
<td>диагностика</td>
</tr>
<tr>
<td><code>Lock()</code></td>
<td>защита <code>results</code></td>
<td>потокобезопасность</td>
</tr>
</tbody>
</table>
<p><strong>Результат выполнения скрипта:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">Поток Thread-1 запущен. Время вызова: 2025-12-27 20:45:11.807784
Поток Thread-2 запущен. Время вызова: 2025-12-27 20:45:11.807957
Поток Thread-3 запущен. Время вызова: 2025-12-27 20:45:11.808193
Поток Thread-3 завершен. Запрос длился 1 секунд. Время завершения: 2025-12-27 20:45:12.808385
Поток Thread-1 завершен. Запрос длился 4 секунд. Время завершения: 2025-12-27 20:45:15.808069
Поток Thread-2 завершен. Запрос длился 10 секунд. Время завершения: 2025-12-27 20:45:21.808165

ИТОГОВЫЙ РЕЗУЛЬТАТ:
https://peps.python.org → {'url': 'https://peps.python.org', 'status': 200, 'data': {'title': 'Данные с https://peps.python.org', 'value': 15}, 'fetched_at': '2025-12-27T20:45:12.808319', 'thread': 'Thread-3'}
https://python.org → {'url': 'https://python.org', 'status': 200, 'data': {'title': 'Данные с https://python.org', 'value': 84}, 'fetched_at': '2025-12-27T20:45:15.808019', 'thread': 'Thread-1'}
https://docs.python.org → {'url': 'https://docs.python.org', 'status': 200, 'data': {'title': 'Данные с https://docs.python.org', 'value': 17}, 'fetched_at': '2025-12-27T20:45:21.808113', 'thread': 'Thread-2'}</pre><p></p>
<h2>Основы threading</h2>
<h3>Создание потока через конструктор <code>threading.Thread</code></h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">threading.Thread(
    target=None,
    args=(),
    kwargs={},
    name=None,
    daemon=None
)</pre><p>Основные параметры:</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Описание</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>target</code></td>
<td>Функция, которая будет выполнена в потоке</td>
</tr>
<tr>
<td><code>args</code></td>
<td>Кортеж позиционных аргументов</td>
</tr>
<tr>
<td><code>kwargs</code></td>
<td>Именованные аргументы</td>
</tr>
<tr>
<td><code>name</code></td>
<td>Имя потока</td>
</tr>
<tr>
<td><code>daemon</code></td>
<td>Демон-поток (<code>True/False</code>)</td>
</tr>
</tbody>
</table>
<h3>Запуск и управление потоками <code>t.start()</code></h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.start()</pre><p></p>
<ul>
<li>Запускает поток</li>
<li>Внутри вызывает run()</li>
<li>Нельзя вызвать повторно</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.run()</pre><p></p>
<ul>
<li>Содержит код потока</li>
<li>Не запускает новый поток, если вызвать напрямую</li>
<li>Обычно не вызывается вручную</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">join(timeout=None)</pre><p></p>
<ul>
<li>Ждёт завершения потока</li>
<li><code>timeout</code> — максимальное время ожидания (в секундах)</li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">t.is_alive()</pre><p></p>
<ul>
<li>Возвращает <code>True</code>, если поток ещё работает</li>
</ul>
<h2>Примитивы синхронизации: Lock, RLock, Semaphore, Event, Condition</h2>
<p><strong>Подробнее:</strong> <a href="https://devpractice.ru/python-lesson-23-concurrency-part-2/" target="_blank" rel="noopener">Python. Урок 23. Потоки и процессы в Python. Часть 2. Синхронизация потоков</a></p>
<hr />
<p>В Python примитивы синхронизации из модуля <code>threading</code> решают одну ключевую задачу: они позволяют нескольким потокам безопасно и предсказуемо взаимодействовать с общим состоянием. Несмотря на наличие <strong>GIL</strong>, эти примитивы остаются необходимыми, потому что <strong>GIL</strong> защищает интерпретатор, но не бизнес-логику и не целостность данных.</p>
<p>Диспетчеры контекста предусмотрены для всех объектов модуля <code>threading</code>, таких как <strong>Lock</strong>, <strong>RLock</strong>, <strong>Condition</strong>, <strong>Semaphore</strong> и <strong>BoundedSemaphore</strong>, то есть для работы с этими объектами может применяться инструкция <code>with</code>.</p>
<p>Начнём с <strong>Lock</strong> и <strong>RLock</strong>, так как они лежат в основе почти всех сценариев синхронизации.</p>
<h3><strong>Lock</strong></h3>
<p><strong>Lock</strong> — это обычный мьютекс, который может быть захвачен только одним потоком в конкретный момент времени. Когда поток вызывает <code>acquire()</code>, он либо сразу получает доступ к критической секции, либо блокируется до тех пор, пока другой поток не освободит <code>lock</code>. После выполнения защищённого участка кода поток обязан вызвать <code>release()</code>. В реальном коде <code>Lock</code> почти всегда используется через контекстный менеджер <code>with</code>, потому что это гарантирует освобождение блокировки даже при исключении. <code>Lock</code> подходит для защиты простых структур данных, таких как <strong>словари</strong>, <strong>списки</strong> или <strong>счётчики</strong>, и <strong>для коротких критических секций</strong>. Важно понимать, что <strong>один и тот же поток не может захватить Lock повторно</strong>: попытка сделать это приведёт к <code>deadlock</code>, когда поток будет ждать самого себя.</p>
<p><strong>Проблема (без Lock)</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

counter = 0

def increment():
    global counter
    for _ in range(100_000):
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> НЕ гарантировано 200000</pre><p><strong>Решение с Lock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:          # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> только один поток внутри
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]

for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)  # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> всегда 200000</pre><p><strong>Что гарантирует Lock</strong></p>
<ul>
<li>Только один поток изменяет общее состояние</li>
<li>Нет гонок данных</li>
</ul>
<h3><strong>RLock</strong></h3>
<p>Эта проблема решается с помощью <strong>RLock</strong>, или <strong>reentrant lock</strong>. По сути это мьютекс с учётом владельца. Поток, который уже владеет <code>RLock</code>, может захватить его ещё раз, и <strong>Python</strong> просто увеличит внутренний счётчик захватов. Освобождать такой <code>lock</code> нужно столько же раз, сколько он был захвачен. <code>RLock</code> необходим в более сложных архитектурах, когда функции с защитой <code>lock</code> вызывают друг друга, либо когда публичный метод и внутренний метод используют одну и ту же блокировку. Без <code>RLock</code> такой код почти неизбежно приводит к взаимной блокировке.</p>
<p><strong>Проблема с Lock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">lock = threading.Lock()

def outer():
    with lock:
        inner()

def inner():
    with lock:   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> deadlock: поток уже держит lock
        print("inner")</pre><p><strong>Решение с RLock</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading

lock = threading.RLock()

def outer():
    with lock:
        print("outer")
        inner()

def inner():
    with lock:
        print("inner")

threading.Thread(target=outer).start()</pre><p><strong>Что даёт RLock</strong></p>
<ul>
<li>Один поток может захватывать блокировку несколько раз</li>
<li>Важно для рекурсии и вложенных вызовов</li>
</ul>
<h3><strong>Semaphore</strong></h3>
<p>Следующий важный примитив — <strong>Semaphore</strong>. В отличие от <strong>Lock</strong>, который допускает ровно одного владельца, <code>semaphore</code> разрешает одновременно находиться в критической секции ограниченному числу потоков. При создании семафора задаётся счётчик, который уменьшается при <code>acquire()</code> и увеличивается при <code>release()</code>. Пока счётчик положительный, потоки могут входить без ожидания, а когда он становится равным нулю, все последующие вызовы <code>acquire()</code> блокируются. Семантически <strong>семафор</strong> описывает не владение ресурсом, а <strong>количество доступных слотов</strong>. Это делает его удобным для ограничения параллельного доступа к внешним системам, таким как <strong>база данных</strong>, <strong>пул соединений</strong> или <strong>сторонний API</strong>. В отличие от <strong>Lock</strong>, <strong>семафор</strong> не привязан к конкретному потоку, поэтому важно строго соблюдать баланс <code>acquire()</code> и <code>release()</code>, иначе система либо «утечёт» в блокировку, либо начнёт пускать больше потоков, чем предполагалось.</p>
<p>Пример: максимум 2 потока одновременно</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

semaphore = threading.Semaphore(2)

def worker(name):
    print(f"{name} ждёт доступ")
    with semaphore:
        print(f"{name} вошёл")
        time.sleep(2)
        print(f"{name} вышел")

threads = [
    threading.Thread(target=worker, args=(f"Thread-{i}",))
    for i in range(5)
]

for t in threads:
    t.start()</pre><p><strong>Что гарантирует Semaphore</strong></p>
<ul>
<li>Не более N потоков внутри секции</li>
<li>Остальные ждут освобождения ресурса</li>
</ul>
<h4><strong>BoundedSemaphore</strong></h4>
<p><strong>BoundedSemaphore</strong> — это вариант семафора из модуля <code>threading</code>, который предназначен для строгого контроля количества одновременных доступов к ресурсу и дополнительно защищает от логических ошибок в коде.</p>
<p>По своей сути <strong>BoundedSemaphore</strong> работает так же, как обычный <strong>Semaphore</strong>: он хранит внутренний счётчик, и поток может войти в критическую секцию, только если счётчик больше нуля. При входе счётчик уменьшается, при выходе увеличивается. Это позволяет ограничить количество потоков, которые одновременно используют общий ресурс.</p>
<p>Ключевое отличие <strong>BoundedSemaphore</strong> от <strong>Semaphore</strong> заключается в том, что он не позволяет превысить начальное значение счётчика. Если вызвать <code>release()</code> больше раз, чем было успешных <code>acquire()</code>, <strong>BoundedSemaphore</strong> выбросит исключение <strong>ValueError</strong>. Обычный <strong>Semaphore</strong> такого не делает и молча увеличивает счётчик, что может привести к незаметным ошибкам и нарушению инвариантов программы.</p>
<p>Таким образом, <strong>BoundedSemaphore</strong> полезен в ситуациях, где важно гарантировать, что количество «освобождений» ресурса строго соответствует количеству его захватов, например при реализации пулов соединений или управлении ограниченными системными ресурсами. Он помогает выявлять ошибки проектирования на раннем этапе, вместо того чтобы позволять программе продолжать работу в некорректном состоянии.</p>
<p>Пример использования <strong>BoundedSemaphore</strong> для ограничения числа одновременных работников:</p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

pool = threading.BoundedSemaphore(2)

def worker(name):
    print(f"{name} пытается войти")
    pool.acquire()
    try:
        print(f"{name} работает")
        time.sleep(1)
    finally:
        pool.release()
        print(f"{name} вышел")

threads = [
    threading.Thread(target=worker, args=(f"Thread-{i}",))
    for i in range(4)
]

for t in threads:
    t.start()
for t in threads:
    t.join()</pre><p>Если в этом примере по ошибке вызвать <code>pool.release()</code> дважды в одном потоке, программа сразу упадёт с <code>ValueError</code>, что явно укажет на ошибку управления ресурсом. Именно это поведение и является главным практическим отличием <strong>BoundedSemaphore</strong> от обычного <strong>Semaphore</strong>.</p>
<h3><strong>Event</strong></h3>
<p><strong>Event</strong> решает другую задачу и не предназначен для защиты критических секций. Это <strong>потокобезопасный флаг</strong>, который может быть установлен или сброшен, и который другие потоки могут проверять или ожидать. Внутренне <code>Event</code> хранит состояние <strong>«установлен»</strong> или <strong>«не установлен»</strong>. Когда поток вызывает <code>wait()</code>, он блокируется до тех пор, пока другой поток не вызовет <code>set()</code>. Если событие уже установлено, <code>wait()</code> возвращается сразу. В отличие от <strong>lock</strong>-ов, событие не «потребляется» при ожидании, и все потоки, ожидающие одного и того же события, будут разбужены одновременно. На практике <strong>Event</strong> чаще всего используется для управления жизненным циклом потоков, например для корректной остановки воркеров или для сигнализации о готовности системы к работе. Это более выразительная и безопасная альтернатива общим флагам и бесконечным циклам с <code>sleep</code>.</p>
<p><strong>Пример: ожидание старта</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time

event = threading.Event()

def worker():
    print("Рабочий поток ждёт сигнал...")
    event.wait()          # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> блокируется
    print("Рабочий поток получил сигнал!")

def starter():
    time.sleep(3)
    print("Сигнал отправлен!")
    event.set()           # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6a6.png" alt="🚦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> разблокирует всех

threading.Thread(target=worker).start()
threading.Thread(target=starter).start()</pre><p><strong>Event идеально подходит для</strong></p>
<ul>
<li>старт / стоп сигналов</li>
<li><strong>graceful shutdown</strong> &#8212; (плавное или корректное завершение работы) &#8212; это процесс остановки компьютерной системы (приложения, сервера, контейнера), при котором она успевает выполнить необходимые задачи по очистке и сохранению данных перед полным выключением.</li>
<li>ожидания готовности ресурса</li>
</ul>
<h3><strong>Condition</strong></h3>
<p><strong>Condition</strong> является самым сложным и одновременно самым гибким примитивом синхронизации. Он объединяет в себе <strong>мьютекс</strong> и <strong>механизм ожидания уведомлений</strong>. Идея <strong>Condition</strong> заключается в том, что поток может ждать не просто сигнала, а выполнения определённого логического условия, связанного с состоянием программы. Поток захватывает условие, проверяет состояние, и если оно не удовлетворяет требованиям, вызывает <code>wait()</code>. При этом <strong>lock</strong> временно освобождается, чтобы другие потоки могли изменить состояние. Когда другой поток вызывает <code>notify()</code> или <code>notify_all()</code>, ожидающие потоки пробуждаются и снова проверяют условие. Именно повторная проверка условия является ключевым моментом, так как пробуждение не гарантирует, что состояние действительно изменилось нужным образом. <strong>Condition</strong> активно используется в классических <strong>паттернах producer–consumer</strong>, <strong>очередях задач</strong> и системах, где потоки должны реагировать на изменение общего состояния, а не просто на факт события.</p>
<p>Если смотреть на эти примитивы как на систему, то <strong>Lock</strong> и <strong>RLock</strong> отвечают за эксклюзивный доступ, <strong>Semaphore</strong> ограничивает параллелизм, <strong>Event</strong> передаёт сигналы между потоками, а <strong>Condition</strong> позволяет потокам координироваться на основе сложных условий. В продакшене выбор примитива почти всегда диктуется смыслом задачи, а не техническими деталями. Хорошая синхронизация делает код не только корректным, но и читаемым, потому что по выбранному примитиву сразу понятно, как именно потоки должны взаимодействовать друг с другом.</p>
<p><strong>Producer / Consumer с Condition</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">import threading
import time
import random

condition = threading.Condition()
queue = []
MAX_ITEMS = 5

def producer():
    for i in range(10):
        time.sleep(random.uniform(0.1, 0.5))
        with condition:
            while len(queue) &gt;= MAX_ITEMS:
                condition.wait()   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ждём, пока потребитель заберёт
            queue.append(i)
            print(f"Producer добавил {i}")
            condition.notify()     # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f514.png" alt="🔔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> сигнал потребителю

def consumer():
    for _ in range(10):
        with condition:
            while not queue:
                condition.wait()   # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/23f3.png" alt="⏳" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ждём данные
            item = queue.pop(0)
            print(f"Consumer забрал {item}")
            condition.notify()     # <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f514.png" alt="🔔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> сигнал производителю
        time.sleep(random.uniform(0.2, 0.6))

threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()</pre><p><strong>Condition = Lock + Event</strong></p>
<ul>
<li><code>wait()</code> → отпускает lock и ждёт</li>
<li><code>notify()</code> → будит ожидающий поток</li>
<li>Позволяет ждать логических условий, а не просто блокировки</li>
</ul>
<h3><strong>Barrier</strong></h3>
<p><code>threading.Barrier</code> — это примитив синхронизации в Python, который позволяет группе потоков одновременно ожидать друг друга в определенной точке выполнения (контрольной точке) перед тем, как продолжить работу.</p>
<p>todo</p>
<h3><strong>Итоговая таблица</strong></h3>
<table>
<thead>
<tr>
<th>Примитив</th>
<th>Для чего</th>
</tr>
</thead>
<tbody>
<tr>
<td><code inline="">Lock</code></td>
<td>Простая защита общего состояния</td>
</tr>
<tr>
<td><code inline="">RLock</code></td>
<td>Вложенные / рекурсивные блокировки</td>
</tr>
<tr>
<td><code inline="">Semaphore</code></td>
<td>Ограничение количества потоков</td>
</tr>
<tr>
<td><code inline="">Event</code></td>
<td>Сигналы между потоками</td>
</tr>
<tr>
<td><code inline="">Condition</code></td>
<td>Сложная координация и ожидание условий</td>
</tr>
</tbody>
</table>
<h1>concurrent.futures</h1>
<p>todo</p>
<h1>multiprocessing</h1>
<p>Теория<br />
Разница между:<br />
fork / spawn / forkserver</p>
<p>IPC (межпроцессное взаимодействие):<br />
Queue<br />
Pipe<br />
Manager<br />
Стоимость сериализации (pickle)<br />
Copy-on-write (Linux)</p>
<p>Практика<br />
Параллельная обработка данных<br />
Использование multiprocessing.Pool</p>
<p>Бенчмарк:<br />
threading vs multiprocessing<br />
Поймать баг с pickling’ом</p>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Продакшн insight: multiprocessing часто убивает latency, если использовать бездумно.</p>
<h1>asyncio</h1>
<p>Ключевая тема для highload backend.</p>
<p>Теория<br />
Event loop<br />
Coroutine<br />
Awaitable<br />
Task vs Future<br />
Cooperative multitasking<br />
Почему async ≠ threading</p>
<p>Практика<br />
Переписать синхронный код в async<br />
Одновременные HTTP-запросы (aiohttp)<br />
Ограничение параллелизма (Semaphore)</p>
<p>Ошибки:<br />
blocking call внутри async<br />
забытый await</p>
<p>Критически важно: понимание, почему один blocking вызов убивает весь сервис.</p>
<p>Сообщение <a href="https://datatalks.ru/python-threading-multiprocessing-asyncio/">Python &#8212; Многозадачность, конкурентность и асинхронность</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/python-threading-multiprocessing-asyncio/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Введение в Структуры данных (Data Structures) и алгоритмы</title>
		<link>https://datatalks.ru/data-structures-and-algorithms/</link>
					<comments>https://datatalks.ru/data-structures-and-algorithms/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 19 Jul 2025 15:27:09 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Python Interview]]></category>
		<category><![CDATA[Python Алгоритмы]]></category>
		<category><![CDATA[Python Собеседование]]></category>
		<category><![CDATA[Python Структуры данных]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1830</guid>

					<description><![CDATA[<p>Введение Нотация Big O Нотация Big O — это математический способ описывать асимптотическую сложность алгоритмов, то есть то, как растут затраты ресурсов при увеличении размера входных данных n. Она абстрагируется от конкретного железа, языка и констант и показывает порядок роста, что делает её универсальным инструментом анализа. Временная сложность (time complexity) описывает, как растёт количество элементарных [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/data-structures-and-algorithms/">Введение в Структуры данных (Data Structures) и алгоритмы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Введение</h1>
<h2>Нотация Big O</h2>
<p><strong>Нотация Big O</strong> — это математический способ описывать <strong>асимптотическую сложность</strong> алгоритмов, то есть то, как растут затраты ресурсов при увеличении <strong>размера входных данных n</strong>. Она абстрагируется от конкретного железа, языка и констант и показывает порядок роста, что делает её универсальным инструментом анализа.</p>
<p><strong>Временная сложность (time complexity)</strong> описывает, как растёт количество элементарных операций алгоритма в зависимости от <strong>n</strong>. Формально говорят, что алгоритм имеет сложность <code>O(f(n))</code>, если существует такая константа <strong>C</strong>, что при достаточно больших <strong>n</strong> время работы не превышает <code>C · f(n)</code>. <strong>Важный момент:</strong> учитывается <strong>худший случай</strong>, если не указано иное. Константы, коэффициенты и члены меньшего порядка отбрасываются, потому что при <strong>больших n</strong> они не влияют на характер роста. Например, <code>3n² + 10n + 5</code> в <strong>Big O</strong> записывается как <code>O(n²)</code>.</p>
<p><strong>Пространственная сложность (space complexity)</strong> описывает, как растёт <strong>объём дополнительной памяти</strong>, используемой алгоритмом, также как функция от <strong>n</strong>. Обычно считают дополнительную память, а не входные данные. Например, <strong>сортировка «на месте»</strong> может иметь <code>O(1)</code> по памяти, а алгоритм, создающий вспомогательный массив размера <strong>n</strong>, — <code>O(n)</code>.</p>
<p>Интуитивно <strong>Big O</strong> отвечает не на вопрос «сколько именно секунд или мегабайт», а на вопрос <strong>«насколько хуже станет, если данных станет в 10 раз больше»</strong>. Именно поэтому нотация широко используется в теории алгоритмов и на практике при выборе подходящего решения.</p>
<p>Часто встречающиеся порядки роста, от лучшего к худшему, выглядят так:</p>
<ul>
<li><strong>константный O(1)</strong> — затраты не зависят от размера входа;</li>
<li><strong>логарифмический O(log n)</strong> — рост очень медленный (типично для бинарного поиска);</li>
<li><strong>линейный O(n)</strong> — один проход по данным;</li>
<li><strong>квазилинейный O(n log n)</strong> — характерен для эффективных сортировок;</li>
<li><strong>квадратичный O(n²)</strong> и выше — быстро становятся непрактичными при больших <strong>n</strong>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2735" src="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg" alt="" width="1330" height="950" srcset="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg 1330w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-300x214.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-1024x731.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-768x549.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-450x321.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-780x557.jpeg 780w" sizes="(max-width: 1330px) 100vw, 1330px" /></a></p>
<p>Важно понимать, что <strong>Big O</strong> — это верхняя оценка, а не точный прогноз. Она не заменяет профилирование, но позволяет заранее отсеять заведомо плохие алгоритмы и сравнивать решения на концептуальном уровне.</p>
<table>
<thead>
<tr>
<th>Нотация Big O</th>
<th>Название</th>
<th>Описание</th>
<th>Пример алгоритма</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>O(1)</strong></td>
<td>Константная</td>
<td>Время и/или память не зависят от размера входных данных</td>
<td>Доступ к элементу массива по индексу <code inline="">a[i]</code>, <code inline="">dict[key]</code></td>
</tr>
<tr>
<td><strong>O(log n)</strong></td>
<td>Логарифмическая</td>
<td>Рост очень медленный: при увеличении входа в 2 раза добавляется 1 шаг</td>
<td>Бинарный поиск, операции в сбалансированных деревьях</td>
</tr>
<tr>
<td><strong>O(log² n)</strong></td>
<td>Полилогарифмическая</td>
<td>Чаще всего результат вложенных логарифмических операций</td>
<td>Некоторые алгоритмы на деревьях и графах</td>
</tr>
<tr>
<td><strong>O(√n)</strong></td>
<td>Сублинейная</td>
<td>Медленнее линейной, но быстрее логарифмической</td>
<td>Проверка простоты числа перебором до √n</td>
</tr>
<tr>
<td><strong>O(n)</strong></td>
<td>Линейная</td>
<td>Время пропорционально размеру входа</td>
<td>Линейный поиск, подсчёт суммы элементов</td>
</tr>
<tr>
<td><strong>O(n log n)</strong></td>
<td>Квазилинейная</td>
<td>Типична для эффективных алгоритмов сортировки</td>
<td>Merge sort, Heap sort, Quick sort (в среднем)</td>
</tr>
<tr>
<td><strong>O(n log² n)</strong></td>
<td>Почти квазилинейная</td>
<td>Встречается в более сложных алгоритмах</td>
<td>Некоторые алгоритмы на строках и графах</td>
</tr>
<tr>
<td><strong>O(n²)</strong></td>
<td>Квадратичная</td>
<td>Обычно два вложенных цикла по входным данным</td>
<td>Bubble sort, Insertion sort (в худшем случае)</td>
</tr>
<tr>
<td><strong>O(n³)</strong></td>
<td>Кубическая</td>
<td>Три вложенных цикла, быстро становится непрактичной</td>
<td>Алгоритм Флойда–Уоршелла</td>
</tr>
<tr>
<td><strong>O(nᵏ)</strong></td>
<td>Полиномиальная</td>
<td>Обобщение квадратичной и кубической</td>
<td>Многие алгоритмы из теории NP</td>
</tr>
<tr>
<td><strong>O(2ⁿ)</strong></td>
<td>Экспоненциальная</td>
<td>Удваивается при каждом новом элементе</td>
<td>Перебор всех подмножеств</td>
</tr>
<tr>
<td><strong>O(3ⁿ)</strong></td>
<td>Экспоненциальная</td>
<td>Ещё быстрее растущая экспонента</td>
<td>Некоторые задачи динамики без оптимизации</td>
</tr>
<tr>
<td><strong>O(n!)</strong></td>
<td>Факториальная</td>
<td>Практически неиспользуема для больших n</td>
<td>Задача коммивояжёра перебором</td>
</tr>
<tr>
<td><strong>O(cⁿ)</strong></td>
<td>Экспоненциальная (обобщённая)</td>
<td>Любая константа &gt; 1 в степени n</td>
<td>Brute-force задачи</td>
</tr>
<tr>
<td><strong>O(n + m)</strong></td>
<td>Линейная по входу</td>
<td>Зависит от нескольких параметров</td>
<td>Алгоритмы на графах (вершины + рёбра)</td>
</tr>
<tr>
<td><strong>O(n·m)</strong></td>
<td>Двухпараметрическая</td>
<td>Часто в графах и матрицах</td>
<td>Обход всех пар вершин</td>
</tr>
<tr>
<td><strong>O(1) память</strong></td>
<td>Константная память</td>
<td>Используется фиксированное количество памяти</td>
<td>In-place сортировки</td>
</tr>
<tr>
<td><strong>O(n) память</strong></td>
<td>Линейная память</td>
<td>Дополнительная память пропорциональна входу</td>
<td>Вспомогательный массив</td>
</tr>
<tr>
<td><strong>O(n²) память</strong></td>
<td>Квадратичная память</td>
<td>Хранение матрицы или таблицы</td>
<td>Матрицы смежности</td>
</tr>
</tbody>
</table>
<h3><strong>Шпаргалка по Big O</strong></h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2732" src="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png" alt="" width="2560" height="1841" srcset="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-300x216.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1024x737.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-768x552.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1536x1105.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-2048x1473.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-450x324.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-780x561.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1600x1151.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h3><strong>Профилирование</strong></h3>
<p><strong>Профилирование</strong> — это процесс измерения и анализа фактического поведения программы во время выполнения. Его цель — понять, куда реально тратится время и память, какие части кода являются узкими местами и какие оптимизации действительно имеют смысл.</p>
<p>В отличие от асимптотического анализа и <strong>Big O</strong>, профилирование работает не с абстрактными оценками, а с конкретными метриками: временем выполнения функций, числом вызовов, потреблением памяти, частотой аллокаций, загрузкой CPU. Часто оказывается, что код с хорошей теоретической сложностью работает медленно из-за констант, кэш-промахов, лишних аллокаций или неудачных структур данных — и именно профилирование это выявляет.</p>
<p><strong>Обычно процесс выглядит так:</strong> программу запускают с реальными или приближенными к реальным входными данными, собирают статистику выполнения, затем анализируют отчёт профайлера. Почти всегда подтверждается <strong>правило 80/20</strong>: небольшая часть кода потребляет большую часть ресурсов. Оптимизация без профилирования часто приводит к бессмысленной усложнённости и микроправкам там, где выигрыш минимален.</p>
<p><strong>Различают несколько основных видов профилирования:</strong></p>
<ul>
<li><strong>Временное профилирование</strong> показывает, сколько времени тратится на функции и строки кода.</li>
<li><strong>Профилирование памяти</strong> показывает рост и распределение потребления памяти, утечки и лишние аллокации.</li>
<li><strong>CPU-профилирование</strong> анализирует загрузку процессора и горячие участки кода.</li>
<li>Есть также <strong>событийное</strong> и <strong>трассировочное профилирование</strong>, полезное для сложных систем.</li>
</ul>
<p>В контексте Python профилирование особенно важно, потому что язык интерпретируемый, и реальные узкие места часто находятся не там, где их ожидают по Big O. Стандартный модуль <strong>cProfile</strong> позволяет получить детальный отчёт по времени выполнения функций, <code>line_profiler</code> показывает стоимость отдельных строк, а <code>memory_profiler</code> помогает анализировать использование памяти.</p>
<p><strong>Ключевая идея проста:</strong> <strong>Big O</strong> говорит, как алгоритм будет масштабироваться, а <strong>профилирование</strong> показывает, как он работает на самом деле. В хорошей инженерной практике <strong>сначала пишут корректный и понятный код, затем профилируют</strong>, и только после этого оптимизируют то, что действительно мешает производительности.</p>
<h3><strong>Динамическое программирование</strong></h3>
<p><strong>Динамическое программирование (dynamic programming, DP)</strong> — это метод проектирования алгоритмов, который применяется к задачам, где решение можно разложить на перекрывающиеся подзадачи, а итоговый ответ строится из решений этих подзадач. Ключевая идея: не решать одну и ту же подзадачу больше одного раза.</p>
<p>Если кратко: рекурсия даёт корректность, а динамическое программирование — эффективность.</p>
<p>Когда задача решается динамическим программированием</p>
<p>У задачи должны быть два свойства.</p>
<p>Первое — оптимальная подструктура. Это означает, что оптимальное решение всей задачи можно получить из оптимальных решений её подзадач. Например, кратчайший путь до точки i проходит через кратчайший путь до предыдущих точек.</p>
<p>Второе — перекрывающиеся подзадачи. Одни и те же подзадачи возникают многократно. Обычная рекурсия будет пересчитывать их снова и снова, а DP — сохраняет и переиспользует результат.</p>
<p>Если хотя бы одно из этих свойств отсутствует, динамическое программирование либо не нужно, либо не применимо.</p>
<h4>Основные подходы DP</h4>
<p><strong>Top-down (memoization)</strong></p>
<p>Решение пишется как рекурсивная функция, но результаты вызовов сохраняются в кэше. При повторном обращении к подзадаче результат берётся из памяти, а не пересчитывается.</p>
<p>Плюс: код близок к математическому определению.<br />
Минус: накладные расходы рекурсии и риск переполнения стека.</p>
<p><strong>Bottom-up (tabulation)</strong></p>
<p>Решение строится итеративно, начиная с самых простых подзадач и постепенно переходя к более сложным.</p>
<p>Плюс: нет рекурсии, обычно быстрее и надёжнее.<br />
Минус: иногда сложнее понять и вывести формулы.</p>
<p>Оба подхода эквивалентны по асимптотике.</p>
<h4><strong>Классический пример: числа Фибоначчи</strong></h4>
<p>Наивная рекурсия имеет экспоненциальную сложность, потому что одно и то же значение вычисляется много раз.</p>
<p>DP-решение хранит уже вычисленные значения и получает сложность O(n) по времени. При этом память можно сократить до O(1), храня только два последних значения. Это важный момент: DP часто можно оптимизировать по памяти.</p>
<h4><strong>Как выглядит DP-решение концептуально</strong></h4>
<p>Почти любая задача DP решается по одному шаблону:</p>
<p>Определяется состояние DP — что именно мы храним (например, dp[i] — ответ для первых i элементов).</p>
<p>Формулируется переход — как получить dp[i] из более простых состояний.</p>
<p>Задаются базовые случаи.</p>
<p>Определяется порядок вычислений.</p>
<p>При необходимости оптимизируется память.</p>
<p>Умение правильно выбрать состояние — самый сложный этап.</p>
<h4><strong>Основные типы задач динамического программирования</strong></h4>
<p>DP на одномерном массиве<br />
Примеры: Фибоначчи, максимальная сумма подмассива, лестница с шагами.</p>
<p>DP на двумерной таблице<br />
Примеры: наибольшая общая подпоследовательность (LCS), расстояние Левенштейна, рюкзак.</p>
<p>DP на строках<br />
Сравнение строк, редактирование, палиндромы.</p>
<p>DP на деревьях (Tree DP)<br />
Решения строятся в postorder-обходе. Примеры: диаметр дерева, максимальный путь.</p>
<p>DP по подмножествам (Bitmask DP)<br />
Используется, когда количество элементов мало (обычно до 20). Пример: задача коммивояжёра.</p>
<p>DP с оптимизацией переходов<br />
Монотонная очередь, divide and conquer DP, convex hull trick — продвинутые темы для сильных кандидатов.</p>
<h4><strong>Временная и пространственная сложность</strong></h4>
<p>Сложность DP обычно равна количеству состояний, умноженному на стоимость перехода.<br />
Например, таблица n × m с O(1) переходом даёт O(nm) по времени и памяти.</p>
<p>Очень частая ошибка — недооценивать размер пространства состояний и получить решение, которое не помещается в память или не укладывается по времени.</p>
<h4><strong>Типичные ошибки на собеседованиях</strong></h4>
<p>Писать рекурсию без мемоизации.<br />
Выбирать слишком большое состояние DP.<br />
Не замечать, что задачу можно решить жадно или через бинарный поиск.<br />
Забывать про оптимизацию памяти, когда она очевидна.</p>
<h4><strong>Как понять, что тут DP</strong></h4>
<p>Фразы-триггеры в условиях:<br />
«найти максимальный/минимальный…»,<br />
«сколькими способами…»,<br />
«оптимальное решение»,<br />
«учитывая предыдущие шаги».</p>
<p>Если решение зависит от предыдущих решений и эти зависимости повторяются — почти наверняка это динамическое программирование.</p>
<h4><strong>Резюме</strong></h4>
<p>Динамическое программирование — это не конкретный алгоритм, а образ мышления. Ты разбиваешь задачу на подзадачи, сохраняешь результаты и собираешь ответ снизу вверх или сверху вниз. Именно поэтому DP считается одной из самых сложных, но и самых мощных тем на собеседованиях.</p>
<h2><strong>Алгоритмы сортировки</strong></h2>
<p><strong>Bubble Sort</strong> — одна из самых простых и наименее эффективных сортировок. Алгоритм многократно проходит по массиву и сравнивает соседние элементы, меняя их местами, если они стоят в неправильном порядке. После каждого прохода самый большой элемент «всплывает» в конец массива, отсюда и название. В худшем и среднем случае сложность по времени составляет O(n²), так как выполняются вложенные проходы. В лучшем случае (если массив уже отсортирован и есть оптимизация с флагом) — O(n). По памяти алгоритм работает за O(1), так как сортирует массив на месте. Bubble Sort стабилен, но практически не используется из-за низкой эффективности.</p>
<p><strong>Selection Sort</strong> работает иначе: на каждом шаге он находит минимальный элемент в неотсортированной части массива и ставит его на правильную позицию. Число сравнений всегда одинаково, независимо от входных данных, поэтому и лучший, и худший, и средний случаи имеют сложность O(n²). Память — O(1), так как сортировка выполняется на месте. Алгоритм не является стабильным, но имеет предсказуемое поведение и минимальное число обменов, что иногда полезно при дорогих операциях записи.</p>
<p><strong>Insertion Sort</strong> имитирует процесс сортировки карт вручную. Алгоритм берёт элементы по одному и вставляет каждый новый элемент в уже отсортированную часть массива, сдвигая большие элементы вправо. В худшем и среднем случае сложность O(n²), но в лучшем случае, когда массив почти отсортирован, — O(n). По памяти — O(1). Insertion Sort стабилен и очень эффективен на маленьких массивах или как вспомогательная сортировка внутри более сложных алгоритмов.</p>
<p><strong>QuickSort</strong> — одна из самых популярных и быстрых сортировок на практике. Он использует стратегию «разделяй и властвуй»: выбирается опорный элемент (pivot), массив разбивается на элементы меньше и больше него, после чего рекурсивно сортируются подмассивы. Средняя сложность по времени — O(n log n), но в худшем случае (неудачный выбор pivot, например уже отсортированный массив) — O(n²). Память обычно O(log n) из-за глубины рекурсии. QuickSort не стабилен, но выигрывает за счёт хорошей локальности данных и отсутствия дополнительной памяти.</p>
<p><strong>Merge Sort</strong> также основан на «разделяй и властвуй». Массив рекурсивно делится пополам, затем отсортированные половины сливаются в один отсортированный массив. В отличие от QuickSort, Merge Sort всегда работает за O(n log n), независимо от входных данных. Основной минус — необходимость дополнительной памяти O(n) для слияния. Алгоритм стабилен и хорошо подходит для сортировки связанных списков и внешней сортировки (когда данные не помещаются в память).</p>
<p><strong>Heap Sort</strong> использует структуру данных «куча» (обычно бинарная max-heap). Сначала массив преобразуется в кучу, затем максимальный элемент извлекается и помещается в конец массива, после чего куча перестраивается. Сложность по времени — O(n log n) во всех случаях, память — O(1), так как сортировка выполняется на месте. Heap Sort не стабилен и на практике часто медленнее QuickSort из-за плохой кэш-локальности, но ценится за гарантированное время работы.</p>
<p><strong>Counting Sort</strong> — не сравнительная сортировка. Она работает только для целочисленных ключей из ограниченного диапазона. Алгоритм подсчитывает, сколько раз встречается каждое значение, а затем по этим подсчётам восстанавливает отсортированный массив. Время работы — O(n + k), где k — размер диапазона значений. Память также O(n + k). Counting Sort стабилен и очень быстр, но применим только при небольшом диапазоне ключей.</p>
<p><strong>Radix Sort</strong> — тоже не сравнительная сортировка. Она сортирует числа (или строки) поразрядно, начиная с младшего или старшего разряда, используя стабильную сортировку (часто Counting Sort) на каждом шаге. Если число разрядов равно d, а диапазон разряда k, то сложность — O(d · (n + k)). Память — O(n + k). Radix Sort эффективен для больших массивов чисел фиксированной длины, но менее универсален, чем сравнительные алгоритмы.</p>
<h2><strong>Алгоритмы обхода деревьев</strong></h2>
<p><strong>Бинарное дерево</strong> — это иерархическая структура данных, состоящая из узлов, где у каждого узла не более двух потомков. Эти потомки обычно называют левым и правым ребёнком. Структура начинается с одного выделенного узла — корня, от которого рекурсивно строятся все остальные узлы.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2740" src="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg" alt="" width="512" height="357" srcset="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg 512w, https://datatalks.ru/wp-content/uploads/2025/07/binary_tree-300x209.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/binary_tree-450x314.jpeg 450w" sizes="(max-width: 512px) 100vw, 512px" /></a></p>
<p>Каждый <strong>узел</strong> бинарного дерева содержит <strong>значение (данные)</strong> и <strong>ссылки</strong> на своих детей. Если у <strong>узла</strong> нет детей, он называется <strong>листом</strong>. Если у узла есть хотя бы один ребёнок, он считается <strong>внутренним узлом</strong>. Важная особенность бинарного дерева в том, что порядок детей имеет значение: <strong>левый</strong> и <strong>правый потомок</strong> — это разные позиции, даже если они содержат одинаковые значения.</p>
<p><strong>Бинарное дерево удобно описывать рекурсивно:</strong> дерево либо пусто, либо состоит из корня, левого поддерева и правого поддерева, каждое из которых само является бинарным деревом. Это рекурсивное определение напрямую связано с тем, как такие структуры обрабатываются в алгоритмах.</p>
<p>Важно отличать бинарное дерево от других похожих структур:</p>
<ul>
<li><strong>Бинарное дерево</strong> — это общее понятие, не накладывающее ограничений на значения в узлах.</li>
<li><strong>Бинарное дерево поиска (BST)</strong> — это частный случай, где для каждого узла все значения в левом поддереве меньше (или равны), а в правом — больше (или равны) значения узла.</li>
<li>Также существуют <strong>полные</strong>, <strong>совершенные</strong>, <strong>сбалансированные</strong> бинарные деревья — это уже свойства формы, а не данных.</li>
</ul>
<p>С точки зрения формы бинарное дерево может быть очень разным. Оно может быть вырожденным, когда каждый узел имеет только одного потомка, и тогда структура по сути превращается в список. В таком случае высота дерева равна количеству узлов, и многие алгоритмы работают медленно. В противоположность этому, сбалансированное бинарное дерево имеет высоту порядка log n, что делает операции поиска и обхода эффективными.</p>
<p>Бинарные деревья широко используются потому, что они <strong>хорошо отражают иерархии</strong> и позволяют <strong>эффективно реализовывать алгоритмы</strong>. На их основе строятся <strong>деревья поиска</strong>, <strong>кучи</strong>, <strong>синтаксические деревья выражений</strong>, <strong>структуры для парсинга</strong>, <strong>индексы в базах данных</strong> и множество алгоритмических задач на собеседованиях.</p>
<p>Под деревом ниже подразумевается в первую очередь <strong>бинарное дерево</strong>, но большинство идей обобщаются и на <strong>n-арные деревья</strong>.</p>
<h3><strong>Depth-First Search (DFS) — обход в глубину</strong></h3>
<p>Это общий класс обходов, при котором алгоритм уходит как можно глубже по одной ветке, прежде чем возвращаться назад. Почти всегда реализуется рекурсивно или с помощью стека. Все классические <strong>«in/pre/post-order»</strong> — это частные случаи <strong>DFS</strong>. Временная сложность любого DFS — <code>O(n)</code>, память — <code>O(h)</code>, где h — высота дерева (в худшем случае <code>O(n)</code>).</p>
<p><strong>DFS</strong> часто спрашивают концептуально: чем отличается от BFS, где используется стек, где рекурсия, какие риски (переполнение стека).</p>
<h3><strong>Preorder Traversal (прямой обход)</strong></h3>
<p><strong>Порядок: </strong>сначала текущий узел → затем левое поддерево → затем правое поддерево.</p>
<p><strong>Идея:</strong> «обработать узел до его детей».<br />
Используется для сериализации дерева, копирования структуры, построения выражений в префиксной форме.</p>
<p><strong>Сложность: </strong>время O(n), память O(h).<br />
Реализуется рекурсивно или итеративно через стек.</p>
<p>На собеседованиях часто спрашивают: как восстановить дерево по preorder + inorder.</p>
<h3><strong>Inorder Traversal (симметричный обход)</strong></h3>
<p><strong>Порядок:</strong> левое поддерево → текущий узел → правое поддерево.</p>
<p><strong>Ключевое свойство:</strong> для бинарного дерева поиска (BST) inorder-обход выдаёт отсортированную последовательность.</p>
<p>Это один из самых любимых обходов интервьюеров, потому что он напрямую связан с BST, проверкой корректности дерева поиска, поиском k-го минимального элемента.</p>
<p><strong>Сложность стандартная:</strong> O(n) по времени, O(h) по памяти.</p>
<h3><strong>Postorder Traversal (обратный обход)</strong></h3>
<p>Порядок: левое поддерево → правое поддерево → текущий узел.</p>
<p>Идея: «обработать узел после детей».<br />
Используется для удаления дерева, освобождения памяти, вычисления выражений в постфиксной форме, задач динамического программирования на деревьях (когда результат узла зависит от детей).</p>
<p>Часто спрашивают как более «неочевидный» обход, особенно в итеративной реализации.</p>
<h3><strong>DFS с явным стеком (итеративный DFS)</strong></h3>
<p>Любой из трёх обходов выше можно реализовать без рекурсии, используя стек. Это важно для языков с ограниченным стеком вызовов.</p>
<p>На собеседованиях могут спросить:<br />
почему рекурсия = стек,<br />
как переписать рекурсивный обход в итеративный,<br />
чем они отличаются по памяти.</p>
<h3><strong>Morris Traversal</strong></h3>
<p>Это продвинутый вариант inorder (иногда preorder), который выполняется за O(1) дополнительной памяти.</p>
<p>Идея: временно «перепрошивать» указатели дерева, чтобы избежать стека и рекурсии, а потом восстанавливать структуру.</p>
<p>Время — O(n), память — O(1).<br />
Алгоритм сложный и редко используется на практике, но иногда встречается как «вопрос со звёздочкой» для сильных кандидатов.</p>
<h3><strong>Breadth-First Search (BFS) / Level Order Traversal — обход в ширину</strong></h3>
<p>Обход по уровням: сначала корень, затем все узлы уровня 1, затем уровня 2 и так далее.</p>
<p>Реализуется с помощью очереди, а не стека.<br />
Время — O(n), память — O(w), где w — максимальная ширина дерева (в худшем случае O(n)).</p>
<p>BFS часто спрашивают в задачах на уровни дерева:<br />
вывод по уровням,<br />
максимальная/минимальная глубина,<br />
зигзагообразный обход,<br />
правый или левый вид дерева.</p>
<h3><strong>Level Order с вариантами</strong></h3>
<p>Это не отдельные алгоритмы, а частые модификации BFS, которые почти гарантированно встречаются на собеседованиях:</p>
<ul>
<li>обход по уровням с группировкой значений</li>
<li>zigzag level order</li>
<li>обход с указанием уровня</li>
<li>поиск первого элемента, удовлетворяющего условию</li>
</ul>
<p>Важно понимать, что все они — один и тот же BFS с небольшой логикой поверх.</p>
<h3><strong>Обход n-арного дерева</strong></h3>
<p>Концептуально тот же <strong>DFS</strong> или <strong>BFS</strong>, но вместо левого и правого поддерева — список детей. На собеседованиях проверяют, понимаешь ли ты, что логика не меняется, меняется только цикл по детям.</p>
<h3><strong>Обход с возвратом значения (Tree DP)</strong></h3>
<p>Часто интервьюеры не называют это «обходом», но по сути это <strong>postorder DFS</strong>, где функция возвращает значение наверх: высоту, сумму, флаг, максимум и т.д.</p>
<p><strong>Примеры задач:</strong></p>
<ul>
<li>высота дерева</li>
<li>диаметр дерева</li>
<li>проверка сбалансированности</li>
<li>максимальный путь</li>
</ul>
<p>Это один из самых важных практических паттернов.</p>
<h2><strong>Алгоритмы поиска чисел</strong></h2>
<p>Под «поиском чисел» здесь понимается поиск элемента в массиве, диапазоне или числовом пространстве.</p>
<h3><strong>Линейный поиск (Linear Search)</strong></h3>
<p>Это самый простой и универсальный алгоритм поиска. Он последовательно проверяет элементы массива один за другим, пока не найдёт нужное значение или не дойдёт до конца.</p>
<p>Алгоритм не делает никаких предположений о структуре данных: массив может быть неотсортированным, произвольным, даже потоковым. Именно поэтому линейный поиск часто используется как базовый вариант или когда данные малы.</p>
<p>Временная сложность в худшем и среднем случае — O(n), в лучшем — O(1), если искомый элемент находится в начале. Память — O(1).</p>
<p>Пример: поиск числа в неотсортированном списке, поиск первого элемента, удовлетворяющего условию.</p>
<h3><strong>Бинарный поиск (Binary Search)</strong></h3>
<p>Бинарный поиск работает только с отсортированными данными. Алгоритм многократно делит диапазон поиска пополам: сравнивает искомое число с элементом в середине массива и отбрасывает половину, где элемента быть не может.</p>
<p>Ключевая идея — уменьшать пространство поиска в два раза на каждом шаге.</p>
<p>Временная сложность — O(log n) во всех случаях, память — O(1) в итеративной реализации или O(log n) при рекурсии.</p>
<p>Бинарный поиск — один из самых частых алгоритмов на собеседованиях. Его спрашивают не только напрямую, но и в виде вариаций: поиск первого/последнего вхождения, нижняя и верхняя границы, поиск по условию.</p>
<p>Пример: поиск числа в отсортированном массиве, поиск позиции вставки.</p>
<h3><strong>Поиск по ответу (Binary Search on Answer)</strong></h3>
<p>Это обобщение бинарного поиска, но применяется не к массиву, а к пространству возможных ответов. Используется, когда есть монотонное условие вида: «если ответ X подходит, то все ответы больше (или меньше) тоже подходят».</p>
<p>Алгоритм не ищет конкретное число в массиве, а подбирает минимальное или максимальное значение, удовлетворяющее условию.</p>
<p>Сложность — O(log R), где R — диапазон возможных значений, умноженная на стоимость проверки условия.</p>
<p>Очень популярный паттерн на собеседованиях.</p>
<p>Пример: найти минимальную скорость, за которую можно выполнить работу за заданное время; найти минимальный размер, при котором условие выполняется.</p>
<h3><strong>Интерполяционный поиск (Interpolation Search)</strong></h3>
<p>Интерполяционный поиск — улучшенная версия бинарного поиска для равномерно распределённых чисел. Вместо середины массива он вычисляет позицию элемента пропорционально значению искомого числа.</p>
<p>Если данные действительно равномерны, алгоритм работает очень быстро.</p>
<p>Средняя сложность — O(log log n), худшая — O(n). Память — O(1).</p>
<p>На практике используется редко, но иногда появляется в теоретических вопросах.</p>
<h3><strong>Jump Search</strong></h3>
<p>Алгоритм разбивает отсортированный массив на блоки фиксированного размера и «прыгает» по ним, пока не найдёт блок, в котором может находиться элемент. Затем выполняется линейный поиск внутри блока.</p>
<p>Оптимальный размер блока — √n, что даёт временную сложность O(√n).</p>
<p>Используется редко, но полезен как промежуточный вариант между линейным и бинарным поиском.</p>
<h3><strong>Exponential Search</strong></h3>
<p>Этот алгоритм применяется для поиска в очень больших или потенциально бесконечных отсортированных массивах. Сначала он экспоненциально увеличивает диапазон поиска (1, 2, 4, 8, …), пока не превысит искомое значение, а затем запускает бинарный поиск в найденном диапазоне.</p>
<p>Сложность — O(log n), память — O(1).</p>
<p>Пример: поиск в массиве неизвестного размера, потоках данных.</p>
<h3><strong>Поиск в хеш-таблице</strong></h3>
<p>Формально это не алгоритм поиска чисел в массиве, но на практике — один из самых частых способов поиска.</p>
<p>Идея: число преобразуется хеш-функцией в индекс, по которому элемент хранится в таблице.</p>
<p>Средняя сложность — O(1), худшая — O(n) при большом количестве коллизий. Память — O(n).</p>
<p>Пример: поиск числа в set или dict в Python.</p>
<h3><strong>Поиск в бинарном дереве поиска (BST)</strong></h3>
<p>Если числа хранятся в бинарном дереве поиска, то поиск идёт по тому же принципу, что и бинарный поиск: на каждом шаге выбирается левое или правое поддерево.</p>
<p>Средняя сложность — O(log n), но в худшем случае (несбалансированное дерево) — O(n). Память — O(h), где h — высота дерева.</p>
<p>Пример: поиск элемента в сбалансированном дереве (AVL, Red-Black Tree).</p>
<h3><strong>Линейный поиск с двумя указателями</strong></h3>
<p>Часто используется в задачах с отсортированными массивами. Вместо поиска одного числа ищется пара, сумма, разность или другое условие.</p>
<p>Алгоритм движется с двух концов массива навстречу друг другу.</p>
<p>Сложность — O(n), память — O(1).</p>
<p>Пример: найти два числа с заданной суммой.</p>
<h3><strong>Поиск с использованием битовых операций</strong></h3>
<p>Применяется в узких задачах: поиск уникального числа, поиск отсутствующего числа, поиск дубликата при ограничениях.</p>
<p>Алгоритмы используют свойства XOR, битовых масок и арифметики.</p>
<p>Сложность — O(n), память — O(1).</p>
<p>Пример: найти число, которое встречается один раз, когда остальные встречаются дважды.</p>
<h3><strong>Важное резюме для собеседований</strong></h3>
<p>Если данные не отсортированы — начинай с линейного поиска или хеш-таблицы.<br />
Если данные отсортированы — почти всегда подходит бинарный поиск или его вариации.<br />
Если ищешь минимальный/максимальный ответ по условию — это бинарный поиск по ответу.<br />
Если ограничения на память жёсткие — избегай хеш-таблиц.</p>
<p>Сообщение <a href="https://datatalks.ru/data-structures-and-algorithms/">Введение в Структуры данных (Data Structures) и алгоритмы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/data-structures-and-algorithms/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
