<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>alchemmist — Articles</title><link>https://alchemmist.xyz/ru/articles/</link><description>Последние записи в блоге alchemmist</description><generator>Hugo 0.163.3</generator><language>ru</language><atom:link href="https://alchemmist.xyz/ru/articles/index.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Thu, 12 Feb 2026 11:07:00 +0300</lastBuildDate><item><title>Тестирование на Python</title><link>https://alchemmist.xyz/ru/articles/testing-in-python/</link><pubDate>Thu, 12 Feb 2026 11:07:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/testing-in-python/</guid><description>Эта статья-интенсив по тестированию на Python подробно объясняет, зачем нужны автотесты, какие виды тестов существуют, и как строится пирамида тестирования. Вы познакомитесь с подходом TDD и циклом Red -&amp;gt; Green -&amp;gt; Refactor, узнаете, когда использовать мок-объекты, а когда нет, и получите практические навыки работы с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры кода, рекомендации по структуре тестов и советы по качественной архитектуре тестового покрытия помогут вам начать писать надёжный и поддерживаемый Python-код с тестами.</description><content:encoded><![CDATA[<p>Эта статья-интенсив по тестированию на Python подробно объясняет,
зачем нужны автотесты, какие виды тестов существуют, и как
строится пирамида тестирования. Вы познакомитесь с подходом TDD
и циклом Red -&gt; Green -&gt; Refactor, узнаете, когда использовать
мок-объекты, а когда нет, и получите практические навыки работы
с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры
кода, рекомендации по структуре тестов и советы по качественной
архитектуре тестового покрытия помогут вам начать писать надёжный
и поддерживаемый Python-код с тестами.</p>

<h2 id="правильное-отношение-к-тестам">
  <a class="link" href="#%d0%bf%d1%80%d0%b0%d0%b2%d0%b8%d0%bb%d1%8c%d0%bd%d0%be%d0%b5-%d0%be%d1%82%d0%bd%d0%be%d1%88%d0%b5%d0%bd%d0%b8%d0%b5-%d0%ba-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc">
    #
  </a>
  Правильное отношение к тестам
</h2>

<p>Вокруг тестов в Python, да и не только, много лишней драматургии.
Обычно есть два крайних мнения. Первое звучит как: &ldquo;без
стопроцентного покрытия вы не разработчик&rdquo;. Второе как: &ldquo;мы
стартап, нам не до тестов&rdquo;. Оба подхода, конечно, имеют право на
существование, но ведут, как правило не туда. В первом случае,
погоня за стопроцентным покрытим с высокой вероятностью может
привести к мусорным тестам, так как метрика покрытия (<em>как бы она
не вычисляллась</em>) не идеальна. Например, никто не запрещяет через
рефликисю получить все возможные объекты в коде, инстанцировать
их, и вообще ничего не проверять, получив стопроцентное покрытие.
Во втором случае, со временем процесс разработки будет становится
все сложнее, а DE (developer experience) ухудшаться из-за рочных
проверок &ldquo;на каждый чих&rdquo;.</p>
<p>Зрелый взгляд на тестирование куда проще. У нас есть система,
которая меняется. У этой системы есть цена ошибки. У этой же
системы есть стоимость проверки. Мы хотим снизить цену ошибки при
вменяемой стоимости проверки. Автотесты решают именно эту задачу.</p>
<p>Важно понимать, что тесты не делают код безошибочным. Они делают
ошибки быстрыми, локализуемыми и дешевыми в исправлении. Когда
ошибка ловится на ноутбуке разработчика через двадцать секунд после
изменения, это одна экономика. Когда ошибка ловится через два дня
в продакшене у реальных пользователей, это почти всегда дороже.</p>

<h2 id="зачем-автотесты-на-практике">
  <a class="link" href="#%d0%b7%d0%b0%d1%87%d0%b5%d0%bc-%d0%b0%d0%b2%d1%82%d0%be%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b0%d0%ba%d1%82%d0%b8%d0%ba%d0%b5">
    #
  </a>
  Зачем автотесты на практике
</h2>

<p>Чаще всего называют три причины. Защита от регрессий, документация
поведения и уверенность при рефакторинге. Это правильные слова,
но чтобы они не были абстрактными, нужно приземлить их на реальные
сценарии.</p>
<p>Представим сервис заказов. В него добавляют поддержку промокодов.
Разработчик изменяет логику расчета итоговой цены и случайно
ломает правило бесплатной доставки для VIP-клиентов. Если тестов
нет, баг уезжает в прод, поддержка получает жалобы, менеджер
поднимает инцидент, команда срочно откатывает релиз. Если тесты
есть, падает конкретный тест расчета доставки, и проблема
решается до релиза.</p>
<p>Документация поведения в тестах особенно заметна в долгоживущих
проектах. Через полгода после запуска фичи никто уже не помнит,
почему именно в случае отмененного заказа и частичного возврата
комиссия пересчитывается определенным образом. Тест с понятным
названием и явным assertion отвечает на этот вопрос быстрее, чем
любой коммит в истории.</p>
<p>Уверенность при рефакторинге вообще стоит считать отдельной
инженерной валютой. Код без рефакторинга стареет и тянет проект
вниз. Код с рефакторингом без тестов превращается в поле
экспериментов с высоким риском. Код с тестами позволяет улучшать
архитектуру постепенно и безопасно.</p>

<h2 id="что-именно-мы-тестируем-уровни-и-назначение">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b8%d0%bc%d0%b5%d0%bd%d0%bd%d0%be-%d0%bc%d1%8b-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc-%d1%83%d1%80%d0%be%d0%b2%d0%bd%d0%b8-%d0%b8-%d0%bd%d0%b0%d0%b7%d0%bd%d0%b0%d1%87%d0%b5%d0%bd%d0%b8%d0%b5">
    #
  </a>
  Что именно мы тестируем: уровни и назначение
</h2>

<!-- TODO: replace link to einglish wiki -->
<p>Слово &ldquo;тест&rdquo; без указания уровня мало что означает. Один тест
может проверять <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8">чистую
функцию</a>
за миллисекунды, другой гонять браузерный сценарий несколько
минут. И это нормально, если мы понимаем, зачем каждый из них
нужен.</p>
<p>Юнит-тест проверяет маленький фрагмент поведения в изоляции.
Обычно это функция или небольшой класс без обращения к сети,
диску и базе данных. Главная ценность юнита в скорости и точности
сигнала. Если он упал, область поиска проблемы очень узкая.</p>
<p>Интеграционный тест проверяет стык нескольких модулей или слоев.
Обычно здесь уже участвуют реальные адаптеры, БД, очередь,
внешний HTTP-клиент через тестовый стенд. Такой тест медленнее,
но он ловит ошибки контрактов и конфигураций, которые юнитами не
поймать.</p>
<p>E2E тест проверяет сквозной пользовательский путь. Это не тест
метода. Это тест бизнес-сценария целиком. Например,
&ldquo;регистрация -&gt; подтверждение почты -&gt; создание заказа -&gt;
оплата&rdquo;. Такие тесты самые дорогие в поддержке, но на даже
небольшое их количество, покрывающее критические участки системы,
дают большую увернность в надёжности.</p>
<p>Регрессионные тесты не отдельный технический уровень,
а назначение. Любой тест становится регрессионным, когда он
защищает уже согласованное поведение от повторной поломки.</p>

<h2 id="пирамида-тестирования-как-модель-затрат">
  <a class="link" href="#%d0%bf%d0%b8%d1%80%d0%b0%d0%bc%d0%b8%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d0%bd%d0%b8%d1%8f-%d0%ba%d0%b0%d0%ba-%d0%bc%d0%be%d0%b4%d0%b5%d0%bb%d1%8c-%d0%b7%d0%b0%d1%82%d1%80%d0%b0%d1%82">
    #
  </a>
  Пирамида тестирования как модель затрат
</h2>

<p>Пирамида тестирования это рабочая модель стоимости проверки.
Внизу много дешевых и быстрых проверок, выше меньше и дороже, на
вершине совсем немного самых дорогих проверок на уровне бизнеса.</p>
<p>Если команда пишет почти одни E2E, она быстро сталкивается
с медленным feedback loop. Каждый запуск долгий, окружение
хрупкое, тесты нестабильны. Разработчики начинают реже запускать
их локально, а CI превращается в место сюрпризов.</p>
<p>Если команда пишет только юниты, она теряет защиту на стыках.
Модули по отдельности могут быть безупречны, но в интеграции
сломаться из-за формата данных, конфигурации или миграций.</p>
<p>Здоровая стратегия почти всегда выглядит как широкий фундамент из
юнитов, умеренный слой интеграционных проверок и ограниченный, но
тщательно подобранный набор E2E на критический путь продукта. Но
в зависимости от специфики проекта, количество и соотношение всех
видов тестов может менятсья и это нормально.</p>

<h2 id="архитектура-и-тестируемость-связаны-напрямую">
  <a class="link" href="#%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc%d0%be%d1%81%d1%82%d1%8c-%d1%81%d0%b2%d1%8f%d0%b7%d0%b0%d0%bd%d1%8b-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e">
    #
  </a>
  Архитектура и тестируемость связаны напрямую
</h2>

<p>Тестируемость редко появляется случайно. Она почти всегда является
следствием архитектурных решений. Чем лучше разделены
ответственности в коде, тем проще писать тесты и тем дешевле их
поддерживать.</p>
<p>Плохой пример обычно выглядит как склеивание бизнес-логики,
инфраструктуры и побочных эффектов в одном методе.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">PostgresClient</span><span class="p">(</span><span class="s2">&#34;postgres://prod&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span> <span class="o">=</span> <span class="n">StripeClient</span><span class="p">(</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span> <span class="o">=</span> <span class="n">MetricsClient</span><span class="p">(</span><span class="s2">&#34;statsd://metrics&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">fetch_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span><span class="o">.</span><span class="n">save_payment</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Здесь тесту сложно выбрать уровень. Для юнит-теста слишком много
внешнего мира. Для интеграционного слишком много разных
ответственностей сразу.</p>
<p>Лучший пример отделяет бизнес-решение от инфраструктуры и
внедряет зависимости извне.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">users_repo</span><span class="p">,</span> <span class="n">payments_repo</span><span class="p">,</span> <span class="n">gateway</span><span class="p">,</span> <span class="n">metrics</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span> <span class="o">=</span> <span class="n">users_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span> <span class="o">=</span> <span class="n">payments_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span> <span class="o">=</span> <span class="n">gateway</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span> <span class="o">=</span> <span class="n">metrics</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь юнит-тест может проверять решения домена через фейковые
зависимости, а интеграционные тесты отдельно проверят репозиторий,
шлюз и метрики. Такой код проще развивать и проще покрывать.</p>

<h2 id="red---green---refactor">
  <a class="link" href="#red---green---refactor">
    #
  </a>
  Red -&gt; Green -&gt; Refactor
</h2>

<p>Red означает, что мы сначала формулируем ожидаемое поведение через
падающий тест. Green означает, что мы пишем минимальный код,
достаточный для прохождения. Refactor означает, что мы улучшаем
структуру кода, не меняя поведение.</p>
<p>Рассмотрим короткий пример. Пусть нужна функция,
которая разбивает сумму по месяцам и последнюю копейку отдает
в последний месяц.</p>
<p>На фазе Red можно начать так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_even_months_with_remainder</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 1000 cents over 3 months -&gt; 333, 333, 334</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">333</span><span class="p">,</span> <span class="mi">333</span><span class="p">,</span> <span class="mi">334</span><span class="p">]</span>
</span></span></code></pre></div><p>На фазе Green пишем минимальную реализацию:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span>
</span></span></code></pre></div><p>На фазе Refactor добавляем защиту от некорректного ввода,
делаем имя переменных яснее, добавляем тесты на крайние случаи.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">months</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;months must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">total</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;total must be non-negative&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">schedule</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_one_month</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">500</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_zero_total</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_invalid_months</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p>Именно так TDD удерживает фокус на контракте и предотвращает
переусложнение раньше времени.</p>

<h2 id="скорость-обратной-связи">
  <a class="link" href="#%d1%81%d0%ba%d0%be%d1%80%d0%be%d1%81%d1%82%d1%8c-%d0%be%d0%b1%d1%80%d0%b0%d1%82%d0%bd%d0%be%d0%b9-%d1%81%d0%b2%d1%8f%d0%b7%d0%b8">
    #
  </a>
  Скорость обратной связи
</h2>

<p>Команда ускоряется не тогда, когда быстрее печатает код.
Команда ускоряется тогда, когда быстрее получает надежный ответ
на вопрос &ldquo;изменение корректно или нет&rdquo;.</p>
<p>Маленькие изолированные тесты дают сигнал за секунды.
Тяжелые интеграционные проверки дают сигнал за минуты.
E2E иногда дают сигнал за десятки минут. Все уровни нужны,
но последовательность их использования критична.</p>
<p>В рабочем режиме разработчик обычно запускает локально быстрый
пакет, потом нужные интеграционные тесты по задаче, и уже после
этого отдает изменения в CI, где гоняется полный набор.</p>
<p>Когда эта дисциплина есть, количество &ldquo;красных&rdquo; сюрпризов в CI
существенно падает. А значит, падает и время ожидания,
и количество контекстных переключений.</p>

<h2 id="london-school-и-classicist-school-в-реальном-проекте">
  <a class="link" href="#london-school-%d0%b8-classicist-school-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  London school и classicist school в реальном проекте
</h2>

