Урок курса
Знакомство с LangChain – модели и простые цепочки
LangChain 1.0.x: c нуля до продакшн LLM-приложений🔹 Установка LangChain и базовых сущностей
LangChain – это открытый фреймворк, упрощающий создание приложений на базе LLM. Он не заменяет OpenAI SDK, а использует его внутри, предлагая более высокий уровень абстракций и готовые инструменты для построения цепочек взаимодействия с моделью.
В этом уроке мы познакомимся с этой библиотекой, подключим её к нашему проекту и разберём ключевые понятия:
модель (LLM)
шаблон промпта (PromptTemplate)
цепочку (Chain)
память (Memory)
🔸Из чего состоит LangChain
LangChain стремительно развивается, и к 2025 году библиотека приобрела модульную структуру — это не один пакет, а несколько, каждый со своей ролью. Всё, что нужно для этого урока, мы уже поставили, когда готовили окружение, так что устанавливать ничего не придётся. Разберёмся, что именно у нас стоит:
💡
langchain-openaiэто дополнительный модуль к библиотеке langchain. И несмотря на то что пакетopenaiуже установлен,langchain-openaiнужен отдельно — он содержит адаптеры и классы (ChatOpenAI,OpenAIEmbeddingsи др.), которые позволяют использовать OpenAI-модели внутри LangChain по единому интерфейсу
💡langchain-core— это ядро библиотеки, где реализованы базовые абстракции LangChain: промпты, схемы сообщений, раннеры и системы вызова моделей. Он устанавливается вместе с основной библиотекой, но может использоваться отдельно для создания лёгких проектов без лишних зависимостей.
💡langgraph— пакет для графов и состояний, в него вынесены инструменты работы с памятью диалога, с ними мы познакомимся ниже
🔸Основные сущности LangChain
LLM Wrappers — Обёртки для моделей
Это классы, предоставляющие унифицированный интерфейс к различным моделям.
Например для OpenAI используется класс OpenAI или ChatOpenAI для чат-моделей из пакета langchain_openai.
Эти классы знают, как вызвать API и получить результат, предоставляя удобный интерфейс.:
# Если вы используете фаил .env, не забывйте импортировать переменные.
# В примерах далее мы будем сокращать этот шаг,
# но имейте ввиду, если мы работаем с API, это необходимо делать
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
# при этом вызове OPENAI_API_KEY и OPENAI_API_BASE подгружаются в окружение
# если вы так делаете, то в вызове класса ChatOpenAI() эти аргументы указывать уже не нужно
load_dotenv()
# Название модели тоже иногда удобно брать из файла .env
# тут мы берём его из файла, а если такой переменной в файле нет, то подставляем "gpt-5"
MODEL_NAME = os.getenv("OPENAI_API_MODEL", "gpt-5")
model = ChatOpenAI(
# api_key=API_KEY, # если не используете dotenv то нужно указать
# base_url=API_BASE, # если не используете dotenv то нужно указать
model=MODEL_NAME,
temperature=0.5
)
response = model.invoke("Привет. Что ты умеешь?")
print(response.content)
💡В примере используется модель GPT-5, но советуем вам для подобных тестовых примеров использовать модели попроще. С расценками на токены определённых моделей можно ознакомится в разделе pricing сайта platform.openai.com или аналогично там, где вы оплачиваете доступ к API ваших моделей.
PromptTemplate — шаблон промпта
Это класс для управления текстом запроса с заполнением параметров. По сути, это строка с плейсхолдерами, которые можно заменять перед отправкой модели:
from langchain_core.prompts import PromptTemplate template = PromptTemplate.from_template("Перескажи суть теста: {article}") prompt = template.invoke({"article": "Здесь находится текст для пересказа."}) print(prompt) # text='Перескажи суть теста: Здесь находится текст для пересказа.'💡 Модуль
langchain_core— это ядро библиотеки, где реализованы базовые абстракции LangChain. Он устанавливается вместе с основной библиотекой.
💡 В этом модуле мы лишь слегка коснемся шаблонов (для понимания), а во втором модуле курса – Prompt Engineering углубимся в тонкости.Chains — цепочки обработки
Ключевая концепция LangChain. Цепочка – это последовательность единиц обработки, которые могут включать LLM, вызовы функций, логические узлы и т.п.
Tools (Инструменты)
Позволяют моделям взаимодействовать с внешними системами (API, базы данных, файловые системы) через структурированные запросы.
Инструмент — это вызываемая функция с определённой схемой входных данных, которую модель может использовать по необходимости. Инструменты расширяют возможности моделей, позволяя им генерировать корректные запросы к внешним системам.
Некоторые модели (OpenAI, Anthropic, Gemini) имеют встроенные серверные инструменты, например, веб-поиск или интерпретаторы кода. Пример:from langchain.tools import tool @tool def search_database(query: str, limit: int = 10) -> str: """Ищет записи в базе данных клиентов по запросу.""" return f"Found {limit} results for '{query}'"💡 Для создания инструмента используется декоратор
@toolAgents (Агенты)
Агенты объединяют языковые модели и инструменты, чтобы решать задачи, выбирать подходящие инструменты и последовательно продвигаться к решению.
create_agentсоздаёт готового к использованию агента.:from langchain.agents import create_agent agent = create_agent( "gpt-5", tools=tools )Memory — память диалога
Механизм хранения состояния диалога между вызовами позволяя запоминать контекст, например для реализации чат бота, который должен помнить о чём диалог.
В актуальной версии фреймворка инструменты работы с памятью перенесены в модуль langgraph:from langchain.agents import create_agent from langgraph.checkpoint.memory import InMemorySaver agent = create_agent( "gpt-5", [get_user_info], checkpointer=InMemorySaver(), # хранение контекста в памяти ) agent.invoke( {"messages": [{"role": "user", "content": "Привет! Меня зовут Ваня"}]}, {"configurable": {"thread_id": "1"}}, )💡 Существуют разные варианты реализации памяти
🔸Почему LangChain?
Можно спросить, зачем нужна эта прослойка, если мы уже умеем вызывать API напрямую? Дело в том, что по мере роста сложности приложения вам придется:
Держать где-то промпты (шаблоны) и подставлять данные.
Обрабатывать историю диалога.
Подключать дополнительные источники данных или инструменты.
Менять модели или провайдеров (например, захотеть переключиться с OpenAI на Anthropic или локальную модель).
LangChain берет часть этой рутины на себя. Он предоставляет единый интерфейс ко многим моделям, помощь в форматировании диалогов, структуру для организации многошаговых взаимодействий.
Проще говоря позволяет писать меньше шаблонного кода, сосредоточившись на логике приложения.
🔸Пример базового использования LangChain
Разберём базовый пример по шагам:
from langchain_openai import ChatOpenAI
chat = ChatOpenAI(
model_name="gpt-5",
temperature=0.7
)
response = chat.invoke("Какой сегодня день недели?")
print(response)
Что здесь произошло:
ChatOpenAI– класс LangChain для чат-моделей OpenAI. Мы указалиmodel_nameиtemperature. Он автоматически возьметOPENAI_API_KEYиз переменной окружения (если она установлена), а такжеOPENAI_API_BASE– то есть интеграция с прокси тоже сохраняется (LangChain внутри использует openai пакет, поэтому наши настройки черезos.getenvдействуют). Мы могли бы явно передатьopenai_api_key="..."иopenai_api_base="..."в конструктор, но если они настроены в окружении, то это не нужно.Метод
invoke()уChatOpenAIпринимает строку – ваш пользовательский запрос – и возвращает строку-ответ от модели. За кулисами LangChain сам сформировал messages с одной ролью user, вызвалopenai.ChatCompletion.createи вернулcontentответа. То естьchat.invoke("Какой сегодня день недели?")примерно эквивалентно тому, что мы делали руками ранее, только короче.Мы сразу печатаем
response. Модель, скорее всего, ответит чем-то вроде: "Сегодня среда." (или другой день, модель не знает реального времени, но попробует угадать или скажет, что не имеет доступа к календарю – зависит от её знаний на момент обучения).
⚠️ Прежде чем идти дальше, проверьте, что эти строки выполняются без ошибок:
from langchain.agents import create_agentиfrom langchain.tools import tool.
Если видитеImportError: cannot import name 'ExecutionInfo' from 'langgraph.runtime'— значит, версии пакетов разъехались: обычно так бывает, когдаlanggraph-prebuiltвстал свежее остальных и обращается к тому, чего в установленномlanggraphещё нет. Вернитесь к уроку про подготовку окружения и переустановите зависимости строго по списку с версиями.
Ещё одна частая причина ошибок — версия интерпретатора: LangChain 1.x требует Python 3.10 или новее, проверьте черезpython --version
🔹 Простейшая цепочка Prompt → LLM → Output
На этом шаге мы разберём создание простейшей цепочки (Chain), основного концепта фреймворка langchain, суть которого кратко мы описывали на предыдущем шаге
Самая простая цепочка – это подача промпта в LLM и получающая ответ. В актуальной версии фреймворка для этого используется класс RunnableSequence
🔸Шаги для создания и использования цепочки
-
Определить шаблон промпта. Например, мы хотим, чтобы модель объяснила какое-то понятие простыми словами. Создадим шаблон: "Объясни простыми словами, что такое {question}"
Здесь{question}– это переменная. В ChatPromptTemplate мы зададём эту переменную, а вдальнейшем при вызове эта переменная будет заменятся строкой из запроса.from langchain_core.prompts import ChatPromptTemplate template = ChatPromptTemplate.from_template("Объясни простыми словами, что такое {question}")Теперь
prompt– объект, который умеет принять конкретныйquestionи выдать готовую строку. Например "Объясни простыми словами, что такое машинное обучение.", если в запросе будет указано машинное обучение -
Создать объект модели. Возьмем, например,
ChatOpenAI(для GPT-4o).from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o", temperature=0)Здесь мы ставим temperature=0 для максимальной детерминированности ответа (чтобы не расплывался). Объект model готов к использованию.
-
Создать цепочку (Chain).
from langchain_core.runnables import RunnableSequence chain = RunnableSequence(first=prompt, last=model)Теперь
chain– связывает наш шаблон и модель. -
Вызвать цепочку с конкретным входом
Для нашего шаблона требуется переменная
"question". Значит:result = chain.invoke({"question": "машинное обучение"}) print(result.content)LangChain сам возьмет шаблон, подставит
question="машинное обучение", получит строчку"Объясни простыми словами, что такое машинное обучение.", затем вызовет модельmodelс этим промптом. Модель вернет ответ вроде"Машинное обучение – это ... "). RunnableSequence вернёт именно содержимое ответа. Вresult.contentбудет строка с объяснением. Мы ее печатаем.
🔸Изменение параметров
Мы указали temperature=0, чтобы ответ был более определенным. Попробуйте поменять на 1 и запустить код несколько раз – могут различаться формулировки ответа (он станет более творческим, но и менее стабильным). Это демонстрирует, что RunnableSequence не мешает нам контролировать параметры модели – мы задаем их при создании model.
Если бы шаблон имел несколько переменных, ChatPromptTemplate.from_template("переменная 1: {var1}, переменная 2: {var2}") , то и chain.invoke() нужно было бы вызывать со всеми аргументами. chain.invoke({"var1": "один", "var2": "два"})
🔸Вызов модели напрямую vs через RunnableSequence
Можно отметить, что мы могли не использовать RunnableSequence, а делать:
prompt = template.invoke({"question": "машинное обучение"})
response = model.invoke(prompt)
и получить тот же результат.
Да, LangChain допускает и так. Но при усложнении цепочек (несколько шагов, memory, обработка результата) выгоднее сразу держать всё в цепочке.
RunnableSequence обеспечитвает единообразие: вы всегда вызываете .invoke() вне зависимости от того, что внутри – один LLM или сложная последовательность. Позже, если нужно будет поменять логику (например, добавить memory), вы модифицируете chain, а код вызова останется chain.invoke(...).
🔸Дополнительный пример (для практики)
Создайте другой шаблон – например, простой переводчик:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnableSequence
prompt = ChatPromptTemplate.from_template("Переведи на английский: {текст}")
model = ChatOpenAI(model="gpt-4o", temperature=0)
chain = RunnableSequence(first=prompt, last=model)
result = chain.invoke({"текст": "Доброе утро"})
print(result.content)
Ожидаемый ответ – "Good morning" (GPT-4o с temperature 0 даст прямой перевод). Так мы убедимся, что можно переиспользовать RunnableSequence с разными шаблонами.
💡 Обратите внимание, мы можем писать шаблон в том числе и название переменных в шаблоне на русском и модель понимает их, потому что GPT-4o владеет русским языком. LangChain не привязан к языку – шаблоны могут быть на любом языке, главное, чтобы модель понимала
🔹 LCEL (LangChain Expression Language)
На прошлом шаге мы реализовывали цепочку с помощью объекта RunnableSequence, но Langchain в новых версиях сильно поменял свою архитектуру и теперь основной подход по построению цепочек это LCEL. Данный способ строит цепочки обработки данных через оператор | (pipe). Данные текут слева направо, как в Unix-командах.
🔸Базовый синтаксис
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
MODEL = os.getenv("OPENAI_API_MODEL", "gpt-5")
prompt = ChatPromptTemplate.from_template("Привет. Меня зовут {name}")
llm = ChatOpenAI(model=MODEL)
basic_chain = prompt | llm
result = basic_chain.invoke({"name": "Вася"})
print(result)
По сути мы тут реализуем то же самое что и с RunnableSequence, промпт форматируется → отправляется в LLM. Но нам не нужно делать дополнительных импортов и код становится чище. Но это конечно не все плюсы по сравнению с использованием отдельного класса.
🔸Основные компоненты (Runnables)
Runnables — это универсальные объекты, которые можно запускать. Они представляют собой отдельные вычислительные шаги или блоки, которые принимают входные данные и возвращают результат. По сути, это замена старой концепции цепочек (Chains) и отдельных LLM шагов, но с более модульной и гибкой архитектурой.
-
RunnablePassthrough— передача данных без изменений
Может показаться что он ничего не делает, но на самом деле полезный объект, дальше это будет видно на практикеfrom langchain_core.runnables import RunnablePassthrough # Просто передаёт входные данные дальше chain = RunnablePassthrough() | llm result = chain.invoke("Привет!") # "Привет!" → llm print(result) -
RunnableLambda— применяет функцию
Польза этого объекта очевидна, можно написать костюмную функцию обработки или короткий скрипт с помощьюlambdaпрямо в аргументеfrom langchain_core.runnables import RunnableLambda def uppercase(text): return text.upper() chain = RunnableLambda(uppercase) | llm chain.invoke("hello") # "HELLO" → llm -
StrOutputParser— извлечь текст из ответа LLM
Простая цепочка выводит нам объектAIMessageи для того чтобы нам из него получить только ответ, нам бы приходилось каждый раз доставать его через атрибут.content, но мы можем упростить это и сразу парсить только ответ, если остальное нам не нужно. Делается это так:from langchain_core.output_parsers import StrOutputParser chain_with_parser = prompt | llm | StrOutputParser() result_with_parser = chain_with_parser.invoke({"name": "Вася"}) # С объектом из примере базового синтаксиса print(type(basic_chain)) # <class 'langchain_core.messages.ai.AIMessage'> print(basic_chain) # content='Привет, Вася! 😊 Как дела?' additional_kwargs={'refusal': None} ... # если парсим ответ print(type(result_with_parser)) # <class 'str'> print(result_with_parser) # Привет, Вася! 😊 Как дела? -
Работа со словарями
Как мы уже убедились, часто приходится передовать в аргументах словарь с переменными, для подстановки этих переменных в шаблоны. LCEL позволяет гибко работать с этими словарями.
Тут мы создаём словарь и аргументы, которые будут переданы при вызове, с помощью классаRunnablePassthroughпередаём дальше по цепочке уже в словаре:# Создать словарь из входных данных chain = {"name": RunnablePassthrough()} | prompt | llm result = chain.invoke("Ваня") # → {"name": "Ваня"} → prompt → llm print(result)Тут мы уже добавляем ключи в словарь используя метод
.assign()chain = ( {"name": RunnablePassthrough()} | RunnablePassthrough.assign(second_name=lambda x: "Иванов") ) result = chain.invoke("Вася") print(result) # вывод: {'name': 'Вася', 'second_name': "Иванов"}⚠️ Важно чтобы значением был объект
А тут достаём определённые ключи из словаря методомrunnable,lambdaфункция подходит для этого.pick()chain = ( {"name": RunnablePassthrough(), "age": lambda x: 25, "city": lambda x: "Москва"} | RunnablePassthrough().pick(["name", "city"]) ) result = chain.invoke("Иван") print(result) # # вывод: {'name': 'Иван', 'city': 'Москва'} -
Параллельное выполнение —
RunnableParallelfrom langchain_core.runnables import RunnableParallel prompt1 = ChatPromptTemplate.from_template("Плюсы {topic}") prompt2 = ChatPromptTemplate.from_template("Минусы {topic}") # Выполняются ОДНОВРЕМЕННО parallel_chain = RunnableParallel( pros=prompt1 | llm | StrOutputParser(), cons=prompt2 | llm | StrOutputParser() ) result = parallel_chain.invoke({"topic": "Python"}) print(result) # result = {"pros": "...", "cons": "..."} -
Условная логика —
RunnableBranchfrom langchain_core.runnables import RunnableBranch prompt_for_questions = ChatPromptTemplate.from_template("вопрос: {text}") prompt_for_statements = ChatPromptTemplate.from_template("не вопрос: {text}") def is_question(x): return "?" in x["text"] branch = RunnableBranch( (is_question, prompt_for_questions | llm), # Если вопрос prompt_for_statements | llm # Иначе (default) ) chain = {"text": RunnablePassthrough()} | branch | StrOutputParser() result = chain.invoke({"text": "Какая сегодня погода?"}) print(result)
🔸Шпаргалка по компонентам
| Компонент | Назначение |
|---|---|
| |
Соединяет компоненты в цепочку |
RunnablePassthrough |
Передаёт данные без изменений |
RunnableLambda(func) |
Применяет функцию к данным |
.assign(key=value) |
Добавляет ключ в словарь |
.pick([keys]) |
Выбирает только указанные ключи |
RunnableParallel |
Параллельное выполнение |
RunnableBranch |
Условная логика (if/else) |
StrOutputParser() |
Извлекает текст из ответа LLM |
🔸Главный принцип
Думайте о данных как о потоке воды:
- Оператор
|— это труба - Каждый компонент — фильтр или трансформер
- Данные текут слева направо
- Каждый этап получает выход предыдущего
Вместо "создай объект, вызови метод" → "опиши поток данных от входа до выхода".
🔹 Сохранение контекста диалога
На этом этапе мы научимся сохранять контекст, чтобы модель помимо актуального вопроса пользователя, получала в промпте так же как и предыдущие сообщения (и вопросы, и ответы) и могла отвечать более осмысленно.
🔸 Современный подход с create_agent
В версии 1.0 радикально изменился подход к работе с памятью:
- Память встроена в агенты через
checkpointer - Состояние хранится автоматически между вызовами
- Поддерживаются разные потоки разговоров через
thread_id
Пример:
import os
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="gpt-4o-mini",
checkpointer=InMemorySaver(), # Сохранение в памяти
)
# Конфигурация с уникальным ID потока разговора
conv_1 = {"configurable": {"thread_id": "conversation_001"}}
question1 = "Привет! Меня зовут Алиса"
response1 = agent.invoke(
{"messages": [{"role": "user", "content": question1}]},
conv_1
)
print("пользователь 1:", question1)
print("Бот:", response1["messages"][-1].content)
question2 = "Какое у меня имя?"
response2 = agent.invoke(
{"messages": [{"role": "user", "content": question2}]},
conv_1
)
print("пользователь 1:", question2)
print("Бот:", response2["messages"][-1].content)
# Новый пользователь с другим thread_id
conv_2 = {"configurable": {"thread_id": "conversation_002"}}
question3 = "Какое у меня имя?"
response3 = agent.invoke(
{"messages": [{"role": "user", "content": question3}]},
conv_2
)
print("пользователь 2:", question3)
print("Бот:", response3["messages"][-1].content)
Ожидаемый вывод:
Как это работает:
create_agent— функция для быстрого создания агентовInMemorySaver()— автоматически сохраняет состояние между вызовами, инициализируется при создании агента в аргументеcheckpointerthread_id— уникальный идентификатор потока разговора, подаётся в словаре в настраеваемых параметрах"configurable"при вызове методаinvoke()
🔸 Альтернативные варианты
Для production-приложений используйте персистентное хранилище, например SQLite или PostgreSQL:
# SQLite (для production)
from langgraph.checkpoint.sqlite import SqliteSaver
checkpointer = SqliteSaver.from_conn_string("database.db")
# PostgreSQL (для масштабирования)
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string("postgresql://...")
💡 Эти и другие дополнительные модули доустанавливаются отдельно pip install langgraph-checkpoint-sqlite langgraph-checkpoint-postgres и т.д.
🔹 Важные моменты работы с памятью:
Просмотр истории:
# Получить состояние потока
state = agent.get_state(config)
print("История сообщений:")
for msg in state.values["messages"]:
print(f"{msg.type}: {msg.content}")
Ограничения по контексту:
Если пользователь и бот обменялись десятком длинных сообщений, при каждом новом запросе вся история вставляется в промпт. Это может:
-
Выйти за пределы лимита контекста модели
-
Сделать ответ медленнее и дороже
Решения:
-
Ограничить число хранимых сообщений (например, последние 10)
-
Использовать суммаризацию старой истории (сворачивать в краткое резюме)
-
Применить скользящее окно (только N последних реплик)
В этом курсе мы работаем с короткой историей, поэтому это не критично. Для продвинутого управления памятью есть и другие варианты:
from langchain.agents.middleware import before_model
from langchain.agents import AgentState
@before_model
def trim_messages(state: AgentState, runtime) -> dict:
"""Оставить только последние 10 сообщений"""
messages = state["messages"]
if len(messages) <= 10:
return None # Изменений нет
# Сохраняем первое (системное) и последние 9
return {
"messages": [messages[0]] + messages[-9:]
}
agent = create_agent(
model="gpt-4o-mini",
tools=[],
checkpointer=InMemorySaver(),
middleware=[trim_messages], # Добавляем middleware
)
Встраивание system-контекста:
Чтобы задать поведение бота, используйте system-сообщение:
agent = create_agent(
model="gpt-4o-mini",
tools=[],
checkpointer=InMemorySaver(),
system_message="Ты дружелюбный помощник по математике. "
"Объясняй концепции просто и с примерами. "
"Используй эмодзи для наглядности."
)
⚠️ При использовании прокси ничего менять не нужно – способ соединения не влияет на память . Она только управляет данными, которые мы отправляем. Больше данных в истории = длиннее запрос = немного дороже и медленнее, но на нескольких репликах это не критично.
Задача 1
Сопоставьте сущность LangChain с описанием:
Сопоставьте элементы
Задача 2
Как получить «детерминированные» ответы в учебных примерах?
Задача 3
Зачем вообще использовать LangChain поверх прямого вызова API?
Задача 4
Для диалога «с памятью» самыё простой в реализации подход это:
Задача 5
Что является риском при работе с контекстом в длинном чате?
Задача 6
🔹 Артефакт – простой чат-бот с контекстом
🔸 Описание задания
Создайте простое консольное приложение, позволяющее пользователю общаться с чат-ботом, который помнит контекст беседы. По сути, это упрощенный вариант ChatGPT
🔸 Требования к функционалу
-
Непрерывный диалог с пользователем: скрипт должен считывать ввод пользователя в цикле
whileс помощьюinput(), передавать его модели и печатать ответ. Затем снова ждать ввода – и так, пока пользователь не решит завершить и не напишет"выход" -
Создайте агента с моделью
"gpt-5" -
Память диалога: бот должен помнить предыдущие реплики. Используем
InMemorySaver() -
Прощание: слово
"выход"тоже нужно передать агенту и напечатать его ответ, и только после этого прервать цикл. Если выйти из цикла сразу по условию, прощание в вывод не попадёт и проверка не пройдёт
Необходимые импорты уже произведены:
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
Ничего импортировать больше не придётся
Реализуйте код в функции solution(). Вызывать её не нужно
⚠️ Библиотеки в коде являются имитацией реальных библиотек в ввиду того что на платформе stepik нет возможности импортирования реальных библиотек. В имитированных библиотеках реализован лишь базовый функционал необходимый для этого задания.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