<p>Спор между школами тестирования существует давно и часто подается
как выбор одного лагеря. На практике полезнее понимать, в чем
сила каждой школы и где ее границы.</p>
<p>London school чаще использует моки и проверяет взаимодействия.
Если ваш код это оркестратор нескольких внешних вызовов,
этот подход дает очень точный контроль над тем, кто и как был
вызван.</p>
<p>Classicist school чаще проверяет состояние и поведение через
реальные объекты без агрессивного мокинга. Если у вас много
чистой доменной логики, такой стиль обычно делает тесты более
устойчивыми к рефакторингу.</p>
<p>Давайте посмотрим на один и тот же кейс двумя стилями.</p>
<p>В mock-heavy подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_id</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="p">(</span><span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_calls_dependencies</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="mi">101</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span><span class="o">=</span><span class="mi">101</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></div><p>В state-oriented подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryRepo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">user_id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryEmailSender</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">send_welcome</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_changes_state</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">InMemoryRepo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">sender</span> <span class="o">=</span> <span class="n">InMemoryEmailSender</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">sender</span><span class="o">.</span><span class="n">sent</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span></code></pre></div><p>Оба теста полезны. Первый лучше контролирует контракт вызовов.
Второй лучше переживает рефакторинг внутренней реализации.</p>

<h2 id="моки-и-стабы-когда-помогают-а-когда-ломают-картину">
  <a class="link" href="#%d0%bc%d0%be%d0%ba%d0%b8-%d0%b8-%d1%81%d1%82%d0%b0%d0%b1%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bf%d0%be%d0%bc%d0%be%d0%b3%d0%b0%d1%8e%d1%82-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bb%d0%be%d0%bc%d0%b0%d1%8e%d1%82-%d0%ba%d0%b0%d1%80%d1%82%d0%b8%d0%bd%d1%83">
    #
  </a>
  Моки и стабы: когда помогают, а когда ломают картину
</h2>

<p>Мок нужен там, где без него тест становится нестабильным,
дорогим или слишком широким. Хороший признак полезного мокинга
это изоляция внешнего мира, а не изоляция собственной логики.</p>
<p>Разберем пример с внешним API курса валют. В production-коде мы
делаем HTTP-запрос, но в юнит-тесте хотим проверить реакцию на
ответы API без реальной сети.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_rate</span><span class="p">(</span><span class="n">base</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">quote</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://rates.example.com/latest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;base&#34;</span><span class="p">:</span> <span class="n">base</span><span class="p">,</span> <span class="s2">&#34;quote&#34;</span><span class="p">:</span> <span class="n">quote</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">timeout</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">resp</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;rate&#34;</span><span class="p">])</span>
</span></span></code></pre></div><p>Юнит-тест с mock:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span><span class="p">,</span> <span class="n">patch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_fetch_rate_parses_response</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span><span class="o">.</span><span class="n">json</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;rate&#34;</span><span class="p">:</span> <span class="s2">&#34;95.4&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="p">(</span><span class="s2">&#34;requests.get&#34;</span><span class="p">,</span> <span class="n">return_value</span><span class="o">=</span><span class="n">fake_response</span><span class="p">)</span> <span class="k">as</span> <span class="n">get_mock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">rate</span> <span class="o">=</span> <span class="n">fetch_rate</span><span class="p">(</span><span class="s2">&#34;USD&#34;</span><span class="p">,</span> <span class="s2">&#34;RUB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rate</span> <span class="o">==</span> <span class="mf">95.4</span>
</span></span><span class="line"><span class="cl">    <span class="n">get_mock</span><span class="o">.</span><span class="n">assert_called_once</span><span class="p">()</span>
</span></span></code></pre></div><p>Тут мок оправдан. Мы не тестируем <code>requests</code>, мы тестируем,
как наша функция обрабатывает ответ.</p>
<p>Но если мы начнем мокать каждый метод собственного репозитория,
каждую внутреннюю функцию и каждый <code>if</code>, тесты станут хрупкими.
Они будут падать от технического рефакторинга, даже если
поведение не изменилось.</p>

<h2 id="unittest-pytest-nose2-что-выбирать-в-2026-году">
  <a class="link" href="#unittest-pytest-nose2-%d1%87%d1%82%d0%be-%d0%b2%d1%8b%d0%b1%d0%b8%d1%80%d0%b0%d1%82%d1%8c-%d0%b2-2026-%d0%b3%d0%be%d0%b4%d1%83">
    #
  </a>
  unittest, pytest, nose2: что выбирать в 2026 году
</h2>

<p>Встроенный <code>unittest</code> остается рабочим вариантом, особенно там,
где важна минимизация зависимостей и строгая стандартизация.</p>
<p><code>pytest</code> в повседневной разработке обычно выигрывает за счет
читаемости, параметризации, фикстур и удобной экосистемы.
Для большинства новых Python-проектов это практический выбор по
умолчанию.</p>
<p><code>nose</code> стоит рассматривать как исторический этап экосистемы.
<code>nose2</code> жив, но в новых кодовых базах он встречается редко,
потому что <code>pytest</code> решает типичные задачи проще и с большей
поддержкой сообщества.</p>

<h2 id="aaa-и-структура-теста-которую-удобно-читать-через-год">
  <a class="link" href="#aaa-%d0%b8-%d1%81%d1%82%d1%80%d1%83%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%83%d1%8e-%d1%83%d0%b4%d0%be%d0%b1%d0%bd%d0%be-%d1%87%d0%b8%d1%82%d0%b0%d1%82%d1%8c-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%b3%d0%be%d0%b4">
    #
  </a>
  AAA и структура теста, которую удобно читать через год
</h2>

<p>Одна из самых простых техник, которая сильно повышает
поддерживаемость тестов, это явная структура Arrange, Act,
Assert. Даже без комментариев она удерживает тест коротким
и линейным.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">quantity</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">discount</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">subtotal</span> <span class="o">=</span> <span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">subtotal</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">discount</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_total_with_discount</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">price</span> <span class="o">=</span> <span class="mi">500</span>
</span></span><span class="line"><span class="cl">    <span class="n">quantity</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">discount</span> <span class="o">=</span> <span class="mf">0.1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">discount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">result</span> <span class="o">==</span> <span class="mi">900</span>
</span></span></code></pre></div><p>Здесь тест легко сканируется глазами. Подготовка данных,
действие, проверка. Ничего лишнего.</p>
<p>Если тест содержит слишком много действий и проверок, это часто
сигнал, что он пытается проверить слишком много разных сценариев
сразу. Лучше разделить такой тест на несколько маленьких.</p>

<h2 id="фикстуры-setupteardown-и-контроль-над-состоянием">
  <a class="link" href="#%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-setupteardown-%d0%b8-%d0%ba%d0%be%d0%bd%d1%82%d1%80%d0%be%d0%bb%d1%8c-%d0%bd%d0%b0%d0%b4-%d1%81%d0%be%d1%81%d1%82%d0%be%d1%8f%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  Фикстуры, setup/teardown и контроль над состоянием
</h2>

<p>Фикстуры в <code>pytest</code> решают ключевую проблему дублирования
подготовки данных и окружения. Но их легко превратить в
непрозрачную магию, если прятать внутри слишком много логики.</p>
<p>Сравним плохой и хороший подход.</p>
<p>Плохо, когда фикстура делает слишком много:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">app_everything</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># создает БД, поднимает сервис, загружает данные,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># настраивает очередь, запускает фоновые воркеры</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;warning&#34;</span><span class="p">:</span> <span class="s2">&#34;fixture is too broad&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>Хорошо, когда фикстуры узкие и ясные по назначению:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">user_email</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;alice@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">in_memory_repo</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">Repo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Repo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_repo_save</span><span class="p">(</span><span class="n">in_memory_repo</span><span class="p">,</span> <span class="n">user_email</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">]</span>
</span></span></code></pre></div><p>Чем яснее фикстуры, тем проще понимать, откуда в тесте взялись
данные и почему они именно такие.</p>

<h2 id="рекомендации-по-юнит-тестам-через-конкретные-примеры">
  <a class="link" href="#%d1%80%d0%b5%d0%ba%d0%be%d0%bc%d0%b5%d0%bd%d0%b4%d0%b0%d1%86%d0%b8%d0%b8-%d0%bf%d0%be-%d1%8e%d0%bd%d0%b8%d1%82-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%ba%d0%be%d0%bd%d0%ba%d1%80%d0%b5%d1%82%d0%bd%d1%8b%d0%b5-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d1%8b">
    #
  </a>
  Рекомендации по юнит-тестам через конкретные примеры
</h2>

<p>Сильный юнит-тест обычно проверяет один смысловой сценарий.
Рассмотрим функцию нормализации телефона.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize_phone</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">digits</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\D&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">==</span> <span class="mi">11</span> <span class="ow">and</span> <span class="n">digits</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;8&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">digits</span> <span class="o">=</span> <span class="s2">&#34;7&#34;</span> <span class="o">+</span> <span class="n">digits</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">11</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;invalid phone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;+&#34;</span> <span class="o">+</span> <span class="n">digits</span>
</span></span></code></pre></div><p>Плохой тест на такую функцию часто пытается проверить сразу
пять форматов и две ошибки в одном кейсе. Хороший набор тестов
разделяет сценарии.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_ru_local_prefix</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;8 (999) 000-00-00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_already_international</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;+7 999 000 00 00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_invalid_length</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;123&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь при падении точно видно, какой именно сценарий сломан.</p>

<h2 id="как-организовать-test-suite-проекта-чтобы-им-реально-пользовались">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%be%d1%80%d0%b3%d0%b0%d0%bd%d0%b8%d0%b7%d0%be%d0%b2%d0%b0%d1%82%d1%8c-test-suite-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b0-%d1%87%d1%82%d0%be%d0%b1%d1%8b-%d0%b8%d0%bc-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be-%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d0%bb%d0%b8%d1%81%d1%8c">
    #
  </a>
  Как организовать test suite проекта, чтобы им реально пользовались
</h2>

<p>Рабочий test suite начинается с понятной структуры каталогов и
режимов запуска. Когда уровни тестов разделены, разработчику проще
выбирать нужный пакет под конкретную задачу.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">project/
</span></span><span class="line"><span class="cl">  app/
</span></span><span class="line"><span class="cl">    domain/
</span></span><span class="line"><span class="cl">    application/
</span></span><span class="line"><span class="cl">    infrastructure/
</span></span><span class="line"><span class="cl">  tests/
</span></span><span class="line"><span class="cl">    unit/
</span></span><span class="line"><span class="cl">    integration/
</span></span><span class="line"><span class="cl">    e2e/
</span></span><span class="line"><span class="cl">  pyproject.toml
</span></span></code></pre></div><p>В <code>pyproject.toml</code> удобно зафиксировать маркеры и общие настройки
<code>pytest</code>, чтобы команда запускала тесты единообразно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tool</span><span class="p">.</span><span class="nx">pytest</span><span class="p">.</span><span class="nx">ini_options</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">addopts</span> <span class="p">=</span> <span class="s2">&#34;-ra -q&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">testpaths</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;tests&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">markers</span> <span class="p">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;integration: tests with real infrastructure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;e2e: end-to-end scenarios&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>Тогда локально можно запускать быстрые тесты отдельно от тяжелых.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pytest tests/unit
</span></span><span class="line"><span class="cl">pytest -m integration
</span></span><span class="line"><span class="cl">pytest -m e2e
</span></span></code></pre></div><p>Ключевой момент в том, что suite должен помогать разработчику,
а не наказывать его долгими непредсказуемыми прогонами.</p>

<h2 id="интеграционные-тесты-на-примере-api-и-базы-данных">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%be%d0%bd%d0%bd%d1%8b%d0%b5-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-api-%d0%b8-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85">
    #
  </a>
  Интеграционные тесты на примере API и базы данных
</h2>

<p>Разберем практический пример с FastAPI и SQLite,
где мы хотим проверить полный путь &ldquo;HTTP-запрос -&gt; запись в БД -&gt;
HTTP-ответ&rdquo;.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ItemIn</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">&#34;test.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_item</span><span class="p">(</span><span class="n">payload</span><span class="p">:</span> <span class="n">ItemIn</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">get_conn</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;INSERT INTO items(name) VALUES (?)&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">payload</span><span class="o">.</span><span class="n">name</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span></code></pre></div><p>Интеграционный тест:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi.testclient</span> <span class="kn">import</span> <span class="n">TestClient</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_create_item_persists_to_db</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">,</span> <span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">db_path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;items.db&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get_test_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setattr</span><span class="p">(</span><span class="s2">&#34;main.get_conn&#34;</span><span class="p">,</span> <span class="n">get_test_conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;book&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;SELECT name FROM items&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rows</span> <span class="o">==</span> <span class="p">[(</span><span class="s2">&#34;book&#34;</span><span class="p">,)]</span>
</span></span></code></pre></div><p>Этот тест уже не юнит. Он проверяет интеграцию HTTP-слоя,
валидации и хранилища.</p>

<h2 id="e2e-на-примере-playwright">
  <a class="link" href="#e2e-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-playwright">
    #
  </a>
  E2E на примере Playwright
</h2>

<p>E2E полезно показывать на коротких, но бизнес-значимых сценариях.
Например, логин пользователя.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">playwright.sync_api</span> <span class="kn">import</span> <span class="n">sync_playwright</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_login_e2e</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">sync_playwright</span><span class="p">()</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">browser</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">chromium</span><span class="o">.</span><span class="n">launch</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">browser</span><span class="o">.</span><span class="n">new_page</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">goto</span><span class="p">(</span><span class="s2">&#34;http://localhost:8000/login&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;email&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;password&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;qwerty&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">click</span><span class="p">(</span><span class="s2">&#34;button[type=&#39;submit&#39;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">wait_for_url</span><span class="p">(</span><span class="s2">&#34;**/dashboard&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">page</span><span class="o">.</span><span class="n">locator</span><span class="p">(</span><span class="s2">&#34;text=Welcome&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">is_visible</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">browser</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>В этом тесте мы не проверяем внутреннюю реализацию логина.
Мы проверяем, что пользователь действительно может пройти
критический путь.</p>

<h2 id="база-данных-в-тестах-миграции-фикстуры-rollback">
  <a class="link" href="#%d0%b1%d0%b0%d0%b7%d0%b0-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%bc%d0%b8%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%b8-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-rollback">
    #
  </a>
  База данных в тестах: миграции, фикстуры, rollback
</h2>

<p>С БД в тестах есть одно правило, которое нельзя нарушать.
Каждый тест должен быть независим по состоянию.</p>
<p>Один из рабочих шаблонов для SQLAlchemy это транзакция на тест,
которая откатывается после выполнения.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">db_session</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="s2">&#34;sqlite:///:memory:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">Session</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span> <span class="o">=</span> <span class="n">Session</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">tx</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">begin</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">session</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tx</span><span class="o">.</span><span class="n">rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>Такой подход дает чистое состояние на каждый тест и сохраняет
высокую скорость прогонов.</p>
<p>Если в проекте используются миграции, их нужно включать в процесс
подготовки тестовой БД, иначе тесты начнут жить в схеме,
которая отличается от production.</p>

<h2 id="приватные-методы-когда-тестировать-напрямую-а-когда-нет">
  <a class="link" href="#%d0%bf%d1%80%d0%b8%d0%b2%d0%b0%d1%82%d0%bd%d1%8b%d0%b5-%d0%bc%d0%b5%d1%82%d0%be%d0%b4%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d1%82%d1%8c-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bd%d0%b5%d1%82">
    #
  </a>
  Приватные методы: когда тестировать напрямую, а когда нет
</h2>

<p>По умолчанию лучше тестировать поведение через публичный API.
Это делает тесты устойчивее к рефакторингу.</p>
<p>Рассмотрим пример.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TaxCalculator</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">calculate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">amount</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tax</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_tax</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">amount</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span></code></pre></div><p>Стабильный тест проверяет <code>calculate</code>, а не <code>_tax</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_adds_tax</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">calc</span> <span class="o">=</span> <span class="n">TaxCalculator</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">calc</span><span class="o">.</span><span class="n">calculate</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="o">==</span> <span class="mi">120</span>
</span></span></code></pre></div><p>Если завтра <code>_tax</code> будет заменен на сложную стратегию
с льготами, тест публичного поведения останется валидным,
если контракт не изменился.</p>
<p>Прямое тестирование приватного метода может быть временно
оправдано в legacy-коде, но как постоянная практика оно
обычно повышает хрупкость тестов.</p>

<h2 id="время-в-тестах-и-детерминизм">
  <a class="link" href="#%d0%b2%d1%80%d0%b5%d0%bc%d1%8f-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%b8-%d0%b4%d0%b5%d1%82%d0%b5%d1%80%d0%bc%d0%b8%d0%bd%d0%b8%d0%b7%d0%bc">
    #
  </a>
  Время в тестах и детерминизм
</h2>

<p>Время это одна из самых частых причин flaky-тестов.
Если тест зависит от <code>datetime.now()</code>, он может вести себя
по-разному в разные дни, часы и timezone.</p>
<p><code>freezegun</code> решает это элегантно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">freezegun</span> <span class="kn">import</span> <span class="n">freeze_time</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">current_period_start</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">now</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">day</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">hour</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">minute</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">second</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">microsecond</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@freeze_time</span><span class="p">(</span><span class="s2">&#34;2026-02-12 10:00:00&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_current_period_start</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">current_period_start</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;2026-02-01T00:00:00&#34;</span>
</span></span></code></pre></div><p>Такой тест детерминирован и не зависит от машины запуска.</p>

<h2 id="плотный-блок-про-pytest-от-базы-до-полезных-встроенных-фикстур">
  <a class="link" href="#%d0%bf%d0%bb%d0%be%d1%82%d0%bd%d1%8b%d0%b9-%d0%b1%d0%bb%d0%be%d0%ba-%d0%bf%d1%80%d0%be-pytest-%d0%be%d1%82-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%be-%d0%bf%d0%be%d0%bb%d0%b5%d0%b7%d0%bd%d1%8b%d1%85-%d0%b2%d1%81%d1%82%d1%80%d0%be%d0%b5%d0%bd%d0%bd%d1%8b%d1%85-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80">
    #
  </a>
  Плотный блок про pytest: от базы до полезных встроенных фикстур
</h2>

<p>Начнем с базового примера с <code>assert</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">*</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_basic</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="mi">12</span>
</span></span></code></pre></div><p>Параметризация позволяет расширять покрытие без копипаста.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;a,b,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="o">-</span><span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_parametrized</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">==</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Проверка исключений:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">divide</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">b</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ZeroDivisionError</span><span class="p">(</span><span class="s2">&#34;division by zero&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">/</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_divide_zero_raises</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ZeroDivisionError</span><span class="p">,</span> <span class="k">match</span><span class="o">=</span><span class="s2">&#34;division by zero&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">divide</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p><code>tmp_path</code> полезен для файловых сценариев.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">save_report</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_save_report</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">report</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;report.txt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">save_report</span><span class="p">(</span><span class="n">report</span><span class="p">,</span> <span class="s2">&#34;ok&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;ok&#34;</span>
</span></span></code></pre></div><p><code>monkeypatch</code> помогает временно менять окружение и атрибуты.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">service_url</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://prod.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_service_url_from_env</span><span class="p">(</span><span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://test.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">service_url</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;https://test.example.com&#34;</span>
</span></span></code></pre></div><p>В реальных проектах такие встроенные инструменты снимают много
болезненного ручного кода в тестах.</p>

<h2 id="интеграция-типизации-и-тестов-две-сетки-безопасности-лучше-одной">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d1%8f-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%be%d0%b2-%d0%b4%d0%b2%d0%b5-%d1%81%d0%b5%d1%82%d0%ba%d0%b8-%d0%b1%d0%b5%d0%b7%d0%be%d0%bf%d0%b0%d1%81%d0%bd%d0%be%d1%81%d1%82%d0%b8-%d0%bb%d1%83%d1%87%d1%88%d0%b5-%d0%be%d0%b4%d0%bd%d0%be%d0%b9">
    #
  </a>
  Интеграция типизации и тестов: две сетки безопасности лучше одной
</h2>

<p>Тесты и статический анализ закрывают разные области риска.
Type checker ловит типовые несоответствия до запуска.
Тесты ловят поведенческие и интеграционные ошибки при выполнении.</p>
<p>Например, type checker может заранее подсветить ситуацию,
когда функция обещает вернуть <code>int</code>, а фактически возвращает
<code>float</code> или <code>None</code>. Тесты в этот момент еще даже не запускались.</p>
<p>В CI зрелого проекта обычно запускают и type checker,
и pytest. Это дает плотный и быстрый контур качества.
Сначала отсекаются структурные ошибки, затем проверяется
поведение.</p>

<h2 id="cicd-как-сделать-тесты-частью-процесса-а-не-пожеланием">
  <a class="link" href="#cicd-%d0%ba%d0%b0%d0%ba-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%87%d0%b0%d1%81%d1%82%d1%8c%d1%8e-%d0%bf%d1%80%d0%be%d1%86%d0%b5%d1%81%d1%81%d0%b0-%d0%b0-%d0%bd%d0%b5-%d0%bf%d0%be%d0%b6%d0%b5%d0%bb%d0%b0%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  CI/CD: как сделать тесты частью процесса, а не пожеланием
</h2>

<p>Если тесты живут только локально, их ценность ограничена.
Кто-то запускает их, кто-то нет, а качество становится вопросом
дисциплины отдельных людей. CI снимает этот риск, потому что
проверки становятся обязательным этапом потока.</p>
<p>Для GitHub Actions минимальный рабочий пайплайн может выглядеть так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tests</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-python@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.12&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Для GitLab CI логика та же:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Ключевая инженерная мысль здесь простая. Неважно, какая платформа
CI выбрана. Важно, что правила качества автоматизированы и
единообразны для всех.</p>

<h2 id="частые-анти-паттерны-из-за-которых-тесты-теряют-ценность">
  <a class="link" href="#%d1%87%d0%b0%d1%81%d1%82%d1%8b%d0%b5-%d0%b0%d0%bd%d1%82%d0%b8-%d0%bf%d0%b0%d1%82%d1%82%d0%b5%d1%80%d0%bd%d1%8b-%d0%b8%d0%b7-%d0%b7%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%82%d0%b5%d1%80%d1%8f%d1%8e%d1%82-%d1%86%d0%b5%d0%bd%d0%bd%d0%be%d1%81%d1%82%d1%8c">
    #
  </a>
  Частые анти-паттерны, из-за которых тесты теряют ценность
</h2>

<p>Один из самых неприятных анти-паттернов это тестирование внутренней
реализации вместо внешнего поведения. Такой тест привязывается
к тому, как код написан сейчас, и ломается при любом рефакторинге,
даже если контракт не менялся.</p>
<p>Второй анти-паттерн это смешивание уровней в одном тесте.
Тест называется юнитом, но внутри идет в сеть, использует
реальную БД, зависит от текущего времени и случайности.
Результат предсказуем: долгий и нестабильный прогон.</p>
<p>Третий анти-паттерн это flaky-тесты, которые иногда красные,
иногда зеленые без изменений в коде. Как только команда привыкает
к мысли &ldquo;этот тест иногда сам по себе падает&rdquo;, доверие к тестовому
пакету рушится.</p>
<p>Четвертый анти-паттерн это попытка покрыть абсолютно все подряд,
включая чужие библиотеки и тривиальные прокси-методы без
бизнес-смысла. Покрытие растет, но полезный сигнал не растет.</p>

<h2 id="реальный-сценарий-red---green---refactor-на-короткой-функции">
  <a class="link" href="#%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d1%8b%d0%b9-%d1%81%d1%86%d0%b5%d0%bd%d0%b0%d1%80%d0%b8%d0%b9-red---green---refactor-%d0%bd%d0%b0-%d0%ba%d0%be%d1%80%d0%be%d1%82%d0%ba%d0%be%d0%b9-%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d0%b8">
    #
  </a>
  Реальный сценарий Red -&gt; Green -&gt; Refactor на короткой функции
</h2>

<p>Сделаем еще один компактный TDD-сценарий, на этот раз про валидацию
пароля. Требование такое: пароль должен быть длиной не меньше
восьми символов, содержать хотя бы одну цифру и хотя бы одну
букву.</p>
<p>Сначала Red:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_ok</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_too_short</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;a1&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">False</span>
</span></span></code></pre></div><p>Green, минимальная реализация:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_digit</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isdigit</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_alpha</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isalpha</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">has_digit</span> <span class="ow">and</span> <span class="n">has_alpha</span>
</span></span></code></pre></div><p>Refactor и расширение тестов:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;value,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">,</span> <span class="kc">True</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abcdefgh&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;12345678&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;ab12&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_parametrized</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="ow">is</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Это простой пример, но он хорошо показывает идею.
Тесты сначала формулируют контракт, потом поддерживают
безопасные изменения.</p>

<h2 id="что-делать-уже-завтра-в-реальном-проекте">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%83%d0%b6%d0%b5-%d0%b7%d0%b0%d0%b2%d1%82%d1%80%d0%b0-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  Что делать уже завтра в реальном проекте
</h2>

<p>Если в проекте тестов мало или они хаотичны, не нужно пытаться
переписать все за одну итерацию. Практичнее начать с маленького,
но системного шага. Выделить критический бизнес-путь и закрыть его
базовым набором проверок на трех уровнях: быстрые юниты,
несколько интеграционных тестов на ключевые контракты и один-два
E2E сценария на пользовательскую ценность.</p>
<p>Параллельно стоит зафиксировать минимальные правила качества в CI.
Запуск юнитов, запуск интеграционных тестов, запуск type checker,
порог покрытия, блокировка merge при красном пайплайне. Это уже
создает каркас процесса, который можно постепенно усиливать.</p>
<p>По мере роста кода нужно следить не только за числом тестов,
но и за их полезностью. Каждый тест должен отвечать на вопрос,
какой риск он закрывает. Когда тесты начинают жить своей жизнью
и проверять не риск, а случайные детали, их ценность быстро
снижается.</p>

<h2 id="вместо-финала">
  <a class="link" href="#%d0%b2%d0%bc%d0%b5%d1%81%d1%82%d0%be-%d1%84%d0%b8%d0%bd%d0%b0%d0%bb%d0%b0">
    #
  </a>
  Вместо финала
</h2>

<p>Тестирование в Python не является бюрократией, если к нему
подходить как к инженерному инструменту, а не как к формальному
требованию. Тесты помогают быстрее двигаться, потому что дают
предсказуемый сигнал, удерживают систему в рабочем состоянии
и позволяют безопасно менять код, который уже приносит бизнесу
ценность.</p>
<p>Команда, которая умеет писать понятные юниты, осмысленные
интеграционные тесты и ограниченный набор E2E на критический путь,
обычно выпускает изменения чаще и спокойнее. Не потому, что у нее
&ldquo;больше тестов&rdquo;, а потому что у нее лучше управляется риск.</p>
<p>И в этом главный смысл всей темы. Тесты нужны не ради тестов.
Они нужны ради управляемой разработки.</p>
]]></content:encoded><category>python</category><category>testing</category><category>eosp</category></item><item><title>Типизация в Python</title><link>https://alchemmist.xyz/ru/articles/typing-python/</link><pubDate>Wed, 11 Feb 2026 16:02:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/typing-python/</guid><description>В этой статье рассматриваются два ключевых вопроса. Зачем использовать типизацию в языке Python, который позволяет писать без неё? И как писать типизированный код на Python правильно? Постараемся быстро познакомиться со всеми необходимыми инструментами, чтобы после прочтения вы уже могли начать осознанно писать свои программы типизированными, ведь это совсем не сложно!
# Динамика vs Статика Итак, что же такое типизация? Новым это понятие может оказаться только для питонистов, ведь большинство классических языков таких как C, Java, Rust и многие другие исходно были созданы, как языки со статической типизацией. Но что это означает? Давайте рассмотрим небольшой пример на C:</description><content:encoded><![CDATA[<p>В этой статье рассматриваются два ключевых вопроса. Зачем
использовать типизацию в языке Python, который позволяет писать
без неё? И как писать типизированный код на Python правильно?
Постараемся быстро познакомиться со всеми необходимыми
инструментами, чтобы после прочтения вы уже могли начать
осознанно писать свои программы типизированными, ведь это совсем
не сложно!</p>

<h2 id="динамика-vs-статика">
  <a class="link" href="#%d0%b4%d0%b8%d0%bd%d0%b0%d0%bc%d0%b8%d0%ba%d0%b0-vs-%d1%81%d1%82%d0%b0%d1%82%d0%b8%d0%ba%d0%b0">
    #
  </a>
  Динамика vs Статика
</h2>

<p>Итак, что же такое типизация? Новым это понятие может оказаться
только для питонистов, ведь большинство классических языков таких
как C, Java, Rust и многие другие исходно были созданы, как языки
со статической типизацией. Но что это означает? Давайте рассмотрим
небольшой пример на C:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">sum</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">sum</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// printf(&#34;%d\n&#34;, sum(&#34;10&#34;, 20));
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Такой код работает и выводит число <code>30</code>. Но обратите внимание,
что последняя строка закомментирована. Если мы раскомментируем
её и опять попробуем скомпилировать программу, то получим
примерно вот такой лог ошибки:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error: passing argument 1 of ‘sum’ makes integer from pointer without cast
</span></span><span class="line"><span class="cl">   10 |     printf(&#34;%d\n&#34;, sum(&#34;10&#34;, 20));
</span></span><span class="line"><span class="cl">      |                        ^~~~
</span></span><span class="line"><span class="cl">      |                        |
</span></span><span class="line"><span class="cl">      |                        char *
</span></span><span class="line"><span class="cl">note: expected ‘int’ but argument is of type ‘char *’
</span></span></code></pre></div><p>Лог сообщает нам, что параметр функции, ожидая <code>int</code>, получил
аргумент типа <code>char *</code> (<em>для упрощения можем считать это
эквивалентом строки</em>). На первый взгляд ничего удивительного, для
нас с вами — питонистов, в этом нет. Ведь вот такой код на Python
тоже упал бы с ошибкой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sum</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;10&#34;</span><span class="p">,</span> <span class="mi">20</span><span class="p">))</span> <span class="c1"># TypeError</span>
</span></span></code></pre></div><p>В чем же разница, спросите вы? Давайте немного подправим оба этих
примера на C и на Python. Попробуем вызвать функцию <code>sum</code> от двух
строк:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;10&#34;</span><span class="p">,</span> <span class="s2">&#34;20&#34;</span><span class="p">))</span> <span class="c1"># &gt; 1020</span>
</span></span></code></pre></div><p>Здесь мы не получаем никаких ошибок, потому что действует
полиморфизм, а для строк операция сложения тоже реализована. Но
что будет в C?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%d</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">sum</span><span class="p">(</span><span class="s">&#34;10&#34;</span><span class="p">,</span> <span class="s">&#34;20&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Такую программу не выйдет даже скомпилировать. Мы опять получим
точно такой же лог ошибки, который был ранее. Обратите внимание
на то, как мы определяли функцию <code>sum</code> в языке C. Там мы явно
указали типы входных аргументов как <code>int</code>. Это означает, что
аргумент любого другого типа нельзя передать в эту функцию. Это
и называется статической типизацией. Также статическая типизация
обязывает указать тип для каждой переменной и запрещает менять
типы переменных после определения оных. То есть тип фиксирован,
статичен.</p>
<p>Вторая же группа языков называется динамически типизированными.
Это такие языки как Python, Lua, JavaScript и другие. В них,
соответственно, тип переменной строго не фиксирован и может
меняться в ходе исполнения программы.</p>

<h2 id="преимущества-типизации">
  <a class="link" href="#%d0%bf%d1%80%d0%b5%d0%b8%d0%bc%d1%83%d1%89%d0%b5%d1%81%d1%82%d0%b2%d0%b0-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8">
    #
  </a>
  Преимущества типизации
</h2>

<p>Пора перейти к вопросу, зачем типизация нам нужна, если в том же
Python всё и так работает отлично. Во-первых, это скорость. На
низком уровне (<em>чем бы он ни был представлен</em>) нам в любом случае
нужно знать типы переменных. А тот факт, что мы можем позволить
себе, их не выставлять, лишь означает, что кто-то делает это за
нас (<em>например, виртуальная машина</em>), а это в свою очередь
требует ресурсов. Отсюда и картина, которую мы наблюдаем
в рейтингах языков по скорости:</p>
<p><img
    src="/images/pl-rating_hu_96ba15e059292ea.webp"
    width="1600"
    height="1079"
    class="content-img" style="width:900px"
    alt=""
    loading="lazy"
    decoding="async"
  /></p>
<p>Python в этом
<a href="https://github.com/niklas-heer/speed-comparison">рейтинге</a>,
кстати говоря, на последнем месте. Может быть типизированный
Python как раз поможет нам исправить этот печальный факт?
К сожалению,
<a href="https://bernsteinbear.com/blog/typed-python/#fnref:simple-annotations">нет</a>.
Поскольку Python как не был исконно, так по сей день остается,
<strong>не</strong> статически типизированным языком. Аннотации типов в Python
остаются опциональными, их можно не указывать. И сам
интерпретатор Python в runtime не проверят их.</p>
<p>Тут мы переходим ко второму преимуществу, которое открывают для
нас типы. И это качество кода.</p>
<blockquote class="markdown-blockquote">
  <p>&ldquo;Цель типизации в Python — помочь инструментам разработки искать
ошибки в кодовых базах на Python с помощью статического
анализа, то есть не выполняя тестов кода.&rdquo;</p>
<p>Лусиану Рамальо, &ldquo;Python. К вершинам мастерства&rdquo;</p>

</blockquote>
<p>Расширяя эту мысль, мы можем уточнить, что цели типизации это:</p>
<ul>
<li>Раннее выявление ошибок — до runtime-а, до падения кода на
продакшене</li>
<li>&ldquo;Экстракция&rdquo; тестов — правильная типизация помогает уменьшить
количество тестов, писать и поддерживать которые сложнее, чем
типы. В тестах остаётся тестировать бизнес логику, а не
банальное несоответствие примитивов.</li>
<li>Улучшение читаемости кода — <a href="https://peps.python.org/pep-0020/">PEP
20</a> говорит нам о том, что
&ldquo;явное лучше неявного&rdquo;. Когда мы читаем код, нам достаточно
увидеть сигнатуру функции, не вчитываясь в её реализацию</li>
<li>Упрощение разработки в IDE — больше подсказок
и предупреждений о потенциальных ошибках.</li>
<li>Повышение качества архитектуры — типы &ldquo;заставляют&rdquo;
проектировать правильные абстракции.</li>
</ul>

<h2 id="как-писать-типизированно">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%bf%d0%b8%d1%81%d0%b0%d1%82%d1%8c-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d0%bd%d0%bd%d0%be">
    #
  </a>
  Как писать типизированно
</h2>

<p>Перед тем, как переходить к коду, давайте разберёмся с тремя
важными понятиями: интерфейс, абстрактный класс и протокол. В чем
разница? Давайте по порядку:</p>
<ul>
<li><strong>Интерфейс</strong> — это класс, у которого все методы абстрактные,
то есть не содержат деталей реализации.</li>
<li><strong>Абстрактный класс</strong> — это класс, у которого, помимо
абстрактных методов, есть еще и реализованные методы.</li>
<li><strong>Протокол</strong> — это неявный интерфейс.</li>
</ul>
<p>Первые два понятия должны быть понятны. А на последнем давайте
остановимся. В Python реализация паттерна интерфейса работает
через наследование, то есть, класс, реализующий интерфейс,
наследуется от этого класса-интерфейса. При этом класс, который
реализует протокол, не наследуется от него и вообще может о нем
не знать.</p>

<h3 id="примитивы">
  <a class="link" href="#%d0%bf%d1%80%d0%b8%d0%bc%d0%b8%d1%82%d0%b8%d0%b2%d1%8b">
    ##
  </a>
  Примитивы
</h3>

<p>Давайте наконец посмотрим на то, как же писать типизированный
код. Начнем с простейшего примера функции, которая
мультиплицирует переданную ей строку $n$ раз.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multi_string</span><span class="p">(</span><span class="n">string</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">string</span> <span class="o">*</span> <span class="n">n</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">multi_string</span><span class="p">(</span><span class="s2">&#34;cat&#34;</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span> <span class="c1"># &gt; catcatcat</span>
</span></span></code></pre></div><p>Это простейшая функция, которая принимает на вход строку и число,
а возвращает строку, полученную путём конкатенации исходной
строки с самой собой $n$ раз. Давайте типизируем эту функцию!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multi_string</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">n</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">string</span> <span class="o">*</span> <span class="n">n</span>
</span></span></code></pre></div><p>Синтаксис простой: типы для входных параметров мы подписываем
через двоеточие, а выходной тип с помощью стрелочки. Теперь наши
редакторы кода (IDE) будут подсвечивать для нас ошибку,
в случае если мы неправильно передадим входные или неправильно
обработаем выходные значения функции:</p>
<p><img src="/images/typing-demo-1.gif" class="content-img" style="width:700px" alt="" loading="lazy" decoding="async" /></p>
<p>Таким образом можно проаннотировать все классические примитивы
— <code>str</code>, <code>int</code>, <code>bytes</code>, <code>float</code>, <code>Decimal</code>, <code>bool</code>.</p>

<h3 id="объединение-типов">
  <a class="link" href="#%d0%be%d0%b1%d1%8a%d0%b5%d0%b4%d0%b8%d0%bd%d0%b5%d0%bd%d0%b8%d0%b5-%d1%82%d0%b8%d0%bf%d0%be%d0%b2">
    ##
  </a>
  Объединение типов
</h3>

<p>В реальности, конечно, бывают и более сложные кейсы, когда
функция может принимать и работать с разными типами, но все же
не любыми. В таких случаях можно использовать объединение типов:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">bytes</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">data</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">data</span>
</span></span></code></pre></div><p>В примере выше функция <code>normalize</code> принимает на вход либо строку,
либо последовательность байт и на выход всегда возвращает строку
в кодировке <code>utf-8</code>. Через <code>union</code> (<code>|</code>) можно перечислить любое
количество типов, но важно понимать, где это уместно. Если вам
хочется написать простыню из объединения десяти типов, стоит
хорошенько подумать. О том как такие ситуации разрешаются, мы
скажем ниже.</p>
<p>Объединение также можно использовать и в указании типа
возвращаемого значения. Но использовать это стоит только для
спецификации опциональности. Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_int</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">value</span><span class="o">.</span><span class="n">isdigit</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
</span></span></code></pre></div><p>По сути, тут мы говорим, что результат функции опционален. Она
может вернуть <code>int</code>, либо не вернуть его, если что-то пошло не
так. Однако, аннотировать возвращаемое значение как <code>int | str</code>
или любым другим подобным образом, где мы просто объединяем два
совершенно разных типа, считается плохой практикой. Потому что
в таком случае непонятно, как обрабатывать результат этой
функции. В результате функции мы можем ожидать либо конкретную
реализацию, про которую нам точно известны её методы и атрибуты,
либо <code>None</code>. Отступление от этого, как правило, будет приводить
к усложнению кода.</p>

<h3 id="спецификация-коллекций">
  <a class="link" href="#%d1%81%d0%bf%d0%b5%d1%86%d0%b8%d1%84%d0%b8%d0%ba%d0%b0%d1%86%d0%b8%d1%8f-%d0%ba%d0%be%d0%bb%d0%bb%d0%b5%d0%ba%d1%86%d0%b8%d0%b9">
    ##
  </a>
  Спецификация коллекций
</h3>

<p>Помимо примитивов мы конечно же можем аннотировать и коллекции.
Но лучше не ограничиваться простым <code>data: list</code>, а специфицировать
и содержание коллекции тоже. Это можно сделать, используя
синтаксис квадратных скобок. Давайте приведём несколько примеров:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">format_user</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">,</span> <span class="n">score</span> <span class="o">=</span> <span class="n">user</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">score</span><span class="si">}</span><span class="s2"> points&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">average</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">total_count</span><span class="p">(</span><span class="n">counters</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">counters</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
</span></span></code></pre></div><p>Это, конечно, не покрывает все возможные сценарии, которые могут
возникнуть в реальном продакшн коде. В секциях
<a href="/ru/articles/typing-python/#typeddict">TypedDict</a>, <a href="/ru/articles/typing-python/#namedtuple">NamedTuple</a>,
<a href="/ru/articles/typing-python/#dataclass">Dataclass</a> мы расширим наш инструментарий.</p>

<h3 id="mapping-и-mutablemapping">
  <a class="link" href="#mapping-%d0%b8-mutablemapping">
    ##
  </a>
  Mapping и MutableMapping
</h3>

<p><code>Mapping</code> и <code>MutableMapping</code> — это абстрактные классы для
словари-подобных структур. <code>Mapping</code> гарантирует только чтение
(ключи, значения, итерацию), а <code>MutableMapping</code> говорит, что
объект можно изменять.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Mapping</span><span class="p">,</span> <span class="n">MutableMapping</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">read_config</span><span class="p">(</span><span class="n">cfg</span><span class="p">:</span> <span class="n">Mapping</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">cfg</span><span class="p">[</span><span class="s2">&#34;DATABASE_URL&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">patch_config</span><span class="p">(</span><span class="n">cfg</span><span class="p">:</span> <span class="n">MutableMapping</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">cfg</span><span class="p">[</span><span class="s2">&#34;DEBUG&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;1&#34;</span>
</span></span></code></pre></div><p>Если функция только читает данные, указывайте <code>Mapping</code>. Если
меняет — <code>MutableMapping</code>. Это маленькая, но важная подсказка для
читателя и статического анализатора.</p>

<h3 id="namedtuple">
  <a class="link" href="#namedtuple">
    ##
  </a>
  NamedTuple
</h3>

<p>Когда нужно описать структуру с фиксированным набором полей и
одновременно сохранить поведение кортежа, удобно использовать
<code>NamedTuple</code>. Это неизменяемый тип данных, который можно
индексировать и при этом читать поля по именам.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NamedTuple</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">NamedTuple</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">print_user</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="si">}</span><span class="s2">) = </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">score</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p><code>NamedTuple</code> хорошо подходит для компактных структур данных,
которые не должны изменяться после создания. Если вам нужны
изменяемые поля и более богатое поведение, лучше выбрать
<code>dataclass</code> или обычный класс.</p>

<h3 id="typeddict">
  <a class="link" href="#typeddict">
    ##
  </a>
  TypedDict
</h3>

<p>Если же ваша структура — это обычный словарь, но вы хотите
типизировать ожидаемые ключи и значения, используйте <code>TypedDict</code>.
Такой тип описывает форму словаря и работает только на уровне
статического анализа.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypedDict</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">TypedDict</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_email</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Это особенно полезно, когда данные приходят из JSON или другого
динамического источника, но вы хотите строгий контракт по ключам.
Если часть ключей опциональна, опишите их явно, чтобы не терять
проверку на уровне типов.</p>

<h3 id="dataclass">
  <a class="link" href="#dataclass">
    ##
  </a>
  Dataclass
</h3>

<p><code>dataclass</code> — это удобный способ описать &ldquo;контейнер данных&rdquo; с
инициализатором, сравнениями и читаемым <code>repr</code>. Такой класс
изменяем, если не указать обратное, и отлично подходит для домена
или DTO.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span><span class="o">.</span><span class="n">username</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">user</span>
</span></span></code></pre></div><p><code>dataclass</code> хорош, когда нужна структура, которая может
изменяться, и когда важна ясная модель данных. Если объект
должен быть неизменяемым, используйте <code>@dataclass(frozen=True)</code>.</p>

<h3 id="enum">
  <a class="link" href="#enum">
    ##
  </a>
  Enum
</h3>

<p><code>Enum</code> помогает описать закрытый набор значений. Это полезно,
когда у поля есть строго ограниченный набор допустимых вариантов,
и вы хотите, чтобы типы не позволяли случайные строки.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Status</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">NEW</span> <span class="o">=</span> <span class="s2">&#34;new&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">DONE</span> <span class="o">=</span> <span class="s2">&#34;done&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">FAILED</span> <span class="o">=</span> <span class="s2">&#34;failed&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">is_done</span><span class="p">(</span><span class="n">status</span><span class="p">:</span> <span class="n">Status</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">status</span> <span class="ow">is</span> <span class="n">Status</span><span class="o">.</span><span class="n">DONE</span>
</span></span></code></pre></div><p>Такой подход удобен для статусов, ролей, флагов, режимов работы,
то есть любого &ldquo;перечислимого&rdquo; доменного значения.</p>

<h3 id="кастомные-классы">
  <a class="link" href="#%d0%ba%d0%b0%d1%81%d1%82%d0%be%d0%bc%d0%bd%d1%8b%d0%b5-%d0%ba%d0%bb%d0%b0%d1%81%d1%81%d1%8b">
    ##
  </a>
  Кастомные классы
</h3>

<p>Конечно же система типов позволяет специфицировать не только
примитивы, но и наши собственные или библиотечные классы.
Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">friends</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">User</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hand_shake</span><span class="p">(</span><span class="n">user1</span><span class="p">:</span> <span class="n">User</span><span class="p">,</span> <span class="n">user2</span><span class="p">:</span> <span class="n">User</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user1</span><span class="o">.</span><span class="n">friends</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">user2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">user2</span><span class="o">.</span><span class="n">friends</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">user1</span><span class="p">)</span>
</span></span></code></pre></div>
<h3 id="абстрактные-классы">
  <a class="link" href="#%d0%b0%d0%b1%d1%81%d1%82%d1%80%d0%b0%d0%ba%d1%82%d0%bd%d1%8b%d0%b5-%d0%ba%d0%bb%d0%b0%d1%81%d1%81%d1%8b">
    ##
  </a>
  Абстрактные классы
</h3>

<p>В Python у нас существует прекрасный модуль <code>collections.abc</code>. Там
уже описан ряд абстрактных классов, которые в 90% случаев закроют
все наши потребности. Они полезны, когда вы хотите описывать
поведение, а не конкретную реализацию. Итак, что же там
представлено? А вот что:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ABCMeta</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncGenerator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncIterable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">AsyncIterator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Awaitable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Buffer</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ByteString</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Callable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Collection</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Container</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Coroutine</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">EllipsisType</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">FunctionType</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Generator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">GenericAlias</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Hashable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ItemsView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Iterable</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Iterator</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">KeysView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Mapping</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MappingView</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableMapping</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableSequence</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">MutableSet</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Reversible</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Sequence</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Set</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">Sized</span>
</span></span><span class="line"><span class="cl"><span class="n">collections</span><span class="o">.</span><span class="n">abc</span><span class="o">.</span><span class="n">ValuesView</span>
</span></span></code></pre></div><p>Как мы видим тут большое количество абстрактных классов.
Большинство из них созданы для описания какого-то свойства
коллекции. Аннотируя с их помощью код, мы можем выражать более
широкие полиморфные границы наших функций. Например:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">total</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">((</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">total</span><span class="p">({</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">})</span>
</span></span></code></pre></div><p>Иногда можно встретить такие же импорты из модуля <code>typing</code>:
<code>from typing import Iterable, Sequence</code>. Но в реальности это
просто реэкспорт реализаций из <code>collections.abc</code>. Поэтому сейчас
лучше импортировать абстрактные классы напрямую из
<code>collections.abc</code>.</p>

<h3 id="sequence-и-iterable">
  <a class="link" href="#sequence-%d0%b8-iterable">
    ##
  </a>
  Sequence и Iterable
</h3>

<p>Эти два типа часто путают. <code>Iterable</code> гарантирует только то, что
объект можно перебирать в цикле. Никаких индексов, длины или
упорядоченности тут не обещается. <code>Sequence</code> же, кроме
возможности итерации, гарантирует наличие индексации и длины,
то есть <code>__getitem__</code> и <code>__len__</code>. Из этого вытекает разница
в том, что можно безопасно делать с объектом.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span><span class="p">,</span> <span class="n">Sequence</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sum_any</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">total</span> <span class="o">+=</span> <span class="n">v</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">total</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">head</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="n">Sequence</span><span class="p">[</span><span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">values</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span></code></pre></div><p>Функция <code>sum_any</code> примет и список, и кортеж, и генератор.
Функция <code>head</code> уже не сможет принять генератор, потому что у него
нет индексации, и мы не можем написать <code>values[0]</code>. Поэтому,
когда вам важно только итерироваться — используйте <code>Iterable</code>,
а если вы опираетесь на индексацию или длину — <code>Sequence</code>.</p>

<h3 id="callable">
  <a class="link" href="#callable">
    ##
  </a>
  Callable
</h3>

<p><code>Callable</code> используется, когда функция принимает другую функцию.
Это особенно важно для коллбеков, обработчиков событий и функций
высшего порядка.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Callable</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">apply</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">],</span> <span class="n">fn</span><span class="p">:</span> <span class="n">Callable</span><span class="p">[[</span><span class="nb">int</span><span class="p">],</span> <span class="nb">int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">fn</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span></code></pre></div><p>Если сигнатура заранее неизвестна, можно использовать
<code>Callable[..., ReturnType]</code>, но это стоит делать как крайний
вариант.</p>

<h3 id="дженерики-generics">
  <a class="link" href="#%d0%b4%d0%b6%d0%b5%d0%bd%d0%b5%d1%80%d0%b8%d0%ba%d0%b8-generics">
    ##
  </a>
  Дженерики (generics)
</h3>

<p>Generics — это параметризованные обобщённые типы. Проще говоря,
это типы, которые сами принимают типы. Это важно, когда вы хотите
сохранить связь между входом и выходом, а не потерять её в <code>Any</code>.
Например, функция <code>first</code> возвращает элемент того же типа, что и
внутри переданной коллекции:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Sequence</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeVar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">T</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s2">&#34;T&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">first</span><span class="p">(</span><span class="n">items</span><span class="p">:</span> <span class="n">Sequence</span><span class="p">[</span><span class="n">T</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">items</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span></code></pre></div><p>Без дженериков нам пришлось бы писать <code>Sequence[Any]</code> и терять
тип результата. А с дженериками анализатор знает, что если мы
передали <code>Sequence[str]</code>, то вернётся <code>str</code>. Это особенно важно
для коллекций, фабрик и репозиториев, где один и тот же код
обрабатывает разные типы.</p>
<p>Также полезно знать, что у <code>TypeVar</code> есть параметр <code>bound</code>,
который позволяет ограничить те типы, которые могут попадать
в дженерик:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Hashable</span><span class="p">,</span> <span class="n">Iterable</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeVar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">HashableT</span> <span class="o">=</span> <span class="n">TypeVar</span><span class="p">(</span><span class="s2">&#34;HashableT&#34;</span><span class="p">,</span> <span class="n">bound</span><span class="o">=</span><span class="n">Hashable</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">mode</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="n">Iterable</span><span class="p">[</span><span class="n">HashableT</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">HashableT</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div>
<h3 id="literal">
  <a class="link" href="#literal">
    ##
  </a>
  Literal
</h3>

<p><code>Literal</code> позволяет зафиксировать конкретные допустимые значения,
а не просто базовый тип. Это полезно, когда у параметра есть
закрытый набор режимов, статусов или ключей.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Literal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">export_report</span><span class="p">(</span><span class="nb">format</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">&#34;csv&#34;</span><span class="p">,</span> <span class="s2">&#34;json&#34;</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Сигнатура выше сразу задаёт контракт: третьего формата тут нет.
Это делает API понятнее и позволяет анализатору ловить опечатки
вроде <code>&quot;jsno&quot;</code> до запуска.</p>

<h3 id="статические-анализаторы">
  <a class="link" href="#%d1%81%d1%82%d0%b0%d1%82%d0%b8%d1%87%d0%b5%d1%81%d0%ba%d0%b8%d0%b5-%d0%b0%d0%bd%d0%b0%d0%bb%d0%b8%d0%b7%d0%b0%d1%82%d0%be%d1%80%d1%8b">
    ##
  </a>
  Статические анализаторы
</h3>

<p>И наконец пришло время познакомиться с тем, кто &ldquo;оживляет&rdquo;
типизацию в Python: со статическими анализаторами. Они читают
аннотации, сопоставляют их с кодом и подсвечивают ошибки до
runtime.</p>
<ul>
<li><code>mypy</code> - классический статический анализатор типов для
постепенного внедрения типизации. Сильная сторона: экосистема
плагинов и тонкая настройка строгости по модулям. Слабая
сторона: без настройки может быть либо слишком мягким, либо
слишком шумным.</li>
<li><code>pyright</code> - быстрый и понятный чекер. Сильная сторона: хорошие
диагностические сообщения и быстрый feedback. Слабая сторона:
расширяемость через плагины хуже, чем в <code>mypy</code>.</li>
<li><code>pyrefly</code> - новый быстрый анализатор на Rust. Сильная сторона:
высокая скорость и интеграция с LSP. Слабая сторона: проект
ещё молодой, поэтому часть поведения может меняться.</li>
<li><code>ty</code> - новый Rust-инструмент от Astral (пока beta).
Сильная сторона: скорость и современная архитектура. Слабая
сторона: pre-release стадия, часть возможностей ещё догоняет
зрелые чекеры.</li>
</ul>
<p>Установить и запускать их можно через <code>uv</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uv tool install mypy
</span></span><span class="line"><span class="cl">uv tool install pyright
</span></span><span class="line"><span class="cl">uv tool install pyrefly
</span></span><span class="line"><span class="cl">uv tool install ty
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">uvx mypy .
</span></span><span class="line"><span class="cl">uvx pyright .
</span></span><span class="line"><span class="cl">uvx pyrefly check
</span></span><span class="line"><span class="cl">uvx ty check
</span></span></code></pre></div><p>Ниже бизнес-пример с намеренными ошибками типизации:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NewType</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span> <span class="o">=</span> <span class="n">NewType</span><span class="p">(</span><span class="s2">&#34;UserId&#34;</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@dataclass</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">User</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">id</span><span class="p">:</span> <span class="n">UserId</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">    <span class="n">is_active</span><span class="p">:</span> <span class="nb">bool</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">discount</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">percent</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">total</span> <span class="o">-</span> <span class="n">total</span> <span class="o">*</span> <span class="p">(</span><span class="n">percent</span> <span class="o">/</span> <span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send_invoice</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="n">User</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">is_active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;invoice for </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">amount</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="mi">42</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="mi">123</span><span class="p">,</span> <span class="n">is_active</span><span class="o">=</span><span class="s2">&#34;yes&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total</span> <span class="o">=</span> <span class="n">discount</span><span class="p">(</span><span class="s2">&#34;1000&#34;</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">send_invoice</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="s2">&#34;500&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Проверка <code>pyright</code> даст сообщения примерно такого вида:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error: Type &#34;float&#34; is not assignable to return type &#34;int&#34;
</span></span><span class="line"><span class="cl">error: Type &#34;None&#34; is not assignable to return type &#34;str&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[42]&#34; is not assignable to &#34;UserId&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[123]&#34; is not assignable to &#34;str&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;yes&#39;]&#34; is not assignable to &#34;bool&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;1000&#39;]&#34; is not assignable to &#34;int&#34;
</span></span><span class="line"><span class="cl">error: &#34;Literal[&#39;500&#39;]&#34; is not assignable to &#34;int&#34;
</span></span></code></pre></div><p>Что важно в этом выводе:</p>
<ul>
<li>Ошибки в <code>discount</code> и <code>send_invoice</code> показывают нарушение
контракта функции: сигнатура обещает одно, реализация делает
другое.</li>
<li>Ошибки в <code>main</code> показывают, что граничный слой (ввод/DTO) передаёт
неверные типы в доменную логику.</li>
<li>Сообщение про <code>UserId</code> демонстрирует, зачем <code>NewType</code> полезен в
бизнес-коде: <code>id</code> пользователя нельзя случайно подменить обычным
<code>int</code> без явного решения разработчика.</li>
</ul>

<h3 id="stub-файлы-pyi">
  <a class="link" href="#stub-%d1%84%d0%b0%d0%b9%d0%bb%d1%8b-pyi">
    ##
  </a>
  Stub файлы (.pyi)
</h3>

<p>Иногда хочется типизировать код, к которому нельзя или неудобно
вносить изменения. Например, это сгенерированный код, код
сторонней библиотеки или даже ваш собственный модуль, где вы не
хотите мешать типы с реализацией. Для этого существуют stub
файлы — файлы с расширением <code>.pyi</code>.</p>
<p>Файл <code>.pyi</code> содержит только типовые сигнатуры и не содержит
реализации. Статические анализаторы ищут их рядом с кодом или в
отдельных пакетах <code>types-*</code>. Пример:</p>
<p><code>calc.py</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
</span></span></code></pre></div><p><code>calc.pyi</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span> <span class="o">...</span>
</span></span></code></pre></div><p>Так вы можете поддерживать типизацию отдельно от реализации, а
иногда даже без доступа к исходникам.</p>

<h3 id="typealias">
  <a class="link" href="#typealias">
    ##
  </a>
  TypeAlias
</h3>

<p>Когда тип становится сложным, его лучше вынести в алиас, чтобы
сигнатуры были читаемыми. Это особенно полезно для бизнес-терминов
вроде <code>UserId</code>, <code>Currency</code>, <code>Payload</code>. Для этого используют
<code>TypeAlias</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">TypeAlias</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="n">Payload</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span> <span class="o">|</span> <span class="nb">float</span><span class="p">]</span>
</span></span></code></pre></div><p>Теперь можно писать:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="n">UserId</span><span class="p">,</span> <span class="n">payload</span><span class="p">:</span> <span class="n">Payload</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>В Python 3.12+ для того же есть синтаксис <code>type</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">type</span> <span class="n">UserId</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="nb">type</span> <span class="n">Payload</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span> <span class="o">|</span> <span class="nb">float</span><span class="p">]</span>
</span></span></code></pre></div><p>Это всё тот же алиас, просто короче и читабельнее.</p>

<h3 id="typealias-vs-newtype">
  <a class="link" href="#typealias-vs-newtype">
    ##
  </a>
  TypeAlias vs NewType
</h3>

<p><code>TypeAlias</code> — это просто синоним типа, он не создаёт новый тип.
<code>NewType</code> же создаёт новый тип на уровне статической проверки,
хотя на runtime это всего лишь функция-обёртка. Это полезно,
когда у вас есть логически разные значения одного базового типа,
например <code>UserId</code> и <code>OrderId</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">NewType</span><span class="p">,</span> <span class="n">TypeAlias</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UserId</span><span class="p">:</span> <span class="n">TypeAlias</span> <span class="o">=</span> <span class="nb">int</span>
</span></span><span class="line"><span class="cl"><span class="n">OrderId</span> <span class="o">=</span> <span class="n">NewType</span><span class="p">(</span><span class="s2">&#34;OrderId&#34;</span><span class="p">,</span> <span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="n">UserId</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="n">OrderId</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p><code>UserId</code> и <code>int</code> считаются одним и тем же типом, а вот <code>OrderId</code>
уже не совместим с <code>int</code> без явного приведения.</p>

<h2 id="как-правильно-использовать-типизацию">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%bf%d1%80%d0%b0%d0%b2%d0%b8%d0%bb%d1%8c%d0%bd%d0%be-%d0%b8%d1%81%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d1%82%d1%8c-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8e">
    #
  </a>
  Как правильно использовать типизацию
</h2>

<p>Сразу хочу сформировать у вас верную предпосылку относительно
типизации. Точно так же как и у тестов, задача у типов
<strong>падать</strong>, не проходить, крашиться и бесить этим нас. Если они
этого не делают, их можно просто выкинуть. Это означает, что чем
менее снисходительна к нам система типизации, тем лучше для нас.
Потому что это заставляет нас думать о том, как писать надёжный
и безопасный код. Строгость системы типизации (<em>как и у тестов</em>)
помогает выявлять ошибки, но для этого они должны <strong>не
проходить.</strong></p>
<p>И да, писать действительно корректно типизированный код сложно.
Это отдельный навык, который развивается так же, как архитектура
или тестирование. Поэтому нормально начинать с малого и
постепенно усиливать строгость.</p>

<h3 id="возвращение-none">
  <a class="link" href="#%d0%b2%d0%be%d0%b7%d0%b2%d1%80%d0%b0%d1%89%d0%b5%d0%bd%d0%b8%d0%b5-none">
    ##
  </a>
  Возвращение <code>None</code>
</h3>

<p>Также обратите внимание на случай, когда функция ничего не
возвращает:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">print_weather</span><span class="p">(</span><span class="n">weather</span><span class="p">:</span> <span class="n">Weather</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Weather:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">date</span><span class="p">,</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">weather</span><span class="o">.</span><span class="n">by_days</span><span class="p">()</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">date</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">temperature</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">humidity</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="se">\t</span><span class="si">{</span><span class="n">data</span><span class="o">.</span><span class="n">wind_speed</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;========&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">tmp</span> <span class="o">=</span> <span class="n">print_weather</span><span class="p">(</span><span class="n">Weather</span><span class="p">())</span> <span class="o">+</span> <span class="mi">1</span>
</span></span></code></pre></div><p>Как известно в Python, если в функции не стоит <code>return</code>, значит
она ничего не возвращает. Об этом знают и статические анализаторы.
Например, <code>pyright</code> для этого кода, где намеренно в последней
строчке допущена ошибка, даст следующее предупреждение:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">error:
</span></span><span class="line"><span class="cl">    Operator &#34;+&#34; not supported for types &#34;None&#34; and &#34;Literal[1]&#34;
</span></span></code></pre></div><p>По этому сообщению становится ясно, что <code>pyright</code> &ldquo;понимает&rdquo;, что
возвращаемый тип функции <code>None</code>. Так значит можно их не
подписывать? Нет, не значит. Во-первых, тут можно просто
сослаться на <a href="https://peps.python.org/pep-0020/">PEP 20</a>, &ldquo;явное
лучше неявного&rdquo;. Во-вторых, стоит учесть специфику
developer experience (DX) при разработке на Python. Все, кто пишут на
Python, знают, что типизация опциональна. И это создаёт
двусмысленность: когда я смотрю на сигнатуру функции, где не
специфицировано возвращаемое значение, я не понимаю, это автор
кода просто не стал её типизировать и на самом деле она возвращает
<strong>не</strong> <code>None</code>, либо там действительно возвращается <code>None</code>.
Разрешается эта двусмысленность только погружением в детали
реализации функции, что усложняет работу с кодом.</p>

<h3 id="вход-и-выход-у-функции">
  <a class="link" href="#%d0%b2%d1%85%d0%be%d0%b4-%d0%b8-%d0%b2%d1%8b%d1%85%d0%be%d0%b4-%d1%83-%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d0%b8">
    ##
  </a>
  Вход и выход у функции
</h3>

<p>Продолжим тему возвращаемых значений. Не стоит прибегать
к использованию абстрактных классов из модуля <code>collections.abc</code>,
а также реализованных самостоятельно, для указания возвращаемого
значения из функции. Они предназначены для входных значений,
чтобы сохранить полиморфность функции в максимально широких
границах. Возвращаемое значение должно быть специфицировано
конкретной реализацией, чтобы было <strong>ясно</strong>, как обрабатывать
возвращаемый результат.</p>





<blockquote class="markdown-alert markdown-alert--important">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>
    

    
      Important
    
  </div>

  <p>Функция должна более ясно говорить о том, какой конкретный тип
она возвращает, чем принимает.</p>
</blockquote>
<br>
<hr>
<br>
<p>Если вы хотите углубиться в тему, ниже — отличные материалы,
которые помогут настроить инструменты и разобраться в деталях
типовой системы Python.</p>
<ul>
<li><a href="https://to.digital/typed-python/intro/intro.html">Типизированный Python для профессиональной
разработчики</a></li>
<li><a href="https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations">FastAPI type hints guide (Полезно для
web-разработчиков)</a></li>
<li><a href="https://realpython.com/python-type-checking/">RealPython, Type Checking
Guide</a></li>
<li><a href="https://python-type-challenges.zeabur.app/">Упражнения</a></li>
</ul>
]]></content:encoded><category>python</category><category>eosp</category></item><item><title>Как внести свой вклад</title><link>https://alchemmist.xyz/ru/articles/how-we-contribute/</link><pubDate>Sat, 07 Feb 2026 21:25:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/how-we-contribute/</guid><description>Это краткое руководство для начинающих, о том, как работать с открытым исходным кодом в рамках практик GitHub flow. Все примеры будут с платформой GitHub, но можно использовать и другие хостинги репозиториев, которые предоставляют необходимый функционал для реализации GitHub flow. Здесь вы можете найти классический способ внести свой вклад почти в любой проект. А так же узнать несколько советов и хаков.
Итак, с чего же начинается любой вклад. В идеале, с проблемы. Проблемой можно называть что угодно, в том числе нехватка каких-то новых фич, отсутствие либо неполнота страниц документации по определённому вопросу, какой-то вопрос, либо же банальные баги, которые есть везде. Лично вы проблему ни с чем не спутаете, ведь она обязательно будет сопровождать зудящим чувством, что “что-то подбешивает.” Но будет ли это проблемой для других пользователей и разработчиков? Для того, чтобы это понять и синхронизировать ваше представление с представлениями команды, и существует issue.</description><content:encoded><![CDATA[<p>Это краткое руководство для начинающих, о том, как работать
с открытым исходным кодом в рамках практик GitHub flow. Все
примеры будут с платформой GitHub, но можно использовать и другие
хостинги репозиториев, которые предоставляют необходимый
функционал для реализации GitHub flow. Здесь вы можете найти
классический способ внести свой вклад почти в любой проект. А так
же узнать несколько советов и хаков.</p>
<p>Итак, с чего же начинается любой вклад. В идеале, с проблемы.
Проблемой можно называть что угодно, в том числе нехватка
каких-то новых фич, отсутствие либо неполнота страниц
документации по определённому вопросу, какой-то вопрос, либо же
банальные баги, которые есть везде. Лично вы проблему ни с чем не
спутаете, ведь она обязательно будет сопровождать зудящим
чувством, что &ldquo;что-то подбешивает.&rdquo; Но будет ли это проблемой для
других пользователей и разработчиков? Для того, чтобы это понять
и синхронизировать ваше представление с представлениями команды,
и существует issue.</p>





<blockquote class="markdown-alert markdown-alert--caution">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>
    

    
      CONTRIBUTION.md
    
  </div>

  <p>В каждом проекте есть свои собственные устоявшиеся правила
того, как нужно в них участвовать. Эти правило описываются либо
в <code>README.md</code> в отдельном параграфе &ldquo;Contribution&rdquo;, либо
в специальном файле <code>CONTRIBUTION.md</code>. Обязательно прочитайте
его в самом начале. Этот файл имеет первый приоритет. То есть
даже если вы понимаете, что какие-то практики отходят от той
правильной схемы, которую мы рассмотрим ниже, всё равно
следуйте им. Некоторые крупные проекты, такие как <code>Postgres</code>
или <code>linux-kernel</code> вообще до сих пор работает по mail-листам.
Следует уважать те традиции, которые сложились в репозитории,
даже если сейчас это выглядит как legacy.</p>
</blockquote>

<h2 id="1-issue">
  <a class="link" href="#1-issue">
    #
  </a>
  1. Issue
</h2>

<p>Issue представляет из себя тикет, который хранится в репозитории.
Создать его может любой человек, пришедший в репозиторий. Но
я рекомендую вам, перед тем как бросаться описывать, вашу
проблему, сделать несколько попыток по поиску подобных issue,
чтобы не нагружать лишней работой мейнтейнеров репозитория.
Давайте посмотрим как это можно сделать:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue list --search <span class="s2">&#34;app crashed&#34;</span>
</span></span></code></pre></div><blockquote class="markdown-blockquote">
  <p>Этот и все дальнейшие примеры будут с использованием
официальной утилиты <code>gh</code>. Она позволяет из консоли выполнять
все базовые операции с GitHub. Рекомендую использовать её для
простых операций, для ускорения работы.</p>

</blockquote>
<p>Параметр <code>search</code> позволяет задавать прям в себе добавлять
различные фильтры поиска. Давайте попробуем их добавить:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue list --search <span class="s2">&#34;app crashed is:open label:bug created:&gt;2024-01-01&#34;</span>
</span></span></code></pre></div><p>Вполне возможно, особенно в крупных проектах, что это позволит
вам выйти на уже заведённый тикет по вашей проблеме и даже найти
в нём решение или ответ. Если же нет, то не стесняйтесь
открывать новый issue. Наилучшим образом его структурировать
поможет схема:</p>
<pre class="mermaid">
  graph LR
    A[Excepted] ---&gt; B(&#34;Action (run/click/etc)&#34;)
    B ---&gt; C[Got]
</pre>

<p>В крупных проектах существуют issue templates, которые помогут
вам оформить issue в соответствии с теми стандартными, которые
приняты в этом проекте. Создать issue можно командой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh issue create
</span></span></code></pre></div><p>Так же отличной практикой является предоставление docker образа,
в котором настроено все окружение, и проблема гарантированно
воспроизводима. Либо хотя бы предоставить лог. Профессиональным
подходом так же будет локализация проблемы, настолько насколько
это возможно. Например, вы получили ошибку в таком файле:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">fruits</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logistic</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="n">fruits</span><span class="o">.</span><span class="n">Apple</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">basket</span> <span class="o">=</span> <span class="n">fruits</span><span class="o">.</span><span class="n">MakeBasket</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">basket</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logistic</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">basket</span><span class="p">)</span>
</span></span></code></pre></div><p>Конечно, можно было бы так в issue и написать. Но еще лучше будет
самостоятельно поэкспериментировать с этим кодом и понять, что
именно не так. Например проверить удалось ли добавить яблоко
в корзину:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">basket</span><span class="o">.</span><span class="n">put</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">basket</span><span class="o">.</span><span class="n">content</span><span class="p">())</span>
</span></span></code></pre></div><p>Возможно проблема в том, что мы пытаемся отправить пустую
корзину, что будет означать баг в методе <code>put</code>. Либо же все-таки
проблема в методе <code>send</code>. Таким образом, мы срезаем лишнее,
оставляя в сообщении issue суть проблемы.</p>
<p>Помните, о том, что даже факт заведения успешного issue,
например, предложения ценной фичи или обнаружение реального бага
или уязвимости — это уже вклад в проект, и это уже успех.
А неуспехом будет создание дублирующего issue или перегрузка
мейнтейнеров лишним контекстом.</p>

<h2 id="2-fork">
  <a class="link" href="#2-fork">
    #
  </a>
  2. Fork
</h2>

<p>Когда вы поняли какую проблемы вы решаете и убедились, что это
реально проблема, можно переходить к её решению. Для этого стоит
для начала сделать fork репозитория. Это будет, как ценным
подспорьем для вас если вы, например, будете тестировать CI или
просто сильно опережать оригинальный репозиторий, так и позволит
отправлять pull request-ы, ведь вы, скорее всего, не имеете прав
для внесения изменений в оригинальный репозиторий:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh repo fork &lt;OWNER&gt;/&lt;REPO&gt;
</span></span></code></pre></div><p>Клонировав fork локально вносите изменения в отдельной ветке,
которую будет удобнее назвать номером issue, который вы пытаетесь
закрыть. Почему? Потому что в коротком лаконичном названии ветки
<strong>далеко</strong> не всегда удастся выразить суть проблемы, на которую,
возможно, ушло несколько месяцев обсуждения в тиките. Поэтому
гораздо удобнее просто на него сослаться, через номер issue:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git switch -c <span class="m">42</span>
</span></span></code></pre></div><p>В GitHub flow мы не создаем сложную структуру из веток, где
у каждой есть свое значение. Вместо этого у нас есть master-ветка
в которую попадают только прошедшие все проверки изменения.
А попадают они туда из множества, так называемых feature-веток,
которые мы предлагаем тут привязывать к конкретным issue. А раз
в какое-то время мы на master ветке выпускаем релиз. В итоге
получается вполне лакончиная структура:</p>
<pre class="mermaid">
  %%{init: {
  &#34;gitGraph&#34;: {
    &#34;tagLabelColor&#34;: &#34;#ffffff&#34;,
    &#34;tagLabelBackground&#34;: &#34;#d73a49&#34;
  }
}}%%
gitGraph
    commit
    commit
    branch &#34;42&#34;
    checkout &#34;42&#34;
    commit
    commit
    checkout main
    merge &#34;42&#34;
    commit tag: &#34;🚩 v1.1.23&#34;

    branch &#34;44&#34;
    checkout &#34;44&#34;
    commit
    commit
    commit
    checkout main
    merge &#34;44&#34;
    commit tag: &#34;🚩 v1.2.0&#34;
</pre>

<p>Так же не забывайте синхронизировать ваш <code>fork</code>, чтобы не
потерять связь с оригинальным проектом:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh repo sync &lt;your-username&gt;/&lt;fork-repo&gt; --branch main
</span></span></code></pre></div>
<h2 id="3-pull-request">
  <a class="link" href="#3-pull-request">
    #
  </a>
  3. Pull request
</h2>

<p>Когда изменения внесены и закоммичены, то можно отправлять pull
request. Удобнее всего просто остаться на вашей ветке и выполнить
команду ниже. <code>gh</code> сам сформирует подходящий pull request:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh pr create
</span></span></code></pre></div><p>После создания PR могут автоматически запуститься проверки, если
в репозитории настроен CI. И это отлично. Потому что проверки
упрощают и ускоряют процесс ревью. Вы сразу можете увидеть, что
в вашем коде например не проходят проверки стилей, или, что вы не
написали тесты на свой код и из-за этого упал code coverage. Не
игнорируйте это, а делайте новой коммиты в той же ветки и после
пуша они автоматически добавятся в PR и проверки будут
перезапущены. Мейнтейнер репозитория даже не начнет ревью вашего
PR, пока все проверки в нем не станут зелёными. Проверить статус
pull request-а вы можете командой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">gh pr status
</span></span></code></pre></div><p>Так же старайтесь сделать аккуратную историю коммитов в pull
request-е, чтобы упросить процесс review. В этом вам помогут
<a href="https://www.geeksforgeeks.org/git/git-squash/">squash</a> коммиты.</p>
<p>Когда вы прорвались через все авто-проверки, не стесняйтесь
обращаться к архитектору репозитория и просить его ревью. Он либо
сам сделает ревью вашего PR, либо делегирует это кому-то из
команды. На GitHub это делается просто через тег: <code>@username</code>.
Часто уведомления о том, что пришёл новый pull request могут быть
отключены, и тем более архитектор не узнает, когда вы внесете все
правки, чтобы пройти CI. Поэтому скромность здесь может привести
к тому, что ваш pull request просто никто не увидит. Тоже,
кстати, касается и issue.</p>
<p>Если вы понимаете, что ваш pull request полностью закрывает тот
issue, который вы решали, то в сообщениях PR можно указать <code>Close \#49</code>, тогда issue будет автоматически закрыт после слияния pull
request-а.</p>

<h2 id="4-merge">
  <a class="link" href="#4-merge">
    #
  </a>
  4. Merge
</h2>

<p>Итак, когда вы прошли все авто-проверки и ручное ревью, то ваш
код попадет в master-ветку репозитория и вы можете считать себя
полноправным контрибьютором в этот проект. Так держать!</p>
<p>Обратите так же внимание на интересную практику, которая, на мой
взгляд, очень профессиональна. Представим что вы обнаружили
и доложили о каком-то баге в issue. Это уже вклад в репозиторий.
Дальше предлагается создать pull request, в котором вы пишете
тест, который воспроизводит этот баг. Тест конечно-же будет
падать. Но вы присылать pull request, внося тем самым этот тест
в состоянии disabled в кодовую базу. Так же можно оставить <code>TODO</code>
метку. И это станет еще большим вкладом в репозиторий, поскольку
вы добавили содержательный тест, а этом всегда очень полезно.
И наконец вторым pull request-ом вы уже присылаете изменения,
которые чинят код так, чтобы он проходил этот тест. И вновь
увеличиваете ваш вклад. Таким образом вы оставили после себя
массу артефактов в репозитории: issue, усиленная система тестов,
и исправленный баг. Это будет отличным примером реализации TDD
(Test Driven Development) подхода.</p>
<br>
<hr>
<br>
<p>Таким образом мы с вами прошли полный цикл от проблемы до вклада
в master-ветку:</p>
<pre class="mermaid">
  flowchart LR
    BUG[&#34;Find a bug / Need feature&#34;] --&gt; ISSUE[&#34;Create Issue&#34;]
    ISSUE --&gt; DISCUSS[&#34;Discussion / Planning&#34;]
    DISCUSS --&gt; PR[&#34;Open Pull Request&#34;]
    PR --&gt; CI[&#34;PR Checks with CI&#34;]
    CI --&gt; REVIEW[&#34;Reviewer Reviews PR&#34;]
    REVIEW --&gt; MERGE[&#34;Merge PR into main&#34;]
    MERGE --&gt; BUG
</pre>

<p>Вы можете проверить свои силы уже сейчас, найдя подходящий issue
с помощью этих прекрасных сервисов:</p>
<ul>
<li><a href="https://goodfirstissue.dev/">Good First Issue</a> — Issue,
которые отлично подходят для новичков.</li>
<li><a href="https://github.com/MunGell/awesome-for-beginners">Awesome for
Beginners</a>
— Подборка проектов для начинающих.</li>
<li><a href="https://up-for-grabs.net/#/">Up For Grabs</a> — Открытые для
решения задачи по разным технологиям.</li>
<li><a href="https://github.com/firstcontributions/first-contributions">First
Contributions</a>
— Репозиторий с манулом с списком открытых проектов.</li>
<li><a href="https://www.firsttimersonly.com/">First Timers Only</a> — Больше
подобных ресурсов.</li>
</ul>
<p>Не забывайте, что вы и сами можете регистрировать свои проекты на
этих ресурсах, чтобы привлекать к себе новых разработчиков!</p>





<blockquote class="markdown-alert markdown-alert--note">
  <div class="markdown-alert__title">
    
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>
    

    
      GitHub CLI
    
  </div>

  <p>Если вас заинтересовала продуктивная работа с GitHub из консоли
с помощью утилиты <code>gh</code>, обратите внимание на её
<a href="https://cli.github.com/manual/">документацию</a>. А так же на то,
что <code>gh</code> поддерживает расширения, ознакомиться с которыми вы
можете с помощью команды <code>gh ext browse</code> или написать своё!</p>
</blockquote>
<p>Так же можно почитать по теме:</p>
<ul>
<li><a href="https://opensource.guide/how-to-contribute/">Official GitHub
guideline</a></li>
<li><a href="https://navendu.me/posts/pull-requests-like-a-pro/">Pull Requests like a Pro, Navendu
Pottekkat</a></li>
<li><a href="https://dev.to/helloquash/10-common-mistakes-to-avoid-when-contributing-to-open-source-projects-1mna">10 Common Mistakes to Avoid When Contributing to Open Source
Projects</a></li>
<li><a href="https://github-help-wanted.com/open-source/beginners-guide/">Open Source for
Beginners</a></li>
</ul>
]]></content:encoded><category>open-source</category><category>eosp</category></item><item><title>Алгоритм Уэлфорда</title><link>https://alchemmist.xyz/ru/articles/welford-algorithm/</link><pubDate>Wed, 10 Sep 2025 17:31:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/welford-algorithm/</guid><description>Представьте, что к вам потоком поступают данные: миллионы транзакций, цены акций, показания датчиков. Вам нужно вычислить среднее значение, дисперсию и корреляции. Классический способ — сохранить все данные и затем вычислить. Но это занимает много памяти, вычисления становятся тяжелыми, на больших числах возникают ошибки округления. Недавно я столкнулся с алгоритмом Уэлфорда, который позволяет проводить онлайн-вычисления за одну итерацию без сохранения больших данных. Это очень элегантный, стабильный и точный алгоритм.
Постановка задачи довольно проста: мы хотим вычислять статистики (среднее, дисперсию, ковариацию, корреляцию) шаг за шагом, когда мы получаем $n$ данных в потоке, без сохранения всех данных и дополнительных итераций. Напомним формулы базовым способом. Среднее:</description><content:encoded><![CDATA[<p>Представьте, что к вам потоком поступают данные: миллионы транзакций, цены акций, показания датчиков. Вам нужно вычислить среднее значение, дисперсию и корреляции. Классический способ — сохранить все данные и затем вычислить. Но это занимает много памяти, вычисления становятся тяжелыми, на больших числах возникают ошибки округления. Недавно я столкнулся с алгоритмом Уэлфорда, который позволяет проводить онлайн-вычисления за одну итерацию без сохранения больших данных. Это очень элегантный, стабильный и точный алгоритм.</p>
<p>Постановка задачи довольно проста: мы хотим вычислять статистики (среднее, дисперсию, ковариацию, корреляцию) шаг за шагом, когда мы получаем $n$ данных в потоке, без сохранения всех данных и дополнительных итераций. Напомним формулы базовым способом. Среднее:
</p>
$$
\overline{x} = \frac{1}{n}\sum\limits_{i = 1}^{n}x_i
$$<p>
Дисперсия:
</p>
$$
s^2 = \frac{1}{n - 1}\sum\limits_{i = 1}^{n}(x_i - \overline{x})^2
$$<p>
Как видите, для среднего нужна сумма: первая проблема — как получить сумму без всех чисел? А для дисперсии нужно среднее: вторая проблема — как узнать среднее без всех данных?</p>

<h2 id="интуиция-метода">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d1%83%d0%b8%d1%86%d0%b8%d1%8f-%d0%bc%d0%b5%d1%82%d0%be%d0%b4%d0%b0">
    #
  </a>
  Интуиция метода
</h2>

<p>Попробуем прочувствовать интуицию метода Уэлфорда. Представьте, у нас есть набор данных: $A = \{1, 2, 3\}$, среднее этого набора $\overline{x}_A = \frac{1 + 2 +3}{3} = 2$, и затем мы получили еще одно число: $6$. Чувствуете, что оно сдвинет наше среднее в сторону больших значений? Но насколько? Представьте похожий набор, но с большим количеством данных: $B = \{1, 1, 2, 2, 3, 3\}$, среднее также равно $2$, и мы также добавляем число $6$. Снова чувствуем, что сдвиг будет в сторону больших значений, но не такой сильный. Проверим нашу интуицию:
</p>
$$
\begin{align*}
&A' = \{1, 2, 3, 6\}, \quad \overline{x}_{A'} = \frac{1 + 2 + 3 + 6}{4} = 3, \quad \Delta_A = |\overline{x}_A - \overline{x}_{A'}| = 1 \\
&B' = \{1, 1, 2, 2, 3, 3, 6\},  \quad \overline{x}_{B'} = \frac{2(1 + 2 + 3) + 6}{7} \approx 2.57, \quad \Delta_{B} = \overline{x}_B - \overline{x}_{B'} \approx 0.57
\end{align*}
$$<p>
Наша интуиция была права! Осталось понять, как мы можем вычислить эту дельту, если у нас есть только предыдущее среднее $\overline{x}_{i - 1}$, новое число $x_i$ и количество наших чисел. Метод Уэлфорда гласит: каждое новое значение сдвигает среднее в свою сторону с весом $1/i$:
</p>
$$
\overline{x}_i = \overline{x}_{i - 1} + \frac{1}{i}(x_i - \overline{x}_{i - 1})
$$<p>
Проверим это для набора $B$. У нас есть среднее без нового числа $\overline{x}_{i - 1} = 2$ и новое число $x_i = 6$, и это седьмое число, $i = 7$. Цель — найти среднее $\overline{x}_i$ по формуле Уэлфорда:
</p>
$$
\overline{x}_i = 2 + \frac{1}{7}(6 - 2) = 2 + 0.57\ldotp\!\ldotp\!\ldotp \ \approx 2.57
$$<p>
Как видите, это абсолютно точное значение. Отлично!</p>

<h2 id="дисперсия-корреляция-ковариация">
  <a class="link" href="#%d0%b4%d0%b8%d1%81%d0%bf%d0%b5%d1%80%d1%81%d0%b8%d1%8f-%d0%ba%d0%be%d1%80%d1%80%d0%b5%d0%bb%d1%8f%d1%86%d0%b8%d1%8f-%d0%ba%d0%be%d0%b2%d0%b0%d1%80%d0%b8%d0%b0%d1%86%d0%b8%d1%8f">
    #
  </a>
  Дисперсия, корреляция, ковариация
</h2>

<p>Истинная сила алгоритма Уэлфорда раскрывается при вычислении дисперсии. Наивный подход требует хранения всех точек данных и пересчета среднего перед вычислением дисперсии, что вычислительно дорого. Метод Уэлфорда изящно решает это, поддерживая текущую оценку дисперсии с использованием следующего рекуррентного соотношения:</p>
<p>Для дисперсии мы поддерживаем текущее среднее $\overline{x}$ и сумму квадратов разностей $S$. Для каждой новой точки данных $x_n$ на шаге $n$:
</p>
$$
\begin{gather*}
\overline{x}_n = \overline{x}_{n-1} + \frac{1}{n}(x_n - \overline{x}_{n-1}) \\
S_n = S_{n-1} + (x_n - \overline{x}_{n-1})(x_n - \overline{x}_n)
\end{gather*}
$$<p>
Дисперсию генеральной совокупности можно затем вычислить как $\sigma^2 = S_n/n$, а выборочную дисперсию как $s^2 = S_n/(n-1)$.</p>
<p>Этот подход красиво расширяется на ковариацию и корреляцию. Для двух переменных $X$ и $Y$ мы поддерживаем счетчики $n$, средние $\overline{x}$ и $\overline{y}$, и накопленные произведения $C_{xy}$:
</p>
$$
\begin{gather*}
\delta_x = x_n - \overline{x}_{n-1}, \quad
\delta_y = y_n - \overline{y}_{n-1} \\
\overline{x}_n = \overline{x}_{n-1} + \frac{\delta_x}{n}, \quad
\overline{y}_n = \overline{y}_{n-1} + \frac{\delta_y}{n} \\
C_{xy_n} = C_{xy_{n-1}} + \delta_x (y_n - \overline{y}_n) \\
\end{gather*}
$$<p>
Ковариация тогда равна $\text{cov}(X,Y) = \frac{C_{xy_n}}{(n-1)}$, а корреляция $\text{corr}(X,Y) = \frac{C_{xy_n}}{\sqrt{S_{x_n} S_{y_n}}}$.</p>

<h2 id="реализация-на-c">
  <a class="link" href="#%d1%80%d0%b5%d0%b0%d0%bb%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8f-%d0%bd%d0%b0-c">
    #
  </a>
  Реализация на C
</h2>

<p>Вот полная реализация алгоритма Уэлфорда на C для вычисления среднего и дисперсии:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;math.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">long</span> <span class="kt">long</span> <span class="n">n</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">mean</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">M2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">welford_state</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">welford_update</span><span class="p">(</span><span class="n">welford_state</span> <span class="o">*</span><span class="n">state</span><span class="p">,</span> <span class="kt">double</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">delta</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean</span> <span class="o">+=</span> <span class="n">delta</span> <span class="o">/</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">M2</span> <span class="o">+=</span> <span class="n">delta</span> <span class="o">*</span> <span class="p">(</span><span class="n">x</span> <span class="o">-</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">double</span> <span class="nf">welford_mean</span><span class="p">(</span><span class="k">const</span> <span class="n">welford_state</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">double</span> <span class="nf">welford_variance</span><span class="p">(</span><span class="k">const</span> <span class="n">welford_state</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">?</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">M2</span> <span class="o">/</span> <span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">:</span> <span class="mf">0.0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">double</span> <span class="nf">welford_stddev</span><span class="p">(</span><span class="k">const</span> <span class="n">welford_state</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nf">sqrt</span><span class="p">(</span><span class="nf">welford_variance</span><span class="p">(</span><span class="n">state</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">welford_state</span> <span class="n">state</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">test_data</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="kt">size_t</span> <span class="n">data_size</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">test_data</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">test_data</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">data_size</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">welford_update</span><span class="p">(</span><span class="o">&amp;</span><span class="n">state</span><span class="p">,</span> <span class="n">test_data</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Added %.1f: mean=%.3f, variance=%.3f, stddev=%.3f</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">               <span class="n">test_data</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="nf">welford_mean</span><span class="p">(</span><span class="o">&amp;</span><span class="n">state</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">               <span class="nf">welford_variance</span><span class="p">(</span><span class="o">&amp;</span><span class="n">state</span><span class="p">),</span> <span class="nf">welford_stddev</span><span class="p">(</span><span class="o">&amp;</span><span class="n">state</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Для ковариации и корреляции мы можем расширить этот подход:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">long</span> <span class="kt">long</span> <span class="n">n</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">mean_x</span><span class="p">,</span> <span class="n">mean_y</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">C2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="n">welford_cov_state</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">welford_cov_update</span><span class="p">(</span><span class="n">welford_cov_state</span> <span class="o">*</span><span class="n">state</span><span class="p">,</span> <span class="kt">double</span> <span class="n">x</span><span class="p">,</span> <span class="kt">double</span> <span class="n">y</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">delta_x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean_x</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">double</span> <span class="n">delta_y</span> <span class="o">=</span> <span class="n">y</span> <span class="o">-</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean_y</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean_x</span> <span class="o">+=</span> <span class="n">delta_x</span> <span class="o">/</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean_y</span> <span class="o">+=</span> <span class="n">delta_y</span> <span class="o">/</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">state</span><span class="o">-&gt;</span><span class="n">C2</span> <span class="o">+=</span> <span class="n">delta_x</span> <span class="o">*</span> <span class="p">(</span><span class="n">y</span> <span class="o">-</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">mean_y</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">double</span> <span class="nf">welford_covariance</span><span class="p">(</span><span class="k">const</span> <span class="n">welford_cov_state</span> <span class="o">*</span><span class="n">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">?</span> <span class="n">state</span><span class="o">-&gt;</span><span class="n">C2</span> <span class="o">/</span> <span class="p">(</span><span class="n">state</span><span class="o">-&gt;</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">:</span> <span class="mf">0.0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>]]></content:encoded><category>algorithms</category><category>C</category><category>math</category></item><item><title>Devsyringe: Хватит копипастить динамические значения</title><link>https://alchemmist.xyz/ru/articles/the-devsyringe/</link><pubDate>Fri, 05 Sep 2025 14:22:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/the-devsyringe/</guid><description>Сегодня я хочу рассказать о devsyringe — инструменте, который помогает автоматизировать работу с динамическими значениями в статических файлах.
Мы, разработчики, живём в мире динамических значений. API-токены, временные URL от сервисов туннелирования, таких как ngrok или localtunnel, строки подключения к базам данных и флаги функций — это жизненная сила наших современных приложений. В компилируемых языках или средах вроде Node.js у нас есть богатая экосистема инструментов, таких как dotenv и библиотеки для работы с конфигурацией, чтобы управлять этой динамикой беспрепятственно. Они позволяют выносить конфигурацию наружу, подставляя значения из переменных окружения или менеджеров секретов во время выполнения.</description><content:encoded><![CDATA[<p>Сегодня я хочу рассказать о <a href="https://devsyringe.alchemmist.xyz">devsyringe</a> — инструменте, который помогает автоматизировать работу с динамическими значениями в статических файлах.</p>
<p>Мы, разработчики, живём в мире динамических значений. API-токены, временные URL от сервисов туннелирования, таких как <code>ngrok</code> или <code>localtunnel</code>, строки подключения к базам данных и флаги функций — это жизненная сила наших современных приложений. В компилируемых языках или средах вроде Node.js у нас есть богатая экосистема инструментов, таких как <code>dotenv</code> и библиотеки для работы с конфигурацией, чтобы управлять этой динамикой беспрепятственно. Они позволяют выносить конфигурацию наружу, подставляя значения из переменных окружения или менеджеров секретов во время выполнения.</p>
<p>Но что насчёт статических частей наших проектов? Представьте простой HTML-файл, которому нужно отобразить временный публичный URL, объект конфигурации JavaScript для лёгкого скрипта или статический конфигурационный файл для устаревшей системы. Они не обрабатываются сборщиком или фреймворком; это плоские файлы. Традиционное решение — ручное, подверженное ошибкам и разочаровывающее: запустить команду, посмотреть на её вывод, скопировать новое значение, открыть файл, найти нужную строку, вставить, сохранить, а затем повторить для каждого другого файла, которому нужно то же значение — скука. Этот процесс выбивает из состояния потока и полностью противоречит автоматизации, которую мы так ценим.</p>
<p>Я слишком часто сталкивался с этой проблемой. Постоянное переключение контекста между терминалом и редактором для обновления URL было назойливой неэффективностью. Я знал, что должен быть лучший способ — способ рассматривать эти статические файлы как динамические конечные точки для конфигурации. Это разочарование стало катализатором для <strong>Devsyringe</strong>, CLI-утилиты, написанной на Go, предназначенной для автоматизации именно этого рабочего процесса.</p>

<h2 id="основная-идея-декларативное-внедрение-конфигурации">
  <a class="link" href="#%d0%be%d1%81%d0%bd%d0%be%d0%b2%d0%bd%d0%b0%d1%8f-%d0%b8%d0%b4%d0%b5%d1%8f-%d0%b4%d0%b5%d0%ba%d0%bb%d0%b0%d1%80%d0%b0%d1%82%d0%b8%d0%b2%d0%bd%d0%be%d0%b5-%d0%b2%d0%bd%d0%b5%d0%b4%d1%80%d0%b5%d0%bd%d0%b8%d0%b5-%d0%ba%d0%be%d0%bd%d1%84%d0%b8%d0%b3%d1%83%d1%80%d0%b0%d1%86%d0%b8%d0%b8">
    #
  </a>
  Основная идея: Декларативное внедрение конфигурации
</h2>

<p>Мотивация к созданию Devsyringe заключалась не в том, чтобы создать ещё один движок шаблонов или сложную систему сборки. Цель была проще и более сфокусированной: создать инструмент, который может запустить команду, распарсить её вывод на предмет конкретного значения с помощью regex, а затем внедрить это значение в один или несколько целевых файлов. Всё поведение определяется декларативно в простом YAML-файле конфигурации, обычно называемом <code>devsyringe.yaml</code>.</p>
<p>Думайте об этом как об универсальном адаптере между динамическим выводом CLI-инструментов и статическим текстом ваших файлов. Он создан для сценариев, где более тяжёлые решения являются избыточными или просто не подходят. Настоящая магия заключается в автоматизации без участия рук; вы определяете правила один раз, и с этого момента одна команда поддерживает всё в синхронизации. Это клей позволяющий связывать между собой различные этапы любого пайплайна.</p>

<h2 id="практические-сценарии-использования-и-применения">
  <a class="link" href="#%d0%bf%d1%80%d0%b0%d0%ba%d1%82%d0%b8%d1%87%d0%b5%d1%81%d0%ba%d0%b8%d0%b5-%d1%81%d1%86%d0%b5%d0%bd%d0%b0%d1%80%d0%b8%d0%b8-%d0%b8%d1%81%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d0%bd%d0%b8%d1%8f-%d0%b8-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d0%bd%d0%b5%d0%bd%d0%b8%d1%8f">
    #
  </a>
  Практические сценарии использования и применения
</h2>

<p>Давайте выйдем за рамки абстрактных концепций и посмотрим на практические, мощные сценарии использования, которые избавляют вас от утомительной ручной работы. Паттерн универсален — любая CLI-команда, которая выводит значение, нужное вам в файле, является идеальным кандидатом для автоматизации.</p>
<p><strong>Классическая автоматизация туннеля.</strong><br>
При разработке веб-приложений вам часто нужно открывать ваш локальный сервер для интернета. Инструменты вроде <code>localtunnel</code> или <code>serveo</code> генерируют уникальные, случайные URL, которые должны быть настроены и в вашем frontend, и в backend коде. Вот как Devsyringe обрабатывает это автоматически:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serums</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">localtunnel</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">lt --port 8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mask</span><span class="p">:</span><span class="w"> </span><span class="l">https://[a-z0-9\-]+\.loca\.lt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">env_file</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">clues</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;API_BASE_URL&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">frontend_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./src/config.js</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">clues</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;API_URL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;const&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Эта конфигурация определяет полный workflow: выполнить команду туннеля, извлечь URL с помощью regex и внедрить его как в <code>.env</code> файл, так и в JavaScript конфигурацию. Весь процесс сводится к одной команде: <code>dsy inject</code>.</p>
<p><strong>Динамическая документация и автоматизация резюме.</strong><br>
Вот более сложный use case: поддержание технической документации или резюме, сгенерированных в LaTeX, в актуальном состоянии с live-статистикой. Представьте, что ваш файл résumé.tex содержит строку вроде: <code>\newcommand{\githubstars}{427}</code></p>
<p>С Devsyringe вы можете автоматически заполнять это вашей текущей статистикой с GitHub:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serums</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">github_stats</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">curl -s &#34;https://api.github.com/users/yourusername/repos&#34; | jq &#39;reduce .[] as $repo (0; . + $repo.stargazers_count)&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mask</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;(\d+)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">resume</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">./resume.tex</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">clues</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;githubstars&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Эта конфигурация вызывает псевдо GitHub API, извлекает общее количество звёзд и обновляет исходный файл LaTeX. Ваше резюме поддерживает точную, актуальную статистику без ручного вмешательства — идеально для заявок на работу или обновлений портфолио. А ведь затем можно настроить CI для регулярного обновления с каждым пушем, если вы используете репозиторий для резюме.</p>
<p><strong>Продвинутое управление инфраструктурой.</strong><br>
Для более сложных setup&rsquo;ов Devsyringe отлично подходит для автоматизации инфраструктуры. Рассмотрим эти сценарии:</p>
<ul>
<li>Получение свежих учётных данных БД из HashiCorp Vault и их внедрение в свойства приложения.</li>
<li>Получение текущих endpoint&rsquo;ов развёртывания из service discovery и обновление конфигураций API gateway.</li>
<li>Получение IP-адресов инфраструктуры от облачных провайдеров и обновление правил security groups.</li>
<li>Извлечение метаданных сборки из CI-систем и обновление манифестов развёртывания.</li>
</ul>
<p>Паттерн остаётся такой же: команда  извлечение вывода  внедрение в файл. Эта простота делает его адаптируемым к бесчисленным сценариям автоматизации. Если вам нужно конкретное значение — нет проблем, используйте: <code>echo '&lt;my-value&gt;'</code> в качестве команды, devsyringe сделает всё остальное сам.</p>

<h2 id="техническая-архитектура-и-реализация">
  <a class="link" href="#%d1%82%d0%b5%d1%85%d0%bd%d0%b8%d1%87%d0%b5%d1%81%d0%ba%d0%b0%d1%8f-%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d0%b8-%d1%80%d0%b5%d0%b0%d0%bb%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8f">
    #
  </a>
  Техническая архитектура и реализация
</h2>

<p>Хотя концепция Devsyringe проста, создание надёжного CLI-инструмента требует тщательной архитектуры. Проект использует сильные стороны Go для конкурентной обработки и кроссплатформенной совместимости, сохраняя простоту и надёжность.</p>
<p><strong>Структура проекта и шаблоны проектирования.</strong><br>
Архитектурная элегантность devsyringe заключается в намеренном использовании <strong>Декоратора (Decorator Pattern)</strong>, который отражает многослойную структуру “капусты” — каждый “лист” оборачивает ядро, добавляя функциональность, сохраняя целостность внутренних слоёв. Этот паттерн является основополагающим для дизайна инструмента: низкоуровневые компоненты остаются в блаженном неведении о слоях выше них, обеспечивая модульность и разделение ответственности.</p>
<p>Крайне важно, что эта философия расширяется за пределы внутренней архитектуры инструмента до его взаимодействия с вашими проектами. Devsyringe работает как <strong>невидимый декоратор</strong> для вашей кодовой базы. Ваше приложение остаётся полностью unaware о его присутствии — не требуется специальных импортов, хуков или изменений в вашем коде. Он просто обновляет статические файлы на диске, и ваш проект запускается именно так, как и раньше, просто с новыми значениями на месте. Это автоматизация, которая ощущается как магия, потому что не оставляет следов своего присутствия.</p>
<p>Этот подход обеспечивает бесшовную интеграцию в любой workflow. Используете ли вы его локально для управления URL туннелей или в CI/CD для внедрения секретов времени сборки, devsyringe улучшает вашу среду, не изменяя её фундаментальное поведение. Ваши конфигурационные файлы остаются валидными, ваши процессы сборки неизменными, и ваша ментальная модель неизменной — всё это при получении мощности внедрения динамических значений.</p>
<p>Кодовая база devsyringe следует стандартным соглашениям Go с чётким разделением ответственности:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">│── cmd
</span></span><span class="line"><span class="cl">│   └── dsy
</span></span><span class="line"><span class="cl">│       └── main.go
</span></span><span class="line"><span class="cl">└── internal
</span></span><span class="line"><span class="cl">    ├── cli
</span></span><span class="line"><span class="cl">    │   ├── commands.go
</span></span><span class="line"><span class="cl">    │   └── tui
</span></span><span class="line"><span class="cl">    │       └── tui.go
</span></span><span class="line"><span class="cl">    ├── config
</span></span><span class="line"><span class="cl">    │   └── config.go
</span></span><span class="line"><span class="cl">    ├── process
</span></span><span class="line"><span class="cl">    │   ├── process.go
</span></span><span class="line"><span class="cl">    │   └── procmanager.go
</span></span><span class="line"><span class="cl">    └── utils
</span></span><span class="line"><span class="cl">        └── utils.go
</span></span></code></pre></div><p>Такая организация делает кодовую базу доступной для обслуживания и тестирования, разделяя логические компоненты.</p>
<p><strong>Ключевые технические решения.</strong><br>
Реализация делает несколько вдумчивых выборов, которые улучшают надёжность и пользовательский опыт:</p>
<p>Фреймворк Cobra CLI обеспечивает профессиональную структуру команд, автоматическую генерацию справки и завершение shell. TUI интерфейс, предлагает мониторинг процессов в реальном времени, запущенных devsyringe:<br>
<img src="/images/demo-devsyringe.gif" class="content-img" style="width:700px" alt="" loading="lazy" decoding="async" />
Система управления процессами использует goroutines и channels для конкурентного выполнения команд и захвата вывода. Внедрение в файлы использует простое pattern matching вместо сложного парсинга, делая его универсально применимым к любому текстовому формату.</p>
<p><strong>Надёжная обработка ошибок и валидация.</strong><br>
Инструмент включает всестороннюю проверку ошибок на каждом этапе: валидация конфигурации, выполнение команды, парсинг вывода и файловые операции. Смысловые сообщения об ошибках направляют пользователей к разрешению проблем, а не загадочно завершаются сбоем.</p>

<h2 id="дистрибуция-и-кроссплатформенная-совместимость">
  <a class="link" href="#%d0%b4%d0%b8%d1%81%d1%82%d1%80%d0%b8%d0%b1%d1%83%d1%86%d0%b8%d1%8f-%d0%b8-%d0%ba%d1%80%d0%be%d1%81%d1%81%d0%bf%d0%bb%d0%b0%d1%82%d1%84%d0%be%d1%80%d0%bc%d0%b5%d0%bd%d0%bd%d0%b0%d1%8f-%d1%81%d0%be%d0%b2%d0%bc%d0%b5%d1%81%d1%82%d0%b8%d0%bc%d0%be%d1%81%d1%82%d1%8c">
    #
  </a>
  Дистрибуция и кроссплатформенная совместимость
</h2>

<p>Инструмент полезен только если его легко установить. Devsyringe использует современные инструменты Go для предоставления бесшовной установки на всех основных платформах.</p>
<p><strong>Автоматизированные сборки с Goreleaser.</strong><br>
Проект использует Goreleaser для автоматизации процесса релиза, генерируя: Кроссплатформенные бинарники для macOS и Linux, Поддержку пакетных менеджеров (Homebrew, AUR), Пакеты Debian и RPM в <a href="https://github.com/alchemmist/devsyringe/releases">GitHub releases</a> с контрольными суммами и подписями. Поддержка Windows уже в <a href="https://github.com/alchemmist/devsyringe/issues/10">бэклоге</a>.</p>
<p><strong>Простота установки.</strong> Пользователи могут установить через несколько каналов. Для Go-разработчиков:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">go install github.com/alchemmist/devsyringe/cmd/dsy@latest
</span></span></code></pre></div><p>Для пользователей macOS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew install devsyringe
</span></span></code></pre></div><p>Для пользователей Arch Linux:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">paru -S devsyringe
</span></span></code></pre></div><p>Этот многоканальный подход гарантирует, что инструмент доступен разработчикам независимо от их предпочитаемого окружения.</p>

<h2 id="почему-devsyringe-изменит-ваш-workflow">
  <a class="link" href="#%d0%bf%d0%be%d1%87%d0%b5%d0%bc%d1%83-devsyringe-%d0%b8%d0%b7%d0%bc%d0%b5%d0%bd%d0%b8%d1%82-%d0%b2%d0%b0%d1%88-workflow">
    #
  </a>
  Почему devsyringe изменит ваш workflow
</h2>

<p>Ценность devsyringe проявляется после первого раза, когда вы используете его по-настоящему. Тот момент, когда вы понимаете, что больше никогда не будете вручную обновлять URL туннеля в нескольких файлах. Когда ваша документация автоматически остаётся актуальной с версиями вашего API. Когда ваше резюме обновляется само с вашими последними достижениями.</p>
<p>Дело не только в сэкономленном времени — хотя оно накапливается удивительно быстро. Дело в поддержании состояния потока и устранении маленьких раздражений, которые накапливаются в течение сессии разработки. Вы можете быстро “заткнуть” любое значение через devsyringe, чтобы продолжить разработку, не отвлекаясь на лишнее.</p>
<p>Кривая обучения намеренно пологая. YAML-конфигурация простая, команды интуитивные, а обратная связь моментальная. Вы можете перейти от установки к решению реальных проблем менее чем за пять минут.</p>

<h2 id="начало-работы-и-следующие-шаги">
  <a class="link" href="#%d0%bd%d0%b0%d1%87%d0%b0%d0%bb%d0%be-%d1%80%d0%b0%d0%b1%d0%be%d1%82%d1%8b-%d0%b8-%d1%81%d0%bb%d0%b5%d0%b4%d1%83%d1%8e%d1%89%d0%b8%d0%b5-%d1%88%d0%b0%d0%b3%d0%b8">
    #
  </a>
  Начало работы и следующие шаги
</h2>

<p>Готовы устранить ручное обновление файлов из вашего workflow? Установка проста:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">go install github.com/alchemmist/devsyringe/cmd/dsy@latest
</span></span></code></pre></div><p>Посетите GitHub <a href="https://github.com/alchemmist/devsyringe">репозиторий</a> для полной документации, примеров и исходного кода. <code>README</code> содержит практические примеры, которые вы можете моментально адаптировать для ваших практических кейсов.</p>
<p>Подумайте, где в вашем workflow вы всё ещё копируете значения между терминалом и редактором. Это ваша отправная точка. Будь то URL туннелей, API-ключи, номера версий или статистика — devsyringe, вероятно, может это автоматизировать.</p>
]]></content:encoded><category>tools</category></item><item><title>Безопасно делимся экраном в Hyprland</title><link>https://alchemmist.xyz/ru/articles/hyrpland-noscreenshare/</link><pubDate>Sat, 23 Aug 2025 17:14:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/hyrpland-noscreenshare/</guid><description>Мы все были в ситуации, когда нужно показать экран с множеством окон, и требуется несколько минут перед нажатием кнопки “Поделиться экраном”, чтобы принять решение. Это нормально, особенно если ваша система — ваш второй (или даже первый) дом: с личными заметками, документами, паролями, чатами и так далее.
Далее мы настроим удобный рабочий процесс для такого случая. Однако эта настройка будет использовать новую функцию, встроенную в Hyprland и, скорее всего, отсутствующую в других DE/композиторах. Я продемонстрирую нашу конечную цель. Например, мой режим демонстрации экрана может выглядеть так:</description><content:encoded><![CDATA[<p>Мы все были в ситуации, когда нужно показать экран с множеством окон, и требуется несколько минут перед нажатием кнопки &ldquo;Поделиться экраном&rdquo;, чтобы принять решение. Это нормально, особенно если ваша система — ваш второй (или даже первый) дом: с личными заметками, документами, паролями, чатами и так далее.</p>
<p>Далее мы настроим удобный рабочий процесс для такого случая. Однако эта настройка будет использовать новую функцию, встроенную в Hyprland и, скорее всего, отсутствующую в других DE/композиторах. Я продемонстрирую нашу конечную цель. Например, мой режим демонстрации экрана может выглядеть так:
<img src="/images/noscreenshare-demo.mp4" class="content-img" style="width:85px" alt="" loading="lazy" decoding="async" /></p>
<blockquote class="markdown-blockquote">
  <p>Если вам нравятся мои обои, вы можете найти <a href="https://github.com/alchemmist/dotfiles/blob/main/wallpapers/miyazaki/images/1198594-3200x1680-desktop-hd-studio-ghibli-background.jpg">эти</a> и много других отличных материалов в моём репозитории <code>dotfiles</code> на <a href="https://github.com/alchemmist/dotfiles/tree/main/wallpapers">GitHub</a>. Не забывайте: я обожаю ваши звёзды!</p>

</blockquote>
<p>Разберём всё по шагам. Во-первых, вспомним, что в <a href="https://hypr.land/news/update50/#:~:text=on%20by%20default.-,No%20screen%20share,-There%E2%80%99s%20a%20new">релизе</a> Hyprland версии <code>0.50.0</code> было добавлено новое правило <code>windowrulev2</code> с опцией <code>noscreenshare</code>. Это свежий релиз, и сейчас оно управляется только из конфигурационного файла, вот так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">windowrulev2 = noscreenshare, class:^(org.telegram.desktop|obsidian|discord)$
</span></span></code></pre></div><p>Эта опция предписывает Hyprland рисовать чёрный прямоугольник вместо содержимого окна (даже если это окно не в фокусе или на заднем плане). Это работает идеально и защищает нашу приватность. Но я обнаружил два неудобных случая: 1. Когда мы хотим сделать скриншот (в моём случае, с помощью flameshot), Hyprland интерпретирует это как попытку поделиться экраном и скрывает все приватные окна. 2. Иногда я хочу показать что-то из моего Obsidian, например, и лезть в конфиги в этот момент — не очень хорошо.</p>

<h2 id="управление-опцией-noscreenshare-через-cli">
  <a class="link" href="#%d1%83%d0%bf%d1%80%d0%b0%d0%b2%d0%bb%d0%b5%d0%bd%d0%b8%d0%b5-%d0%be%d0%bf%d1%86%d0%b8%d0%b5%d0%b9-noscreenshare-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-cli">
    #
  </a>
  Управление опцией noscreenshare через CLI
</h2>

<p>Чтобы исправить этот опыт, напишем скрипт для управления правилом окна в конфиге. Если у вас нет времени, полная версия скрипта здесь. Скрипт будет комментировать или раскомментировать строку в конфигурационном файле с правилом и сохранять информацию о текущем состоянии правила в файле-маркере <code>~/.config/hypr/.screenshare_rule_disabled</code>. Скрипт можно запустить с командой <code>toggle</code>, чтобы изменить состояние на противоположное:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">CONFIG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/hypr/hyprland.conf&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">STATE_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/hypr/.screenshare_rule_disabled&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">toggle_rule<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">	<span class="c1"># TODO</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="si">${</span><span class="nv">1</span><span class="si">}</span> in
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;toggle&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        toggle_rule
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span>
</span></span></code></pre></div><p>Напишем функцию <code>toggle_rule</code>. Мы будем использовать утилиту Linux <code>sed</code> (<em>Stream Editor</em>). Это один из самых мощных инструментов для обработки текста в Linux и Unix-системах. Он обычно используется для таких задач, как поиск и замена, преобразование текста и потоковое редактирование. Синтаксис <code>sed</code>, в общем виде, выглядит так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sed <span class="o">[</span>options<span class="o">]</span> <span class="s2">&#34;script&#34;</span> <span class="o">[</span>file...<span class="o">]</span>
</span></span></code></pre></div><p>Нам нужна опция <code>-i</code> для редактирования файла на месте. Затем пишем скрипт. Сначала нам нужно найти строку с правилом <code>noscreenshare</code>, как в regex:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;/windowrolev2.*noscreenshare.*&#34;</span>
</span></span></code></pre></div><p>Мы начинаем с <code>&quot;/&quot;</code> для написания поискового запроса, нам нужно, чтобы строка начиналась с <code>&quot;windowrolev2&quot;</code>, после чего <code>&quot;.*&quot;</code> для любого количества любых символов, между которыми <code>&quot;noscreenshare&quot;</code>. Затем мы используем <code>&quot;/&quot;</code> для завершения поискового запроса и <code>&quot;s/&quot;</code> (<em>substitute</em>) для замены:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;/windowrulev2.*noscreenshare.*/s/^#//&#34;</span>
</span></span></code></pre></div><p>Наконец, мы используем регулярное выражение <code>&quot;^#&quot;</code> для поиска решётки в начале строки и заменяем её на ничто: <code>&quot;//&quot;</code>. Полная команда выглядит так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sed -i <span class="s1">&#39;/windowrulev2.*noscreenshare.*/s/^#//&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG_FILE</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Затем используем <code>sed</code> для добавления решётки в начало строки аналогичным образом, в зависимости от наличия файла-маркера:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">toggle_rule<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        sed -i <span class="s1">&#39;/windowrulev2.*noscreenshare.*/s/^#//&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        rm -f <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;on&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        sed -i <span class="s1">&#39;/windowrulev2.*noscreenshare.*/s/^/#/&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        touch <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;of&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><p>На последнем шаге скрипта мы вызываем эту функцию, если была дана опция <code>toggle</code>, и возвращаем json-подобную строку для waybar. Полный скрипт:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">CONFIG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/hypr/hyprland.conf&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">STATE_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.config/hypr/.screenshare_rule_disabled&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">toggle_rule<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        sed -i <span class="s1">&#39;/windowrulev2.*noscreenshare.*/s/^#//&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        rm -f <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;on&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">        sed -i <span class="s1">&#39;/windowrulev2.*noscreenshare.*/s/^/#/&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONFIG_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        touch <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;off&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="si">${</span><span class="nv">1</span><span class="si">}</span> in
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;toggle&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">        toggle_rule
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl">    *<span class="o">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$STATE_FILE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">            <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;off&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span>
</span></span><span class="line"><span class="cl">            <span class="nv">CLASS</span><span class="o">=</span><span class="s2">&#34;on&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">fi</span>
</span></span><span class="line"><span class="cl">        <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;{\&#34;class\&#34;: \&#34;</span><span class="nv">$CLASS</span><span class="s2">\&#34;, \&#34;alt\&#34;: \&#34;</span><span class="nv">$CLASS</span><span class="s2">\&#34;}&#34;</span>
</span></span></code></pre></div>
<h2 id="модуль-waybar">
  <a class="link" href="#%d0%bc%d0%be%d0%b4%d1%83%d0%bb%d1%8c-waybar">
    #
  </a>
  Модуль Waybar
</h2>

<p>Чтобы использовать это удобно и мгновенно определять текущее состояние приватности, напишем модуль waybar:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;custom/noscreenshare&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec&#34;</span><span class="p">:</span> <span class="s2">&#34;~/scripts/toggle_noscreenshare.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;on-click&#34;</span><span class="p">:</span> <span class="s2">&#34;~/scripts/toggle_noscreenshare.sh toggle &amp;&amp; pkill -SIGRTMIN+4 waybar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;signal&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;interval&#34;</span><span class="p">:</span> <span class="s2">&#34;once&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;return-type&#34;</span><span class="p">:</span> <span class="s2">&#34;json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{icon}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format-icons&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;off&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;on&#34;</span><span class="p">:</span> <span class="s2">&#34;󰗹&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;tooltip&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span>
</span></span></code></pre></div><p>Опишем, что здесь происходит. Чтобы получить значение для этого модуля, мы используем поле <code>exec</code> и указываем наш скрипт. Затем мы определяем, что произойдёт при клике на модуль в опции <code>on-click</code>: запуск скрипта с опцией <code>toggle</code> и отправка системного сигнала waybar. Это позволяет не перезагружать весь процесс waybar, а только этот модуль. Здесь очень важно синхронизировать значение <code>signal</code> и число в команде <code>pkill -SIGRTMIN+4 waybar</code>, и сделать это значение уникальным только для этого модуля waybar. Затем мы настраиваем однократный запуск нашего скрипта при старте waybar и определяем формат отображения (<code>tooltip: false</code> для отключения текста при наведении курсора). Я использую эти иконки nerdfonts для этого, но они не одинакового размера. Поэтому я тонко настраиваю отступы и добавляю некоторые стили:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">#</span><span class="nn">custom-noscreenshare</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="mh">#1e1e2e</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span> <span class="mi">5</span><span class="kt">px</span> <span class="mi">3</span><span class="kt">px</span> <span class="mi">0</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#181825</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">custom-noscreenshare</span><span class="p">.</span><span class="nc">on</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-size</span><span class="p">:</span> <span class="mi">18</span><span class="kt">px</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">  <span class="k">padding-right</span><span class="p">:</span> <span class="mi">14</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">custom-noscreenshare</span><span class="p">.</span><span class="nc">off</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">font-size</span><span class="p">:</span> <span class="mi">16</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding-right</span><span class="p">:</span> <span class="mi">16</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding-left</span><span class="p">:</span> <span class="mi">9</span><span class="kt">px</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>После этого мы получаем удобный интерфейс для практичного контроля нашей приватности:<br>
<img src="/images/noscreenshare-demo.gif" class="content-img" style="width:700px" alt="" loading="lazy" decoding="async" /></p>
]]></content:encoded><category>linux</category></item><item><title>Деградация батареи в Linux</title><link>https://alchemmist.xyz/ru/articles/battery-degradation/</link><pubDate>Fri, 11 Jul 2025 09:58:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/battery-degradation/</guid><description>Мы все знаем, что батарея не вечна. Это нормально — можно заменить изношенную батарею в сервисе и снова использовать устройство. Но очень полезно видеть текущее состояние батареи. В MacOS это стандартная функция, в Linux мы, конечно, можем сделать то же самое. Давайте разберёмся!
Сначала мы поймём, как найти нужное значение, а затем создадим красивый минималистичный модуль для waybar.
# Находим значение деградации батареи Сначала получим список всех устройств питания:</description><content:encoded><![CDATA[<p>Мы все знаем, что батарея не вечна. Это нормально — можно заменить изношенную батарею в сервисе и снова использовать устройство. Но очень полезно видеть текущее состояние батареи. В MacOS это стандартная функция, в Linux мы, конечно, можем сделать то же самое. Давайте разберёмся!</p>
<p>Сначала мы поймём, как найти нужное значение, а затем создадим красивый минималистичный модуль для waybar.</p>

<h2 id="находим-значение-деградации-батареи">
  <a class="link" href="#%d0%bd%d0%b0%d1%85%d0%be%d0%b4%d0%b8%d0%bc-%d0%b7%d0%bd%d0%b0%d1%87%d0%b5%d0%bd%d0%b8%d0%b5-%d0%b4%d0%b5%d0%b3%d1%80%d0%b0%d0%b4%d0%b0%d1%86%d0%b8%d0%b8-%d0%b1%d0%b0%d1%82%d0%b0%d1%80%d0%b5%d0%b8">
    #
  </a>
  Находим значение деградации батареи
</h2>

<p>Сначала получим список всех устройств питания:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">upower -e
</span></span></code></pre></div><p>Затем найдём батарею в этом списке:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">upower -e <span class="p">|</span> grep BAT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># For example:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /org/freedesktop/UPower/devices/battery_BATT</span>
</span></span></code></pre></div><p>Затем получим детальную информацию об этом устройстве:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">upower -i <span class="k">$(</span>upower -e <span class="p">|</span> grep BAT<span class="k">)</span>
</span></span></code></pre></div><p>Мы получим что-то вроде этого:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">  native-path:          BATT
</span></span><span class="line"><span class="cl">  vendor:               DESAY
</span></span><span class="line"><span class="cl">  model:                BASE-BAT
</span></span><span class="line"><span class="cl">  serial:               1
</span></span><span class="line"><span class="cl">  power supply:         yes
</span></span><span class="line"><span class="cl">  updated:              Fri Jul 11 11:37:00 2025 (7 seconds ago)
</span></span><span class="line"><span class="cl">  has history:          yes
</span></span><span class="line"><span class="cl">  has statistics:       yes
</span></span><span class="line"><span class="cl">  battery
</span></span><span class="line"><span class="cl">    present:             yes
</span></span><span class="line"><span class="cl">    rechargeable:        yes
</span></span><span class="line"><span class="cl">    state:               charging
</span></span><span class="line"><span class="cl">    warning-level:       none
</span></span><span class="line"><span class="cl">    energy:              25.514 Wh
</span></span><span class="line"><span class="cl">    energy-empty:        0 Wh
</span></span><span class="line"><span class="cl">    energy-full:         47.5052 Wh
</span></span><span class="line"><span class="cl">    energy-full-design:  59.4247 Wh
</span></span><span class="line"><span class="cl">    energy-rate:         11.1573 W
</span></span><span class="line"><span class="cl">    voltage:             11.972 V
</span></span><span class="line"><span class="cl">    charge-cycles:       650
</span></span><span class="line"><span class="cl">    time to full:        2.0 hours
</span></span><span class="line"><span class="cl">    percentage:          53%
</span></span><span class="line"><span class="cl">    capacity:            79.9417%
</span></span><span class="line"><span class="cl">    technology:          lithium-ion
</span></span><span class="line"><span class="cl">    charge-start-threshold:        75%
</span></span><span class="line"><span class="cl">    charge-end-threshold:          80%
</span></span><span class="line"><span class="cl">    charge-threshold-supported:    yes
</span></span><span class="line"><span class="cl">    icon-name:          &#39;battery-good-charging-symbolic&#39;
</span></span><span class="line"><span class="cl">  History (charge):
</span></span><span class="line"><span class="cl">    1752222927	53.000	charging
</span></span><span class="line"><span class="cl">  History (rate):
</span></span><span class="line"><span class="cl">    1752223020	11.157	charging
</span></span><span class="line"><span class="cl">    1752222990	11.123	charging
</span></span><span class="line"><span class="cl">    1752222987	11.192	charging
</span></span><span class="line"><span class="cl">    1752222957	10.822	charging
</span></span><span class="line"><span class="cl">    1752222927	10.857	charging
</span></span></code></pre></div><p>Здесь много интересной информации, например количество циклов зарядки, время до полной зарядки и т.д. Сейчас нас интересуют два параметра:</p>
<ul>
<li><code>energy-full-design</code> — изначальный полный объём батареи</li>
<li><code>energy-full</code> — текущий объём батареи</li>
</ul>
<p>Разница между этими параметрами и есть деградация батареи:
</p>
$$
\text{Degradation} = \left(1 - \frac{E_{\text{full}}}{E_{\text{design}}}\right) \times 100\%
$$<p>
Теперь посчитаем этот показатель в процентах.</p>
<p>Сначала найдём эти параметры и разделим их:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">upower -i <span class="k">$(</span>upower -e <span class="p">|</span> grep BAT<span class="k">)</span> <span class="p">|</span> awk <span class="s1">&#39;\
</span></span></span><span class="line"><span class="cl"><span class="s1">	/energy-full:/ {ef=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">	/energy-full-design:/ {efd=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">	END {print ef/efd}&#39;</span>
</span></span></code></pre></div><p>Затем посчитаем процентное значение:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;(1 - </span><span class="k">$(</span>upower -i <span class="k">$(</span>upower -e <span class="p">|</span> grep BAT<span class="k">)</span> <span class="p">|</span> awk <span class="s1">&#39;\
</span></span></span><span class="line"><span class="cl"><span class="s1">        /energy-full:/ {ef=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">        /energy-full-design:/ {efd=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">        END {print ef/efd}&#39;</span><span class="k">)</span><span class="s2">) * 100 + 0.5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">	<span class="p">|</span> bc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output example:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 20.558200</span>
</span></span></code></pre></div><p>На последнем шаге нам нужно округлить значение:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;(1 - </span><span class="k">$(</span>upower -i <span class="k">$(</span>upower -e <span class="p">|</span> grep BAT<span class="k">)</span> <span class="p">|</span> awk <span class="s1">&#39;\
</span></span></span><span class="line"><span class="cl"><span class="s1">        /energy-full:/ {ef=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">        /energy-full-design:/ {efd=$2}\
</span></span></span><span class="line"><span class="cl"><span class="s1">        END {print ef/efd}&#39;</span><span class="k">)</span><span class="s2">) * 100 + 0.5&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">	<span class="p">|</span> bc <span class="se">\
</span></span></span><span class="line"><span class="cl">	<span class="p">|</span> cut -d<span class="s1">&#39;.&#39;</span> -f1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output example:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 20</span>
</span></span></code></pre></div>
<h2 id="модуль-для-waybar">
  <a class="link" href="#%d0%bc%d0%be%d0%b4%d1%83%d0%bb%d1%8c-%d0%b4%d0%bb%d1%8f-waybar">
    #
  </a>
  Модуль для Waybar
</h2>

<p>В моём конфиге waybar <code>~/.config/waybar/config.json</code> я добавил два модуля в правую секцию, но вы можете сделать как вам удобно. В итоге я получил это:
<img
    src="/images/battery-waybar-module_hu_7adf571591eb9de4.webp"
    width="1600"
    height="1050"
    class="content-img"
    alt=""
    loading="lazy"
    decoding="async"
  /></p>
<blockquote class="markdown-blockquote">
  <p>Если вам нравятся мои обои, вы можете найти их и другие классные штуки в моём репозитории <code>dotfiles</code> на <a href="https://github.com/alchemmist/dotfiles/tree/main/wallpapers">GitHub</a>. Не забывайте: я люблю ваши звёзды!</p>

</blockquote>
<p>Посмотрим:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">...</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;modules-left&#34;</span><span class="p">:</span> <span class="p">[</span><span class="err">...</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;modules-center&#34;</span><span class="p">:</span> <span class="p">[</span><span class="err">...</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;modules-right&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;battery&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;custom/battery-degradation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="err">...</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="err">...</span>
</span></span></code></pre></div><p>Первый модуль — стандартный <code>battery</code>, у меня он отлично работает на hyprland. Здесь мы определяем уровни хорошего, предупреждающего и критического уровня заряда, задаём nerd-иконки для разных состояний батареи:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;battery&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;states&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;good&#34;</span><span class="p">:</span> <span class="mi">90</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;warning&#34;</span><span class="p">:</span> <span class="mi">25</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;critical&#34;</span><span class="p">:</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34;{icon} {capacity}%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format-charging&#34;</span><span class="p">:</span> <span class="s2">&#34; {capacity}%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format-plugged&#34;</span><span class="p">:</span> <span class="s2">&#34; {capacity}%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format-icons&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;󰂎&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁺&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁻&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁼&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁽&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁾&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁿&#34;</span><span class="p">,</span> <span class="s2">&#34;󰂀&#34;</span><span class="p">,</span> <span class="s2">&#34;󰂁&#34;</span><span class="p">,</span> <span class="s2">&#34;󰂂&#34;</span><span class="p">,</span> <span class="s2">&#34;󰁹&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;tooltip&#34;</span><span class="p">:</span> <span class="s2">&#34;{time}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;style&#34;</span><span class="p">:</span> <span class="s2">&#34;{capacity &lt; 10 ? &#39;color: red;&#39; : &#39;color: normal;&#39;}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span>
</span></span></code></pre></div><p>Второй модуль — тема сегодняшнего разговора:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;custom/battery-degradation&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;format&#34;</span><span class="p">:</span> <span class="s2">&#34; {}%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;interval&#34;</span><span class="p">:</span> <span class="s2">&#34;once&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec&#34;</span><span class="p">:</span> <span class="s2">&#34;~/scripts/battery-degradation.sh&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;tooltip&#34;</span><span class="p">:</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="err">,</span>
</span></span></code></pre></div><p>Я поместил нашу команду в скрипт — так удобнее. Вы можете сделать так же, не забудьте добавить shebang <code>#!/bin/bash</code> и:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">chmod +x ~/scripts/battery-degradation.sh
</span></span></code></pre></div><p>В модуле мы определяем формат с nerd-иконкой и процентом, отключаем подсказку и устанавливаем однократный запуск при старте waybar.</p>
<p>И завершаем эти модули CSS-стилем:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">#</span><span class="nn">battery</span><span class="o">,</span>
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">custom-battery-degradation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="mh">#1e1e2e</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">opacity</span><span class="p">:</span> <span class="mf">0.7</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin</span><span class="p">:</span> <span class="mi">3</span><span class="kt">px</span> <span class="mi">0</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin-top</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#181825</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">battery</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding-right</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">min-width</span><span class="p">:</span> <span class="mi">50</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">custom-battery-degradation</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">border-radius</span><span class="p">:</span> <span class="mi">0</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span> <span class="mi">0</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin-right</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">min-width</span><span class="p">:</span> <span class="mi">35</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">#</span><span class="nn">battery</span><span class="p">.</span><span class="nc">critical</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">color</span><span class="p">:</span> <span class="kc">red</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Готово! В результате:
<img
    src="/images/waybar-battery-module-2_hu_6854c9035f9f40e3.webp"
    width="151"
    height="41"
    class="content-img"
    alt=""
    loading="lazy"
    decoding="async"
  /></p>
]]></content:encoded><category>linux</category></item><item><title>Ортогонализация Грама-Шмидта</title><link>https://alchemmist.xyz/ru/articles/gram-schmidt/</link><pubDate>Sun, 06 Jul 2025 07:16:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/gram-schmidt/</guid><description>В большинстве практических задач нам требуется проекция вектора на подпространство. Если подпространство задано ортогональным или ортонормированным базисом, это можно сделать легко. Для ортогонализации векторов используется метод Грама-Шмидта.
Имеем исходные векторы $v_1, v_2, \dots, v_n$, и нам нужно получить ортогональные векторы $u_1, u_2, \dots, u_n$. Общая схема метода: 0. Удаляем все нулевые векторы.
Берём первый вектор без изменений: $u_1 = v_1$. Следующий вектор находим из $v_2$ и проекций на предыдущие векторы: $u_2 = v_2 - \text{Proj}_{u_1}{v_2}$. Повторяем: $u_3 = v_3 - \text{Proj}_{u_1}{v_3} - \text{Proj}_{u_2}{v_3}$. Продолжаем до вектора $u_n$: $u_n = v_n - \sum_{i=1}^{n-1}\text{Proj}_{u_i}{v_n}$. Напомним формулу проекции: $\text{Proj}_{u}{v} = \frac{\langle u, v\rangle}{||u||^2}u$. Для получения ортонормированного базиса нормируем векторы: $e_i = \frac{u_i}{||u_i||}$. Рассмотрим пример: $v_1 = \begin{pmatrix}1 \\ 1\end{pmatrix}, \ v_2 = \begin{pmatrix}1 \\ 0\end{pmatrix}$. Сначала задаём $u_1 = v_1$, затем: $u_2 = v_2 - \text{Proj}_{u_1}{v_2} = \begin{pmatrix}1 \\ 0\end{pmatrix} - \begin{pmatrix}0.5 \\ 0.5\end{pmatrix} = \begin{pmatrix}0.5 \\ -0.5\end{pmatrix}$. В итоге получаем ортогональные векторы: $u_1 = \begin{pmatrix}1 \\ 0\end{pmatrix}$, $u_2 = \begin{pmatrix}0.5 \\ -0.5\end{pmatrix}$.</description><content:encoded><![CDATA[<p>В большинстве практических задач нам требуется проекция вектора на подпространство. Если подпространство задано ортогональным или ортонормированным базисом, это можно сделать легко. Для ортогонализации векторов используется метод Грама-Шмидта.</p>
<p>Имеем исходные векторы $v_1, v_2, \dots, v_n$, и нам нужно получить ортогональные векторы $u_1, u_2, \dots, u_n$. Общая схема метода:
0. Удаляем все нулевые векторы.</p>
<ol>
<li>Берём первый вектор без изменений: $u_1 = v_1$.</li>
<li>Следующий вектор находим из $v_2$ и проекций на предыдущие векторы: $u_2 = v_2 - \text{Proj}_{u_1}{v_2}$.</li>
<li>Повторяем: $u_3 = v_3 - \text{Proj}_{u_1}{v_3} - \text{Proj}_{u_2}{v_3}$.</li>
<li>Продолжаем до вектора $u_n$: $u_n = v_n - \sum_{i=1}^{n-1}\text{Proj}_{u_i}{v_n}$.
Напомним формулу проекции: $\text{Proj}_{u}{v} = \frac{\langle u, v\rangle}{||u||^2}u$. Для получения ортонормированного базиса нормируем векторы: $e_i = \frac{u_i}{||u_i||}$.</li>
</ol>
<p>Рассмотрим пример: $v_1 = \begin{pmatrix}1 \\ 1\end{pmatrix}, \ v_2 = \begin{pmatrix}1 \\ 0\end{pmatrix}$.
Сначала задаём $u_1 = v_1$, затем: $u_2 = v_2 - \text{Proj}_{u_1}{v_2} = \begin{pmatrix}1 \\ 0\end{pmatrix} - \begin{pmatrix}0.5 \\ 0.5\end{pmatrix} = \begin{pmatrix}0.5 \\ -0.5\end{pmatrix}$.
В итоге получаем ортогональные векторы: $u_1 = \begin{pmatrix}1 \\ 0\end{pmatrix}$, $u_2 = \begin{pmatrix}0.5 \\ -0.5\end{pmatrix}$.</p>
]]></content:encoded><category>math</category></item><item><title>Стационарные точки и метод Лагранжа</title><link>https://alchemmist.xyz/ru/articles/stationary-points-and-lagrange/</link><pubDate>Sun, 06 Jul 2025 05:19:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/stationary-points-and-lagrange/</guid><description>При анализе функции мы ищем стационарные точки. Без ограничений используем градиент — вектор частных производных: $$ \nabla f = \begin{pmatrix} \displaystyle \frac{\partial f}{\partial x_1} \\ \displaystyle \frac{\partial f}{\partial x_2} \\ \dots \\ \displaystyle \frac{\partial f}{\partial x_n} \end{pmatrix} $$ Пример: $f(x, y, z) = x^2 + y^2 - 4x - 4y + z^4 - 4z^2$. Градиент: $$ \nabla f = \begin{pmatrix} \displaystyle \frac{\partial f}{\partial x} \\ \displaystyle \frac{\partial f}{\partial y} \\ \displaystyle \frac{\partial f}{\partial z} \end{pmatrix} = \begin{pmatrix} 2x - 4 \\ 2y - 4 \\ 4z^3 - 8z \end{pmatrix} $$ Приравниваем к нулю: $\nabla f = 0$, получаем систему:</description><content:encoded><![CDATA[<p>При анализе функции мы ищем стационарные точки. Без ограничений используем градиент — вектор частных производных:
</p>
$$
\nabla f = \begin{pmatrix}
\displaystyle \frac{\partial f}{\partial x_1} \\
\displaystyle \frac{\partial f}{\partial x_2} \\
\dots \\
\displaystyle \frac{\partial f}{\partial x_n}
\end{pmatrix}
$$<p>
Пример: $f(x, y, z) = x^2 + y^2 - 4x - 4y + z^4 - 4z^2$. Градиент:
</p>
$$
\nabla f = \begin{pmatrix}
\displaystyle \frac{\partial f}{\partial x} \\
\displaystyle \frac{\partial f}{\partial y} \\
\displaystyle \frac{\partial f}{\partial z}
\end{pmatrix} =  \begin{pmatrix}
2x - 4 \\
2y - 4 \\
4z^3 - 8z
\end{pmatrix}
$$<p>
Приравниваем к нулю: $\nabla f = 0$, получаем систему:
</p>
$$
\begin{cases}2x - 4 = 0 \\ 2y - 4 = 0 \\ 4z^3 - 8z = 0 \end{cases} \quad \Rightarrow \quad \begin{cases}x = 2\\ y = 2 \\ \left[
\begin{array}{l}
z = 0 \\
z = \sqrt{2} \\
z = -\sqrt{2}
\end{array}
\right.\end{cases}
$$<p>
Стационарные точки: $(2, 2, 0), \ (2, 2, \sqrt{2}), \ (2, 2, -\sqrt{2})$.</p>
<blockquote class="markdown-blockquote">
  <p>Для дальнейшего анализа используйте <a href="/ru/articles/hessian-matrix/">матрицу Гессе</a>.</p>

</blockquote>
<p>Для задач с <strong>ограничениями</strong> применяем <strong>метод множителей Лагранжа</strong>. Пример: $f(x, y) = x^2 + y^2$ при условии $x + y = 1$. Перепишем ограничение как $g(x, y) = x + y - 1 = 0$. Градиенты:
</p>
$$
\nabla f = \begin{pmatrix}2x \\ 2y\end{pmatrix}, \quad \nabla g = \begin{pmatrix}1 \\ 1\end{pmatrix}
$$<p>
Уравнения Лагранжа в общем виде:
</p>
$$
\begin{cases}
\displaystyle \frac{\partial f}{\partial x_1} = \lambda \frac{\partial g}{\partial x_1} \\
\displaystyle \frac{\partial f}{\partial x_2} = \lambda \frac{\partial g}{\partial x_2} \\
\quad \dots \\
\displaystyle \frac{\partial f}{\partial x_n} = \lambda \frac{\partial g}{\partial x_n} \\
g(x_1, x_2, \dots x_n) = 0
\end{cases}
$$<p>
Для нашего случая:
</p>
$$
\begin{cases}2x = \lambda \cdot 1\\2y = \lambda \cdot 1\\x + y = 1\end{cases} \quad \Rightarrow \quad \begin{cases}\lambda = 2x\\\lambda = 2y\\x + y = 1\end{cases} \quad \Rightarrow \quad \begin{cases}x = y\\2x = 1\end{cases}\quad \Rightarrow \quad x = y = \frac{1}{2}
$$<p>
Стационарная точка: $\left(\frac{1}{2}, \frac{1}{2} \right)$. Итог: без ограничений решаем $\nabla f = 0$, с ограничениями — метод Лагранжа.```</p>
]]></content:encoded><category>math</category></item><item><title>Матрица Гессе</title><link>https://alchemmist.xyz/ru/articles/hessian-matrix/</link><pubDate>Sat, 05 Jul 2025 12:48:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/hessian-matrix/</guid><description>Предположим, у нас есть функция и несколько её стационарных точек, которые нужно классифицировать (максимум, минимум, седловая точка). Для функции без ограничений используем матрицу Гессе.
Как найти стационарные точки, читайте в статье: “Стационарные точки и метод Лагранжа”
Разберём на примере. Функция и стационарные точки: $$ f(x, y) = x^3 + y^3 - 3xy; \quad A = (0; 0), \ B = (1; 1) $$ Сначала построим матрицу Гессе. В общем виде она выглядит так:</description><content:encoded><![CDATA[<p>Предположим, у нас есть функция и несколько её стационарных точек, которые нужно классифицировать (максимум, минимум, седловая точка). Для функции без ограничений используем матрицу Гессе.</p>
<blockquote class="markdown-blockquote">
  <p>Как найти стационарные точки, читайте в статье: &ldquo;<a href="/ru/articles/stationary-points-and-lagrange/">Стационарные точки и метод Лагранжа</a>&rdquo;</p>

</blockquote>
<p>Разберём на примере. Функция и стационарные точки:
</p>
$$
f(x, y) = x^3 + y^3 - 3xy; \quad A = (0; 0), \ B = (1; 1)
$$<p>
Сначала построим матрицу Гессе. В общем виде она выглядит так:
</p>
$$
H_f(x) =
\begin{bmatrix}
\displaystyle \frac{\partial^2 f}{\partial x_1^2} & \displaystyle \frac{\partial^2 f}{\partial x_1 \partial x_2} & \cdots & \displaystyle \frac{\partial^2 f}{\partial x_1 \partial x_n} \\
\displaystyle \frac{\partial^2 f}{\partial x_2 \partial x_1} & \displaystyle \frac{\partial^2 f}{\partial x_2^2} & \cdots & \displaystyle \frac{\partial^2 f}{\partial x_2 \partial x_n} \\
\vdots & \vdots & \ddots & \vdots \\
\displaystyle \frac{\partial^2 f}{\partial x_n \partial x_1} & \displaystyle \frac{\partial^2 f}{\partial x_n \partial x_2} & \cdots & \displaystyle \frac{\partial^2 f}{\partial x_n^2}
\end{bmatrix}
$$<p>
Для нашей функции: $H = \begin{bmatrix}6x & -3 \\-3 & 6y\end{bmatrix}$. Подставим точки в матрицу и найдём собственные значения:
</p>
$$
\begin{align}
&H_A = \begin{bmatrix}0 & -3 \\ -3 & 0\end{bmatrix} 
&&H_B = \begin{bmatrix}6 & -3 \\ -3 & 6\end{bmatrix} 
\\[1em]
&\det(H_A - \lambda I) = 
\det \begin{bmatrix}
0 - \lambda & -3 \\
-3 & 0 - \lambda
\end{bmatrix} =
&&\det(H_B - \lambda I) = \det \begin{bmatrix}
6 - \lambda & -3 \\
-3 & 6 - \lambda
\end{bmatrix} =
\\
&= (-\lambda)(-\lambda) - (-3)(-3) = \lambda^2 - 9 = 0 \ \Rightarrow \quad \quad \quad \quad
&&= (6-\lambda)^2 - 9 = 0 \ \Rightarrow
\\[1em]
&\Rightarrow \ \lambda^2 = 9 \ \Rightarrow \  \lambda = \pm 3 \ \Rightarrow
&&\Rightarrow \ (6-\lambda)^2 = 9 \ \Rightarrow \ 6 - \lambda = \pm 3 \ \Rightarrow
\\[1em]
&\Rightarrow \lambda_1 = 3, \quad \lambda_2 = -3
&&\Rightarrow \lambda_1 = 6 - 3 = 3, \quad \lambda_2 = 6 + 3 = 9
\end{align}
$$<p>
Применяем правила:</p>
<ul>
<li>Если все собственные значения положительны — локальный минимум</li>
<li>Если все отрицательны — локальный максимум</li>
<li>Если знаки разные — седловая точка</li>
</ul>
<p>Результат: точка $A$ — седловая, точка $B$ — локальный минимум.```</p>
]]></content:encoded><category>math</category></item><item><title>Подпространства</title><link>https://alchemmist.xyz/ru/articles/subspaces/</link><pubDate>Thu, 03 Jul 2025 10:42:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/subspaces/</guid><description>Начнём с полезного определения: Ненулевые векторы $u, v \in \mathbb{R}$ называются коллинеарными векторами, если они пропорциональны друг другу, то есть существует $\lambda \in \mathbb{R}$ такое, что $u = \lambda v$. Коллинеарные векторы обозначаются как $u || v$. Нулевой вектор коллинеарен любому вектору. Геометрически векторы коллинеарны, если они имеют одинаковое или противоположное направление.
Рассмотрим плоскость $\pi$ в пространстве $\mathbb{R}^3$, проходящую через начало координат. Возьмём два неколлинеарных вектора $u$ и $v$ из этой плоскости. Тогда любую точку $x \in \pi$ можно представить как: $x = \alpha u + \beta v$.</description><content:encoded><![CDATA[<p>Начнём с полезного определения: Ненулевые векторы $u, v \in \mathbb{R}$ называются <strong>коллинеарными векторами</strong>, если они пропорциональны друг другу, то есть существует $\lambda \in \mathbb{R}$ такое, что $u = \lambda v$. Коллинеарные векторы обозначаются как $u || v$. Нулевой вектор коллинеарен любому вектору. Геометрически векторы коллинеарны, если они имеют одинаковое или противоположное направление.</p>
<p>Рассмотрим плоскость $\pi$ в пространстве $\mathbb{R}^3$, проходящую через начало координат. Возьмём два неколлинеарных вектора $u$ и $v$ из этой плоскости. Тогда любую точку $x \in \pi$ можно представить как: $x = \alpha u + \beta v$.</p>
<p><img
    src="/images/two-vectors-on-plane_hu_f81c1a3af72a6417.webp"
    width="1600"
    height="883"
    class="content-img" style="width:400px"
    alt=""
    loading="lazy"
    decoding="async"
  />
Хорошо, это означает, что мы можем определить плоскость $\pi$ как:
</p>
$$
\pi = \{\text{all vectors }\  x \ \text { represented as }\ x = \alpha u + \beta v\}
$$<p>
Обобщим понятие плоскости, проходящей через начало координат, на пространство произвольной размерности:
<strong>Линейной оболочкой</strong> векторов $v_1, v_2, \dots, v_k \ \in \ \mathbb{R}^n$ называется множество векторов, представимых в виде суммы векторов $v_1, v_2, \dots, v_k$ с произвольными коэффициентами:
</p>
$$
P = \text{span}{(v_1, v_2, \dots, v_k)} = \{ x: \ x = \alpha_1 v_1 + \alpha_2 v_2 + \dots + \alpha_k v_k, \ \alpha_i \in \mathbb{R} \}.
$$<p>
Выражение $\alpha_1 v_1 + \alpha_2 v_2 + \dots + \alpha_k v_k$ будем называть линейной комбинацией векторов $v_1, v_2, \dots, v_k$.</p>
<p>Сформулируем ключевое определение: <strong>линейным подпространством</strong> $\mathbb{R}^n$ называется линейная оболочка конечного числа векторов $v_1, v_2, \dots v_k \ \in \mathbb{R}^n$:
</p>
$$
V = \text{span}(v_1, v_2, \dots, v_k)
$$<p>
Понятия линейной оболочки и линейного подпространства эквивалентны. Любая линейная оболочка и любое подпространство содержат $0$, поскольку мы можем положить все коэффициенты $\alpha_1, \alpha_2, \dots, \alpha_k$ равными нулю.</p>
<p>Почему линейные подпространства важны? Вот пример:</p>
<blockquote class="markdown-blockquote">
  <p>Возьмём чёрно-белое цифровое фото размером 512 на 512 пикселей. В памяти компьютера это фото может быть представлено как $512 \times 512 = 262144$ чисел — каждое представляет интенсивность соответствующего пикселя от 0 (полностью чёрный) до 1 (полностью белый). Таким образом, фото может быть представлено вектором $x$ в пространстве размерности $d=262144$.</p>
<p>Теперь рассмотрим важную практическую задачу — сжатие фото без значительных потерь. Оказывается, эту проблему можно решить, найдя специальное низкоразмерное подпространство и спроецировав вектор $x$ на это подпространство.</p>

</blockquote>
<p>Мы говорили, что подпространство должно содержать ноль. Это означает, что прямая $y = 2x$ является подпространством, а прямая $y = x + 2$ — нет. Но многие свойства подпространств сохраняются, даже когда они не содержат ноль. Такие подпространства называются аффинными подпространствами. <strong>Аффинным подпространством</strong> $\mathbb{R}^n$ называется множество векторов $A$ вида $a + v$, где $a \in \mathbb{R}^n$ — фиксированный вектор, а вектор $v$ принадлежит линейному подпространству $V$. Пусть линейное подпространство $V$ — это линейная оболочка векторов $v_1, v_2, \dots, v_k$. Тогда аффинное подпространство — это множество:
</p>
$$
A = \{ x: \ x = a + \alpha_1 v_1 + \alpha_2 v_2 + \dots + \alpha_k v_k, \ \alpha_i \in \mathbb{R}\}
$$<p>
Пусть $V$ — подпространство в $\mathbb{R}^n$. <strong>Размерностью</strong> этого подпространства называется минимальное количество векторов $v_1, v_2, \dots, v_k \in \mathbb{R}^n$, необходимых для определения подпространства $V$:
</p>
$$
V = \text{span}(v_1, v_2, \dots, v_k).
$$<p>
Размерность обозначается как: $\dim{(V)}$.
Подумаем об этом. Например, у нас есть два неколлинеарных вектора $u, v \in \mathbb{R}^{100}$ и подпространство $V = \text{span}(u, v)$. Мы знаем, что каждый вектор $x \in V$ имеет 100 координат, но по определению подпространства: $x = \alpha u + \beta v$. Это означает, что любой вектор $x$ определяется всего двумя параметрами $\alpha$ и $\beta$, следовательно $\dim{(V)} = 2$. Для определения точки в $\mathbb{R}^n$ нам нужно $n$ параметров, для определения точки в $V$ нам нужно $\dim{(V)}$ параметров.</p>
<p><img
    src="/images/define-subspace_hu_d2ba0211e5bef597.webp"
    width="1600"
    height="497"
    class="content-img" style="width:800px"
    alt=""
    loading="lazy"
    decoding="async"
  /></p>
<p>Существует подпространство, определяемое только одним вектором $0$: $V = \text{span}(0)$. Мы называем это подпространство тривиальным и считаем $\dim{(V)} = 0$.</p>
<p>Если у нас есть подпространство $V = \text{span}(v_1, v_2, \dots, v_k)$, может ли $\dim{(V)}$ быть меньше $k$? Да, если и только если один из векторов может быть выражен как линейная комбинация остальных векторов, например: $v_1 = \gamma_2v_2 + \gamma_3v_3 + \dots + \gamma_kv_k$.</p>
<p>Набор векторов называется <strong>линейно независимым</strong>, если равенство:
</p>
$$
\alpha_1 v_1 + \alpha_2 v_2 + \dots + \alpha_k v_k = 0
$$<p>
выполняется только при $\alpha_1 = \alpha_2 = \dots = \alpha_k = 0$. Если равенство может выполняться при ненулевых коэффициентах, векторы <strong>линейно зависимы</strong>.</p>
<p>Рассмотрим пространство $\mathbb{R}^n$ и заданный ненулевой вектор $w \in \mathbb{R}^n$. Тогда множество векторов $P$, перпендикулярных $w$:
</p>
$$
P = \{ \ x: \langle x;w \rangle = 0\ \}
$$<p>
является линейным подпространством с $\dim{(P)} = n - 1$</p>
]]></content:encoded><category>math</category></item><item><title>Прямая и плоскость</title><link>https://alchemmist.xyz/ru/articles/line-and-plane/</link><pubDate>Sat, 28 Jun 2025 17:18:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/line-and-plane/</guid><description>Линейная алгебра начинается с базовой геометрической интуиции, которая распространяется на многомерные пространства. Сейчас мы поговорим о прямой, плоскости и пространстве.
# Прямая на плоскости и в пространстве Как задать прямую на плоскости? Конечно, через две точки. Но это не всё. Существуют ещё три метода:
Параметрическое уравнение
Пусть прямая $l$ задана двумя точками $r_0 = (x_0; y_0)$ и $r_1 = (x_1;y_1)$. Заметим, что вектор $a = r_0 - r_1$ направлен вдоль $l$. Следовательно, любая точка на $l$ может быть определена как $r = r_0 + ta$, где $t \in \mathbb{R}$, а вся прямая $l$ задаётся при $-\infty &amp;lt; t &amp;lt; \infty$. Это выражение мы называем параметрическим уравнением:</description><content:encoded><![CDATA[<p>Линейная алгебра начинается с базовой геометрической интуиции, которая распространяется на многомерные пространства. Сейчас мы поговорим о прямой, плоскости и пространстве.</p>

<h2 id="прямая-на-плоскости-и-в-пространстве">
  <a class="link" href="#%d0%bf%d1%80%d1%8f%d0%bc%d0%b0%d1%8f-%d0%bd%d0%b0-%d0%bf%d0%bb%d0%be%d1%81%d0%ba%d0%be%d1%81%d1%82%d0%b8-%d0%b8-%d0%b2-%d0%bf%d1%80%d0%be%d1%81%d1%82%d1%80%d0%b0%d0%bd%d1%81%d1%82%d0%b2%d0%b5">
    #
  </a>
  Прямая на плоскости и в пространстве
</h2>

<p>Как задать прямую на плоскости? Конечно, через две точки. Но это не всё. Существуют ещё три метода:</p>
<ol>
<li>
<p><strong>Параметрическое уравнение</strong><br>
Пусть прямая $l$ задана двумя точками $r_0 = (x_0; y_0)$ и $r_1 = (x_1;y_1)$. Заметим, что вектор $a = r_0 - r_1$ направлен вдоль $l$. Следовательно, любая точка на $l$ может быть определена как $r = r_0 + ta$, где $t \in \mathbb{R}$, а вся прямая $l$ задаётся при $-\infty < t < \infty$. Это выражение мы называем <em>параметрическим уравнением</em>:<br>
</p>
$$
l = \{r: r = r_o + at, \ t \in \mathbb{R} \}
$$</li>
<li>
<p><strong>Линейное уравнение</strong><br>
Запишем параметрическое уравнение через координаты:<br>
</p>
$$
\begin{bmatrix}x \\ y\end{bmatrix} = \begin{bmatrix}x_0\\y_0\end{bmatrix} + \begin{bmatrix}t \cdot a_1 \\ t \cdot a_2\end{bmatrix} \Rightarrow \begin{cases}x = x_0 + ta_1 \\ y = y_0 + ta_2\end{cases}
$$<p>
Предположим, что $a_1 \neq 0$ и $a_2 \neq 0$, и выразим $t$ из каждого уравнения: $\frac{x - x_0}{a_1} = \frac{y - y_0}{a_2} \Rightarrow a_2x - a_1y + (a_1y_0 - a_2x_0) = 0$. Поскольку $a_1, a_2, x_0, x_1$ — числа, это означает, что мы можем определить его как линейное уравнение:<br>
</p>
$$
Ax + By + C = 0
$$<p>
где $A = a_2$, $B = -a_1$, $C = (a_1y_0 - a_2x_0)$</p>
</li>
<li>
<p><strong>Нормаль и точка</strong><br>
Последний метод. Выберем произвольную точку на прямой $l$ с радиус-вектором $r_0$. Проведём вектор $n$ из точки $r_0$, перпендикулярный прямой $l$. Тогда для любой точки $r$ на прямой $l$ векторы $r - r_0$ и $n$ будут перпендикулярны. Перепишем это через скалярное произведение:<br>
</p>
$$
l = \{ r: \langle r - r_0;n\rangle = 0\}
$$<p>
Разумеется, $r_0$ может быть любой точкой на прямой $l$, а нормальный вектор $n$ — любым вектором, перпендикулярным прямой $l$. Чтобы найти нормаль к прямой, нам нужен направляющий вектор $a = \begin{pmatrix}a_1 \\ a_2\end{pmatrix}$ из <em>параметрического уравнения</em>, тогда нормальный вектор будет: $n = \begin{bmatrix}-a_2\\ a_1\end{bmatrix}$ или $n’ = -n = \begin{bmatrix} a_2 \\ -a_1\end{bmatrix}$</p>
</li>
</ol>
<p>Подведём итог и посмотрим на взаимосвязь между этими методами. Если прямая проходит через две точки $r_0 = (x_0; y_0)$ и $r_1 = (x_1; y_1)$, то её можно определить любым из трёх методов:</p>
<ul>
<li>Параметрическое уравнение:<br>

$$
r = r_0 + ta = \begin{bmatrix}x_0\\y_0\end{bmatrix} + t \cdot \begin{bmatrix}x_1 - x_0 \\ y_1 - y_0\end{bmatrix} = \begin{bmatrix}x_0\\y_0\end{bmatrix} + \begin{bmatrix}t \cdot a_1 \\ t \cdot a_2\end{bmatrix}
$$</li>
<li>Линейное уравнение:<br>

$$
Ax + By + C = 
\underbrace{a_2}_{A} x + 
\underbrace{(-a_1)}_{B} y + 
\underbrace{(a_1 y_0 - a_2 x_0)}_{C} = 0;
$$</li>
<li>Нормаль и точка:<br>

$$
\langle r - r_0; n\rangle = \left\langle \begin{bmatrix}x - x_0\\y - y_0\end{bmatrix}; \begin{bmatrix}a_1 \\ -a_1\end{bmatrix}\right\rangle = \left\langle\begin{bmatrix}x - x_0 \\ y - y_0\end{bmatrix}; \begin{bmatrix}A \\ B\end{bmatrix} \right\rangle
$$</li>
</ul>
<p>Как насчёт прямой в пространстве? Параметрическое уравнение не меняется, векторы становятся длиннее. Линейное уравнение изменится: теперь это не одно уравнение, а система двух уравнений:<br>
</p>
$$
\begin{cases}
a_2x - a_1y + (a_1y_0 - a_2x_0) = 0 \\
a_3y - a_2z + (a_2z_0 - a_3y_0) = 0
\end{cases}
$$<p>
Метод с нормалью и точкой <strong>не работает в пространстве</strong>!</p>
<p>Дополнение: рассмотрим <strong>расстояние от точки до прямой</strong>:<br>
</p>
$$
d(P, l) = \frac{|Ax_0 + By_0 + C|}{\sqrt{A^2 + B^2}}
$$<p>
Мы можем определить, где находится точка относительно прямой: группа точек с одной стороны, для которых верно $Ax +By + C > 0$; группа с другой стороны, для которых верно $Ax +By + C < 0$.</p>

<h2 id="плоскость-в-пространстве">
  <a class="link" href="#%d0%bf%d0%bb%d0%be%d1%81%d0%ba%d0%be%d1%81%d1%82%d1%8c-%d0%b2-%d0%bf%d1%80%d0%be%d1%81%d1%82%d1%80%d0%b0%d0%bd%d1%81%d1%82%d0%b2%d0%b5">
    #
  </a>
  Плоскость в пространстве
</h2>

<p>Снова этот вопрос. Как задать плоскость в пространстве? Конечно, через три неколлинеарные точки, не лежащие на одной прямой. <strong>Коллинеарные точки</strong> лежат на одной прямой. И снова рассмотрим три метода:</p>
<ol>
<li><strong>Параметрическое уравнение</strong>:<br>

$$
\pi = \{r: r = r_0 + t_1v_1 + t_2v_2, \quad t_1, t_2 \in \mathbb{R}\}
$$</li>
<li><strong>Нормаль и точка</strong>: такой же, как для прямой: $\langle r - r_0; n\rangle = 0$</li>
<li><strong>Линейное уравнение</strong>: начнём от нормали и точки и развернём его:<br>

$$
\langle r - r_0; n\rangle = 0 \quad \Rightarrow \quad 
\underbrace{n_1}_{A} x\  + \ 
\underbrace{n_2}_{B} y\  + \
\underbrace{n_3}_{C} z \ 
\underbrace{- \langle r_0 ;n\rangle}_{D} = 0. \ \Rightarrow \ Ax + By + Cz + D = 0
$$</li>
</ol>
<p>Посмотрим на взаимосвязь между этими методами:</p>
<ul>
<li>Чтобы переписать параметрическое уравнение в нормаль и точку, нужно найти нормальный вектор из системы уравнений:  $\begin{cases}\langle n; v_1\rangle = 0 \\ \langle n; v_2\rangle = 0\end{cases}$</li>
<li>Чтобы переписать форму нормали и точки в линейное уравнение, мы можем использовать: $A = n_1$, $B = n_2$, $C = n_3$, $D = -\langle n ;r_0\rangle$</li>
<li>Если у нас есть три точки, определяющие плоскость, мы можем перейти к параметрическому уравнению: $\begin{array}a v_1 = r_1 - r_0\\ v_2 = r_2 - r_0\end{array}$</li>
</ul>
<p>Проекцией точки $P$ на плоскость $\pi$ называется точка $P_\pi \in \pi$, ближайшая к точке $P$: $P_\pi = \arg{\min{||P - X||}}, \ X \in \pi$. Тогда расстояние от точки $P$ до плоскости $\pi$ будет $d(P, \pi)$ — расстояние между точкой $P$ и её проекцией $P_\pi$:<br>
</p>
$$
d(P, \pi) = \frac{|Ax_0 + By_0 + Cz_0 + D|}{\sqrt{A^2 + B^2 + C^2}}
$$<p>
Определение расположения точки относительно плоскости такое же, как для прямой.</p>
]]></content:encoded><category>math</category></item><item><title>Основы многомерных векторов</title><link>https://alchemmist.xyz/ru/articles/multivariate-vectors/</link><pubDate>Thu, 26 Jun 2025 14:18:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/multivariate-vectors/</guid><description>Что такое вектор? Для натурального числа $n$ назовём вектором упорядоченный список из $n$ действительных чисел $v = \begin{pmatrix}v_1&amp;amp;v_2&amp;amp;v_3&amp;amp;\ldots&amp;amp;v_n\end{pmatrix}$. Число $n$ называется размерностью вектора: $\dim{v} = n$. Множество всех возможных векторов размерности $n$ обозначается $R^n$. Обычные числа называются “скалярами”. Например, $5$ — скаляр, а $\begin{pmatrix}1&amp;amp;3\end{pmatrix}$ — вектор.
# Базовые операции с векторами Сумма векторов — сумма соответствующих координат: $$ v + w = \begin{pmatrix}v_1 \\ v_2 \\ \ldots \\ v_n \end{pmatrix} + \begin{pmatrix}w_1 \\ w_2 \\ \ldots \\ w_n \end{pmatrix} = \begin{pmatrix}v_1 + w_1 \\ v_2 + w_2 \\ \ldots \\ v_n + w_n \end{pmatrix} $$ Складывать можно только векторы одинаковой размерности. Умножение вектора на скаляр: $$ \lambda v = \lambda \cdot \begin{pmatrix}v_1 \\ v_2 \\ \ldots \\ v_n \end{pmatrix} = \begin{pmatrix}\lambda v_1 \\ \lambda v_2 \\ \ldots \\ \lambda v_n \end{pmatrix} $$ Разность векторов — сумма векторов, где один умножен на скаляр $-1$. Скалярное произведение векторов: $$ x \cdot y = \langle x;y\rangle = x_1 y_1 + x_2 y_2 + \ldots + x_n y_n = \sum \limits_{i = 1}^{n}x_i y_i $$ Свойства скалярного произведения: Коммутативность: $\langle v; w\rangle = \langle w; v\rangle$ Дистрибутивность: $\langle x; (v + w)\rangle = \langle x; v\rangle + \langle x; w \rangle$ Совместимость со скалярным умножением: $\langle v; \lambda w\rangle = \lambda \langle v;w\rangle$ Если скалярное произведение равно нулю, векторы называются ортогональными # Геометрическая интерпретация В геометрии вектор — направленный отрезок.</description><content:encoded><![CDATA[<p>Что такое вектор? Для натурального числа $n$ назовём <strong>вектором</strong> упорядоченный список из $n$ действительных чисел $v = \begin{pmatrix}v_1&v_2&v_3&\ldots&v_n\end{pmatrix}$. Число $n$ называется размерностью вектора: $\dim{v} = n$. Множество всех возможных векторов размерности $n$ обозначается $R^n$.
Обычные числа называются &ldquo;скалярами&rdquo;. Например, $5$ — скаляр, а $\begin{pmatrix}1&3\end{pmatrix}$ — вектор.</p>

<h2 id="базовые-операции-с-векторами">
  <a class="link" href="#%d0%b1%d0%b0%d0%b7%d0%be%d0%b2%d1%8b%d0%b5-%d0%be%d0%bf%d0%b5%d1%80%d0%b0%d1%86%d0%b8%d0%b8-%d1%81-%d0%b2%d0%b5%d0%ba%d1%82%d0%be%d1%80%d0%b0%d0%bc%d0%b8">
    #
  </a>
  Базовые операции с векторами
</h2>

<ol>
<li>Сумма векторов — сумма соответствующих координат:

$$
v + w = \begin{pmatrix}v_1 \\ v_2 \\ \ldots \\ v_n \end{pmatrix} + \begin{pmatrix}w_1 \\ w_2 \\ \ldots \\ w_n \end{pmatrix} = \begin{pmatrix}v_1 + w_1 \\ v_2 + w_2 \\ \ldots \\ v_n + w_n \end{pmatrix}
$$
Складывать можно только векторы одинаковой размерности.</li>
<li>Умножение вектора на скаляр:

$$
\lambda v = \lambda \cdot \begin{pmatrix}v_1 \\ v_2 \\ \ldots \\ v_n \end{pmatrix} = \begin{pmatrix}\lambda v_1 \\ \lambda v_2 \\ \ldots \\ \lambda v_n \end{pmatrix}
$$</li>
<li>Разность векторов — сумма векторов, где один умножен на скаляр $-1$.</li>
<li>Скалярное произведение векторов:

$$
x \cdot y = \langle x;y\rangle = x_1 y_1 + x_2 y_2 + \ldots + x_n y_n = \sum \limits_{i = 1}^{n}x_i y_i
$$
Свойства скалярного произведения:</li>
</ol>
<ul>
<li>Коммутативность: $\langle v; w\rangle = \langle w; v\rangle$</li>
<li>Дистрибутивность: $\langle x; (v + w)\rangle = \langle x; v\rangle + \langle x; w \rangle$</li>
<li>Совместимость со скалярным умножением: $\langle v; \lambda w\rangle = \lambda \langle v;w\rangle$</li>
<li>Если скалярное произведение равно нулю, векторы называются <strong>ортогональными</strong></li>
</ul>

<h2 id="геометрическая-интерпретация">
  <a class="link" href="#%d0%b3%d0%b5%d0%be%d0%bc%d0%b5%d1%82%d1%80%d0%b8%d1%87%d0%b5%d1%81%d0%ba%d0%b0%d1%8f-%d0%b8%d0%bd%d1%82%d0%b5%d1%80%d0%bf%d1%80%d0%b5%d1%82%d0%b0%d1%86%d0%b8%d1%8f">
    #
  </a>
  Геометрическая интерпретация
</h2>

<p>В геометрии вектор — направленный отрезок.
<img
    src="/images/the-vector_hu_3dba13051fbe9e9b.webp"
    width="984"
    height="628"
    class="content-img" style="width:400px"
    alt=""
    loading="lazy"
    decoding="async"
  /></p>
<p><strong>Длина</strong> вектора вычисляется по теореме Пифагора: ФОРМУЛА_4. Вектор единичной длины называется <strong>единичным вектором</strong>. Ненормированный вектор можно нормировать умножением на $\lambda = \frac{1}{||v||}$.
<strong>Расстояние</strong> между векторами: $d = ||v - w|| = \sqrt{\sum\limits_{i = 1}^{n}{(v_i - w_i)^2}}$
<strong>Угол</strong> между векторами:
</p>
$$
\cos{\theta} = \frac{\langle u;b \rangle}{||u||\ ||v||}
$$<p>
<strong>Проекция</strong> вектора $u$ на $w$:
</p>
$$
Proj_w(u) = \frac{\langle u;w\rangle}{||w||^2}w
$$<p><strong>Неравенство Коши-Буняковского</strong>:
</p>
$$
-||v||\ ||w|| \leqslant \langle v;w\rangle \leqslant ||v||\ ||w||
$$<p>
<strong>Неравенство треугольника</strong>: $||v + w|| \leqslant ||v|| + ||w||$<br>
Для ортогональных векторов выполняется теорема Пифагора: $||a + b||^2 = ||a||^2 + ||b||^2$.```</p>
]]></content:encoded><category>math</category></item><item><title>Прозрачное развертывание с GitHub Actions</title><link>https://alchemmist.xyz/ru/articles/deploy-gh-actions/</link><pubDate>Tue, 24 Jun 2025 14:18:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/deploy-gh-actions/</guid><description>Недавно я нашёл простой способ выполнять любые операции на своих серверах через GitHub Actions. Мне нравится прозрачность этого подхода — я могу контролировать все шаги и процессы. Сейчас я покажу, как настроить чистое развертывание, но вы можете адаптировать это под свои задачи.
Как мы разворачиваем вручную? В простейшем случае подключаемся к серверу, вытягиваем репозиторий и перезапускаем приложение.
Автоматизируем этот процесс. Начнём с подключения. Обычно это выглядит так:
ssh root@192.168.0.1 -i ~/.ssh/id_rsa25519 Нам нужны три компонента для доступа:</description><content:encoded><![CDATA[<p>Недавно я нашёл простой способ выполнять любые операции на своих серверах через GitHub Actions. Мне нравится прозрачность этого подхода — я могу контролировать все шаги и процессы. Сейчас я покажу, как настроить чистое развертывание, но вы можете адаптировать это под свои задачи.</p>
<p>Как мы разворачиваем вручную? В простейшем случае подключаемся к серверу, вытягиваем репозиторий и перезапускаем приложение.</p>
<p><img src="https://alchemmist.xyz/images/deploy-pipeline.svg" class="content-img" alt="" loading="lazy" decoding="async" /></p>
<p>Автоматизируем этот процесс. Начнём с подключения. Обычно это выглядит так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ssh root@192.168.0.1 -i ~/.ssh/id_rsa25519
</span></span></code></pre></div><p>Нам нужны три компонента для доступа:</p>
<ol>
<li>IP-адрес сервера</li>
<li>Имя пользователя</li>
<li>Приватный SSH-ключ
Для &ldquo;чистого&rdquo; развертывания автоматизируем все шаги. Чтобы GitHub Actions мог подключиться к серверу, передадим данные через секреты.</li>
</ol>
<p>В репозитории GitHub зайдите в <code>Settings</code> → <code>Security</code> → <code>Secrets and Variables</code> → <code>Actions</code>. Добавьте три секрета: <code>SSH_USER</code>, <code>SSH_HOST</code>, <code>SSH_KEY</code>. Не забудьте добавить публичный ключ в <code>~/.ssh/authorized_keys</code> на сервере. Первый шаг завершён.</p>
<p>Далее — вытягивание репозитория. Если сервер чистый, его нужно клонировать. Для приватных репозиториев вы можете столкнуться с ошибкой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">git@github.com: Permission denied (publickey).
</span></span><span class="line"><span class="cl">fatal: Could not read from remote repository.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Please make sure you have the correct access rights
</span></span><span class="line"><span class="cl">and the repository exists.
</span></span></code></pre></div><p>Сгенерируйте SSH-ключи на сервере:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ssh-keygen -C github -t ed25519
</span></span></code></pre></div><p>Добавьте публичный ключ в <code>Deploy keys</code> в настройках репозитория GitHub. Создайте секрет <code>SSH_PROJECT_PATH</code> для пути к проекту.</p>
<p>Финальный шаг — GitHub Actions. Создайте файл <code>.gitub/workflows/deploy.yaml</code> (имя не важно):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Pull repo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">appleboy/ssh-action@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_USER }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            cd ${{ secrets.SSH_PROJECT_PATH }}
</span></span></span><span class="line"><span class="cl"><span class="sd">            git pull origin main --rebase</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Compose down</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">appleboy/ssh-action@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_USER }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            cd ${{ secrets.SSH_PROJECT_PATH }}
</span></span></span><span class="line"><span class="cl"><span class="sd">            docker compose down</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Compose up</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">appleboy/ssh-action@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_HOST }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_USER }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.SSH_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            cd ${{ secrets.SSH_PROJECT_PATH }}
</span></span></span><span class="line"><span class="cl"><span class="sd">            docker compose up -d</span><span class="w">
</span></span></span></code></pre></div><p>В этом примере я использую Docker для перезапуска сервисов. Выберите свой подход!```</p>
]]></content:encoded><category>github</category></item><item><title>Очистка диска в Linux</title><link>https://alchemmist.xyz/ru/articles/linux-clean-up/</link><pubDate>Sun, 02 Feb 2025 14:18:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/linux-clean-up/</guid><description>Если вы используете Docker, хорошее начало — очистка связанных с ним данных (только если уверены, что контейнеры и тома не содержат важного):
docker system prune # 1. Очистка пакетов Советы для Arch-систем:
Поиск самых тяжёлых пакетов (топ-20):
pacman -Qi | awk &amp;#39;/^Name/{name=$3} /^Installed Size/{print $4 $5, name}&amp;#39; | sort -hr | head -n 20 Поиск неиспользуемых зависимостей:
pacman -Qdtq Внимание! В этом списке могут быть важные пакеты, которые просто никто не использует в данный момент.</description><content:encoded><![CDATA[<p>Если вы используете Docker, хорошее начало — очистка связанных с ним данных (только если уверены, что контейнеры и тома не содержат важного):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">docker system prune
</span></span></code></pre></div>
<h2 id="1-очистка-пакетов">
  <a class="link" href="#1-%d0%be%d1%87%d0%b8%d1%81%d1%82%d0%ba%d0%b0-%d0%bf%d0%b0%d0%ba%d0%b5%d1%82%d0%be%d0%b2">
    #
  </a>
  1. Очистка пакетов
</h2>

<p>Советы для Arch-систем:</p>
<p>Поиск самых тяжёлых пакетов (топ-20):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pacman -Qi <span class="p">|</span> awk <span class="s1">&#39;/^Name/{name=$3} /^Installed Size/{print $4 $5, name}&#39;</span> <span class="p">|</span> sort -hr <span class="p">|</span> head -n <span class="m">20</span>
</span></span></code></pre></div><p>Поиск неиспользуемых зависимостей:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pacman -Qdtq
</span></span></code></pre></div><p><strong>Внимание!</strong> В этом списке могут быть важные пакеты, которые просто никто не использует в данный момент.</p>
<p>Удаление неиспользуемых зависимостей (если уверены):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo pacman -Rns <span class="k">$(</span>pacman -Qdtq<span class="k">)</span>
</span></span></code></pre></div><p>Поиск забытых пакетов (топ-20 давно неиспользуемых):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">expac --timefmt<span class="o">=</span><span class="s1">&#39;%Y-%m-%d %T&#39;</span> <span class="s1">&#39;%l %n&#39;</span> <span class="p">|</span> sort <span class="p">|</span> head -n <span class="m">20</span>
</span></span></code></pre></div><p>Очистка кэша <code>pacman</code> (сохраняет текущую и предыдущую версии пакетов):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo paccache -r
</span></span></code></pre></div><p>Полная очистка кэша (оставляет только установленные версии):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">sudo paccache -r -k <span class="m">0</span>
</span></span></code></pre></div><p>Помните: после этого вы не сможете откатить пакеты без интернета.</p>

<h2 id="2-очистка-файлов">
  <a class="link" href="#2-%d0%be%d1%87%d0%b8%d1%81%d1%82%d0%ba%d0%b0-%d1%84%d0%b0%d0%b9%d0%bb%d0%be%d0%b2">
    #
  </a>
  2. Очистка файлов
</h2>

<p>Топ-15 самых тяжёлых директорий в домашнем каталоге:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">du -h ~/ --max-depth<span class="o">=</span><span class="m">1</span> <span class="p">|</span> sort -hr <span class="p">|</span> head -n <span class="m">15</span>
</span></span></code></pre></div><p>Топ-20 самых тяжёлых файлов в домашнем каталоге:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">find ~/ -type f -exec du -Sh <span class="o">{}</span> + <span class="p">|</span> sort -rh <span class="p">|</span> head -n <span class="m">20</span>
</span></span></code></pre></div><p>Этих двух команд обычно достаточно для поиска точек очистки. Можно перейти в тяжёлую директорию и повторить команду внутри неё.```</p>
]]></content:encoded><category>linux</category></item></channel></rss>