Repository: andreypomortsev/yndx-python-handbook

Issue:
Feature: Add all solutions for 4.1
1. Выложить решения для задач A,B,C,D,E,F,G,H,I,J параграфа 4.1
2. Покрыть тестами 100% решений, обновить README.md.

Discussion:

Solution patch:
diff --git a/.coveragerc b/.coveragerc
index e6b3c205..0f67eec1 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -6,15 +6,8 @@ source =
 omit =
     conftest.py
     constants.py
-    *__init__.py
-    utils*
-    *test_2*
-    *test_3*
-    solutions/2.*
-    solutions/3.*
     tests/data/*
-    tests/2.*
-    tests/3.*
+    tests/*
 
 [report]
 show_missing = True
diff --git a/README.md b/README.md
index 62f37030..bb1532ee 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,8 @@ _"Хендбук по Python поможет овладеть основным с
 
 ### Описание Проекта
 
-Решения написаны мной, но некоторые варианты для тестов могут быть позаимствованы из [одноименного](https://t.me/handbook_python) телеграм канала. Решение задач подразумевает использование знаний полученных после изучения соответствующего параграфа.
-Так же я решил покрыть тестами все свои решения, они не совпадают на 100% с тестами в учебнике, но при замене моего решения на ваш код, можно будет понять на каких данных не работает код или какие строчки не покрыты тестами (эти строки могут быть лишними). 
+Решения написаны мной, но некоторые тестовые данные могут быть позаимствованы из [одноименного](https://t.me/handbook_python) телеграм канала. Решение задач подразумевает использование знаний полученных после изучения соответствующего параграфа.
+Так же я решил покрыть тестами все свои решения, тесты не совпадают на 100% с тестами в учебнике, но при замене моего решения на ваш код, можно будет понять на каких данных не работает код или какие строчки не покрыты тестами (эти строки могут быть лишними). 
 
 ## Установка
 
@@ -151,6 +151,12 @@ else:
 Файл `wrapped_22_q.py` после применения `wrap_answer` к `22_q.py`:
 
 ```python
+from tests.utils import time_limit, memory_limit
+from tests.constants import TIME_LIMIT, MEMORY_LIMIT
+
+
+@time_limit(TIME_LIMIT)
+@memory_limit(MEMORY_LIMIT)
 def main():
     a = float(input())
     b = float(input())
@@ -414,7 +420,7 @@ def main():
 <details>
 <summary><h3>3.4 Встроенные возможности по работе с коллекциями</h3></summary>
 
-- [3.4 Списочные выражения. Модель памяти для типов языка Python](https://education.yandex.ru/handbook/python/article/spisochnye-vyrazheniya-model-pamyati-dlya-tipov-yazyka-python)
+- [3.4 Встроенные возможности по работе с коллекциями](https://education.yandex.ru/handbook/python/article/vstroennye-vozmozhnosti-po-rabote-s-kollekciyami)
 
 ### [Тестовые данные для задач](./tests/data/test_data_34.py)
 
@@ -444,7 +450,7 @@ def main():
 </details>
 
 <details>
-<summary><h3>3.5 Встроенные возможности по работе с коллекциями</h3></summary>
+<summary><h3>3.5 Потоковый ввод/вывод. Работа с текстовыми файлами. JSON</h3></summary>
 
 - [3.5 Потоковый ввод/вывод. Работа с текстовыми файлами. JSON](https://education.yandex.ru/handbook/python/article/potokovyj-vvodvyvod-rabota-s-tekstovymi-fajlami-json)
 
@@ -484,19 +490,53 @@ def main():
 
 | Решение              | Тесты                |
 |----------------------|----------------------|
-| А. [Функциональное приветствие](./solutions/4.1/41_a.py) | [❌](./tests/4.1/test_41_a.py) |
-| B. [Функциональный НОД](./solutions/4.1/41_b.py) | [❌](./tests/4.1/test_41_b.py) |
-| C. [Длина числа](./solutions/4.1/41_c.py) | [❌](./tests/4.1/test_41_c.py) |
-| D. [Имя of the month](./solutions/4.1/41_d.py) | [❌](./tests/4.1/test_41_d.py) |
-| E. [Числовая строка](./solutions/4.1/41_e.py) | [❌](./tests/4.1/test_41_e.py) |
-| F. [Модернизация системы вывода](./solutions/4.1/41_f.py) | [❌](./tests/4.1/test_41_f.py) |
-| G. [Шахматный «обед»](./solutions/4.1/41_g.py) | [❌](./tests/4.1/test_41_g.py) |
-| H. [А роза упала на лапу Азора 7.0](./solutions/4.1/41_h.py) | [❌](./tests/4.1/test_41_h.py) |
-| I. [Простая задача 5.0](./solutions/4.1/41_i.py) | [❌](./tests/4.1/test_41_i.py) |
-| J. [Слияние](./solutions/4.1/41_j.py) | [❌](./tests/4.1/test_41_j.py) |
+| А. [Функциональное приветствие](./solutions/4.1/41_a.py) | [✅](./tests/4.1/test_41_a.py) |
+| B. [Функциональный НОД](./solutions/4.1/41_b.py) | [✅](./tests/4.1/test_41_b.py) |
+| C. [Длина числа](./solutions/4.1/41_c.py) | [✅](./tests/4.1/test_41_c.py) |
+| D. [Имя of the month](./solutions/4.1/41_d.py) | [✅](./tests/4.1/test_41_d.py) |
+| E. [Числовая строка](./solutions/4.1/41_e.py) | [✅](./tests/4.1/test_41_e.py) |
+| F. [Модернизация системы вывода](./solutions/4.1/41_f.py) | [✅](./tests/4.1/test_41_f.py) |
+| G. [Шахматный «обед»](./solutions/4.1/41_g.py) | [✅](./tests/4.1/test_41_g.py) |
+| H. [А роза упала на лапу Азора 7.0](./solutions/4.1/41_h.py) | [✅](./tests/4.1/test_41_h.py) |
+| I. [Простая задача 5.0](./solutions/4.1/41_i.py) | [✅](./tests/4.1/test_41_i.py) |
+| J. [Слияние](./solutions/4.1/41_j.py) | [✅](./tests/4.1/test_41_j.py) |
 
 </details>
 
+## Создание Issue
+
+Если вы обнаружили ошибку, очепятку или баг в проекте, пожалуйста, создайте новый issue, следуя этому шаблону:
+
+### Шаблон для Issue
+
+**Описание ошибки:**
+- Краткое описание проблемы.
+
+**Шаги для воспроизведения:**
+1. Опишите последовательность шагов, которые привели к появлению ошибки.
+2. Укажите точные команды, скрипты или действия, названия файла(ов) если возможно.
+
+**Ожидаемое поведение:**
+- Опишите, как все должно работать при нормальных условиях.
+
+**Фактическое поведение:**
+- Опишите, что происходит на самом деле. 
+
+**Контекст и окружение:**
+- Операционная система и версия.
+- Версия используемого ПО (Python, VSCode, PyCharm, etc.).
+- Укажите любые установленные зависимости (например, версии библиотек).
+
+**Логи и скриншоты:**
+- Прикрепите любые ошибки из логов или скриншоты, если это применимо.
+
+**Дополнительная информация:**
+- Любые другие важные детали, которые помогут в исправлении ошибки.
+
+---
+
+Спасибо за ваш вклад! 😊
+
 ## Автор
 
 - [Andrei Pomortsev](https://www.linkedin.com/in/andreypomortsev/)
diff --git a/solutions/4.1/41_a.py b/solutions/4.1/41_a.py
new file mode 100644
index 00000000..ee54c51e
--- /dev/null
+++ b/solutions/4.1/41_a.py
@@ -0,0 +1,2 @@
+def print_hello(name: str) -> None:
+    print(f"Hello, {name}!")
diff --git a/solutions/4.1/41_b.py b/solutions/4.1/41_b.py
new file mode 100644
index 00000000..5f14b7b0
--- /dev/null
+++ b/solutions/4.1/41_b.py
@@ -0,0 +1,4 @@
+def gcd(a: int, b: int) -> int:
+    while b:
+        a, b = b, a % b
+    return a
diff --git a/solutions/4.1/41_c.py b/solutions/4.1/41_c.py
new file mode 100644
index 00000000..0ae75ee1
--- /dev/null
+++ b/solutions/4.1/41_c.py
@@ -0,0 +1,14 @@
+def number_length(number: int) -> int:
+    if not number:
+        return 1
+
+    length = 0
+
+    if number < 0:
+        number = -number
+
+    while number > 0:
+        length += 1
+        number //= 10
+
+    return length
diff --git a/solutions/4.1/41_d.py b/solutions/4.1/41_d.py
new file mode 100644
index 00000000..9787a9a5
--- /dev/null
+++ b/solutions/4.1/41_d.py
@@ -0,0 +1,16 @@
+def month(number: int | str, lang: str) -> str:
+    months = {
+        "1": {"en": "January", "ru": "Январь"},
+        "2": {"en": "February", "ru": "Февраль"},
+        "3": {"en": "March", "ru": "Март"},
+        "4": {"en": "April", "ru": "Апрель"},
+        "5": {"en": "May", "ru": "Май"},
+        "6": {"en": "June", "ru": "Июнь"},
+        "7": {"en": "July", "ru": "Июль"},
+        "8": {"en": "August", "ru": "Август"},
+        "9": {"en": "September", "ru": "Сентябрь"},
+        "10": {"en": "October", "ru": "Октябрь"},
+        "11": {"en": "November", "ru": "Ноябрь"},
+        "12": {"en": "December", "ru": "Декабрь"},
+    }
+    return months.get(str(number), {}).get(lang, "")
diff --git a/solutions/4.1/41_e.py b/solutions/4.1/41_e.py
new file mode 100644
index 00000000..862ea8ce
--- /dev/null
+++ b/solutions/4.1/41_e.py
@@ -0,0 +1,2 @@
+def split_numbers(numbers: str) -> tuple:
+    return tuple(map(int, numbers.split()))
diff --git a/solutions/4.1/41_f.py b/solutions/4.1/41_f.py
new file mode 100644
index 00000000..0315d49a
--- /dev/null
+++ b/solutions/4.1/41_f.py
@@ -0,0 +1,8 @@
+from typing import Set
+
+
+def modern_print(phrase: str, cache: Set[str] = set()) -> None:
+    if phrase in cache:
+        return
+    cache.add(phrase)
+    print(phrase)
diff --git a/solutions/4.1/41_g.py b/solutions/4.1/41_g.py
new file mode 100644
index 00000000..de427ff9
--- /dev/null
+++ b/solutions/4.1/41_g.py
@@ -0,0 +1,6 @@
+def can_eat(horsey: tuple, victim: tuple) -> bool:
+    x1, y1 = horsey
+    x2, y2 = victim
+    hor = abs(x2 - x1) == 1 and abs(y2 - y1) == 2
+    vert = abs(x2 - x1) == 2 and abs(y2 - y1) == 1
+    return hor or vert
diff --git a/solutions/4.1/41_h.py b/solutions/4.1/41_h.py
new file mode 100644
index 00000000..2336d203
--- /dev/null
+++ b/solutions/4.1/41_h.py
@@ -0,0 +1,22 @@
+def is_palindrome(input_value) -> bool:
+    """
+    Проверяет, является ли входное значение палиндромом.
+
+    Аргументы:
+        input_value (str | int | tuple | list): Входное значение для проверки
+            на палиндром.
+
+    Возвращает:
+        bool: True, если входное значение является палиндромом,
+            False в противном случае.
+    """
+    if isinstance(input_value, (tuple, list)):
+        input_value = list(map(lambda x: str(x).lower(), input_value))
+
+    elif isinstance(input_value, str):
+        input_value = "".join(map(lambda x: x.lower(), input_value.split()))
+
+    else:
+        input_value = str(input_value)
+
+    return input_value == input_value[::-1]
diff --git a/solutions/4.1/41_i.py b/solutions/4.1/41_i.py
new file mode 100644
index 00000000..6c7aff3c
--- /dev/null
+++ b/solutions/4.1/41_i.py
@@ -0,0 +1,27 @@
+def is_prime(n: int) -> bool:
+    """
+    Проверяет, является ли заданное число простым.
+
+    Аргументы:
+        n (int): Число для проверки на простоту.
+
+    Возвращает:
+        bool: True — если переданное число простое, а иначе — False.
+    """
+    if n <= 1:
+        return False
+
+    elif n <= 3:
+        return True
+
+    elif n % 2 == 0 or n % 3 == 0:
+        return False
+
+    i = 5
+
+    while i * i <= n:
+        if n % i == 0 or n % (i + 2) == 0:
+            return False
+        i += 6
+
+    return True
diff --git a/solutions/4.1/41_j.py b/solutions/4.1/41_j.py
new file mode 100644
index 00000000..de8f1e56
--- /dev/null
+++ b/solutions/4.1/41_j.py
@@ -0,0 +1,30 @@
+def merge(left: tuple, right: tuple) -> tuple:
+    """
+    Объединяет два отсортированных списка в один отсортированный кортеж.
+
+    Аргументы:
+        left (tuple): Отсортированный кортеж.
+        right (tuple): Отсортированный кортеж.
+
+    Возвращает:
+        tuple: Объединенный отсортированный кортеж.
+    """
+    merged = []
+    left_index = right_index = 0
+
+    # Сравниваем элементы left и right
+    while left_index < len(left) and right_index < len(right):
+
+        if left[left_index] < right[right_index]:
+            merged.append(left[left_index])
+            left_index += 1
+
+        else:
+            merged.append(right[right_index])
+            right_index += 1
+
+    # Добавляем оставшиеся элементы
+    merged.extend(left[left_index:])
+    merged.extend(right[right_index:])
+
+    return tuple(merged)


Test patch:
diff --git a/tests/4.1/test_41_a.py b/tests/4.1/test_41_a.py
new file mode 100644
index 00000000..58766a6d
--- /dev/null
+++ b/tests/4.1/test_41_a.py
@@ -0,0 +1,30 @@
+from io import StringIO
+from typing import Callable
+
+import pytest
+
+from tests.data.test_data_41 import a_test_data
+
+
+@pytest.mark.parametrize(
+    "arg, expected_output, _",
+    a_test_data,
+    ids=[i[-1] for i in a_test_data],
+)
+def test_input_output(
+    monkeypatch: pytest.MonkeyPatch,
+    decorated_function: Callable,
+    arg: str,
+    expected_output: str,
+    _: str,
+) -> None:
+
+    # Считываем print с функции
+    mock_stdout = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_stdout)
+
+    decorated_function(arg)
+
+    printed_output = mock_stdout.getvalue()
+
+    assert printed_output == expected_output
diff --git a/tests/4.1/test_41_b.py b/tests/4.1/test_41_b.py
new file mode 100644
index 00000000..c29f7682
--- /dev/null
+++ b/tests/4.1/test_41_b.py
@@ -0,0 +1,21 @@
+from typing import Callable, Tuple
+
+import pytest
+
+from tests.data.test_data_41 import b_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    b_test_data,
+    ids=[i[-1] for i in b_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    args: Tuple[int],
+    expected_output: str,
+    _: str,
+) -> None:
+    returned_output = decorated_function(*args)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_c.py b/tests/4.1/test_41_c.py
new file mode 100644
index 00000000..f7f53a4a
--- /dev/null
+++ b/tests/4.1/test_41_c.py
@@ -0,0 +1,21 @@
+from typing import Callable
+
+import pytest
+
+from tests.data.test_data_41 import c_test_data
+
+
+@pytest.mark.parametrize(
+    "arg, expected_output, _",
+    c_test_data,
+    ids=[i[-1] for i in c_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    arg: int,
+    expected_output: int,
+    _: str,
+) -> None:
+    returned_output = decorated_function(arg)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_d.py b/tests/4.1/test_41_d.py
new file mode 100644
index 00000000..82c6ad2b
--- /dev/null
+++ b/tests/4.1/test_41_d.py
@@ -0,0 +1,21 @@
+from typing import Callable, Tuple, Union
+
+import pytest
+
+from tests.data.test_data_41 import d_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    d_test_data,
+    ids=[i[-1] for i in d_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    args: Tuple[Union[int, str], str],
+    expected_output: str,
+    _: str,
+) -> None:
+    returned_output = decorated_function(*args)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_e.py b/tests/4.1/test_41_e.py
new file mode 100644
index 00000000..0c7016a5
--- /dev/null
+++ b/tests/4.1/test_41_e.py
@@ -0,0 +1,21 @@
+from typing import Callable, Tuple
+
+import pytest
+
+from tests.data.test_data_41 import e_test_data
+
+
+@pytest.mark.parametrize(
+    "arg, expected_output, _",
+    e_test_data,
+    ids=[i[-1] for i in e_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    arg: str,
+    expected_output: Tuple[int],
+    _: str,
+) -> None:
+    returned_output = decorated_function(arg)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_f.py b/tests/4.1/test_41_f.py
new file mode 100644
index 00000000..0147da94
--- /dev/null
+++ b/tests/4.1/test_41_f.py
@@ -0,0 +1,37 @@
+from io import StringIO
+from typing import Callable, Tuple
+
+import pytest
+
+from tests.data.test_data_41 import f_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    f_test_data,
+    ids=[i[-1] for i in f_test_data],
+)
+def test_input_output(
+    monkeypatch: pytest.MonkeyPatch,
+    decorated_function: Callable,
+    args: Tuple[str],
+    expected_output: str,
+    _: str,
+) -> None:
+
+    outputs = []
+
+    for input in args:
+        # Считываем print с функции
+        mock_stdout = StringIO()
+        monkeypatch.setattr("sys.stdout", mock_stdout)
+
+        decorated_function(input)
+        printed = mock_stdout.getvalue()
+
+        if printed:
+            outputs.append(printed)
+
+    printed_output = "".join(outputs)
+
+    assert printed_output == expected_output
diff --git a/tests/4.1/test_41_g.py b/tests/4.1/test_41_g.py
new file mode 100644
index 00000000..55db45bd
--- /dev/null
+++ b/tests/4.1/test_41_g.py
@@ -0,0 +1,21 @@
+from typing import Callable, Tuple
+
+import pytest
+
+from tests.data.test_data_41 import g_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    g_test_data,
+    ids=[i[-1] for i in g_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    args: Tuple[Tuple[int]],
+    expected_output: bool,
+    _: str,
+) -> None:
+    returned_output = decorated_function(*args)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_h.py b/tests/4.1/test_41_h.py
new file mode 100644
index 00000000..9e9cccbf
--- /dev/null
+++ b/tests/4.1/test_41_h.py
@@ -0,0 +1,21 @@
+from typing import Any, Callable
+
+import pytest
+
+from tests.data.test_data_41 import h_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    h_test_data,
+    ids=[i[-1] for i in h_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    args: Any,
+    expected_output: bool,
+    _: str,
+) -> None:
+    returned_output = decorated_function(args)
+
+    assert returned_output == expected_output
diff --git a/tests/4.1/test_41_i.py b/tests/4.1/test_41_i.py
new file mode 100644
index 00000000..01849729
--- /dev/null
+++ b/tests/4.1/test_41_i.py
@@ -0,0 +1,39 @@
+from typing import Callable
+
+import pytest
+
+from tests import utils
+from tests.data.test_data_41 import i_test_data
+
+
+@pytest.mark.parametrize(
+    "arg, expected_output, _",
+    i_test_data,
+    ids=[i[-1] for i in i_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    arg: int,
+    expected_output: int,
+    _: str,
+) -> None:
+    returned_output = decorated_function(arg)
+
+    assert returned_output == expected_output
+
+
+@pytest.mark.slow
+@pytest.mark.xfail(reason="Слишком большие числа на входе.")
+def test_time_overload(
+    decorated_function: Callable,
+):
+    """
+    Проверяет, что функция вызывает TimeLimitExceeded при использовании слишком
+    больших чисел.
+
+    Аргументы:
+        decorated_function (Callable): Декорированная функция для тестирования.
+    """
+    a = 2305843009213693951
+    with pytest.raises(utils.TimeLimitExceeded):
+        decorated_function(a)
diff --git a/tests/4.1/test_41_j.py b/tests/4.1/test_41_j.py
new file mode 100644
index 00000000..35fb15de
--- /dev/null
+++ b/tests/4.1/test_41_j.py
@@ -0,0 +1,42 @@
+from typing import Callable, Tuple
+
+import pytest
+
+from tests import utils
+from tests.data.test_data_41 import j_test_data
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    j_test_data,
+    ids=[i[-1] for i in j_test_data],
+)
+def test_input_output(
+    decorated_function: Callable,
+    args: Tuple[Tuple[int]],
+    expected_output: Tuple[int],
+    _: str,
+) -> None:
+    returned_output = decorated_function(*args)
+
+    assert returned_output == expected_output
+
+
+@pytest.mark.slow
+@pytest.mark.xfail(reason="Слишком большие числа на входе.")
+def test_memory_overload(
+    decorated_function: Callable,
+):
+    """
+    Проверяет, что функция вызывает MemoryLimitExceeded при использовании
+    слишком больших чисел.
+
+    Аргументы:
+        decorated_function (Callable): Декорированная функция для тестирования.
+    """
+    first = tuple(range(10**6))
+
+    with pytest.raises(utils.MemoryLimitExceeded):
+        decorated_function(first, first)
+
+    del first
diff --git a/tests/conftest.py b/tests/conftest.py
index d5f1df84..0726b1c2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,7 @@
 import json
 import os
 import sys
+from pathlib import Path
 from types import ModuleType
 from typing import Callable, Generator, Tuple, Union
 
@@ -10,6 +11,7 @@
 from _pytest.main import Session
 
 from . import utils
+from .constants import MEMORY_LIMIT, TEST_FUNCTION_NAMES, TIME_LIMIT
 
 # Добавить родительский каталог в системный путь
 # Это позволяет импортировать модули из родительского каталога
@@ -38,9 +40,9 @@ def wrapper(file_path: str, file_name: str, args: Union[str, None]) -> str:
 
         imports = (
             "from tests.utils import time_limit, memory_limit\n"
-            "from tests.constants import TIME_LIMIT, MEMORY_LIMIT\n\n"
+            "from tests.constants import TIME_LIMIT, MEMORY_LIMIT\n\n\n"
         )
-        decorators = "@time_limit(TIME_LIMIT)\n"
+        decorators = "@time_limit(TIME_LIMIT)\n@memory_limit(MEMORY_LIMIT)\n"
 
         # Обертываем считанный из файла код в функцию main
         if args:
@@ -293,3 +295,79 @@ def cleanup() -> None:
 
     request.addfinalizer(cleanup)
     return add_file
+
+
+@pytest.fixture
+def load_module() -> Callable[[str], ModuleType]:
+    """
+    Загружает модуль Python из указанного файла с использованием importlib.
+
+    Фикстура возвращает функцию, которая принимает путь к файлу,
+    загружает модуль из этого файла и возвращает его.
+
+    Аргументы:
+        file_path (str): Путь к файлу модуля, который нужно загрузить.
+
+    Возвращает:
+        Callable[[str], ModuleType]: Функция, которая загружает и
+            возвращает модуль.
+    """
+
+    def _load_module(file_path: str) -> ModuleType:
+        file_path = Path(file_path).resolve()
+        spec = importlib.util.spec_from_file_location(
+            "solution", str(file_path)
+        )
+        module = importlib.util.module_from_spec(spec)
+        spec.loader.exec_module(module)  # Загружает и выполняет модуль
+
+        return module
+
+    return _load_module
+
+
+@pytest.fixture
+def decorated_function(
+    load_module: Callable[[str], ModuleType],
+    request: pytest.FixtureRequest,
+) -> Callable:
+    """
+    Предоставляет декорированную функцию с ограничениями по времени и памяти.
+
+    Аргументы:
+        load_module (Callable[[str], ModuleType]): Фикстура для загрузки
+            модуля.
+        request (pytest.FixtureRequest): Объект запроса pytest.
+        function_name (str): Имя тестируемой функции.
+
+    Возвращает:
+        Callable: Декорированная функция с ограничениями по времени и памяти.
+
+    Исключения:
+        AttributeError: Если функция с указанным именем не найдена в модуле.
+    """
+    testing_file_path, file_name = utils.get_tested_file_details(request)
+
+    solution = load_module(testing_file_path)
+
+    # Пример: solutions/4.1/21_a.py -> solutions/4.1/
+    dir_path = Path(testing_file_path).parent
+
+    # Пример: solutions/4.1/21_a.py -> 4.1
+    file_dir = dir_path.name
+
+    # Получаем a из 41_a
+    solution_letter = file_name[-1]
+    function_name = TEST_FUNCTION_NAMES[file_dir][solution_letter]
+
+    # Импортируем модуль для тестов
+    solution = load_module(testing_file_path)
+    func = getattr(solution, function_name, None)
+
+    if func is None:
+        raise AttributeError(f"Функция {function_name} не найдена в модуле")
+
+    decorated_func = utils.memory_limit(MEMORY_LIMIT)(func)
+    decorated_func = utils.time_limit(TIME_LIMIT)(decorated_func)
+
+    return decorated_func
diff --git a/tests/constants.py b/tests/constants.py
index d642dd2c..b2e04dd1 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -10,3 +10,18 @@
         "t": "numbers.num",
     },
 }
+
+TEST_FUNCTION_NAMES = {
+    "4.1": {
+        "a": "print_hello",
+        "b": "gcd",
+        "c": "number_length",
+        "d": "month",
+        "e": "split_numbers",
+        "f": "modern_print",
+        "g": "can_eat",
+        "h": "is_palindrome",
+        "i": "is_prime",
+        "j": "merge",
+    }
+}
diff --git a/tests/data/test_data_41.py b/tests/data/test_data_41.py
new file mode 100644
index 00000000..009257c7
--- /dev/null
+++ b/tests/data/test_data_41.py
@@ -0,0 +1,263 @@
+a_test_data = [
+    ("world", "Hello, world!\n", "first open test"),
+    ("Yandex", "Hello, Yandex!\n", "second open test"),
+    ("", "Hello, !\n", "empty string"),
+    (" ", "Hello,  !\n", "whitespace string"),
+    ("9", "Hello, 9!\n", "digit"),
+    ("89p13", "Hello, 89p13!\n", "mixed"),
+]
+
+b_test_data = [
+    ((12, 45), 3, "first open test"),
+    ((144, 96), 48, "second open test"),
+    ((13, 29), 1, "small co-prime"),
+    ((48, 18), 6, "a > b"),
+    ((18, 48), 6, "a < b"),
+    ((7, 7), 7, "the same prime"),
+    ((100, 100), 100, "the same non prime"),
+    ((12**21, 13**23), 1, "huge co-prime numbers"),
+]
+
+c_test_data = [
+    (12345, 5, "first open test"),
+    (-100500, 6, "second open test"),
+    (10**82, 83, "long int"),
+    (-1 * 10**82, 83, "negative long int"),
+    (0, 1, "zero"),
+    (1, 1, "one"),
+    (10**882, 883, "long long int"),
+]
+
+d_test_data = [
+    ((1, "en"), "January", "first open test"),
+    ((7, "ru"), "Июль", "second open test"),
+    ((2, "en"), "February", "february_en"),
+    ((3, "en"), "March", "march_en"),
+    ((4, "en"), "April", "april_en"),
+    ((5, "en"), "May", "may_en"),
+    ((6, "en"), "June", "june_en"),
+    ((8, "en"), "August", "august_en"),
+    ((9, "en"), "September", "september_en"),
+    ((10, "en"), "October", "october_en"),
+    ((11, "en"), "November", "november_en"),
+    ((12, "en"), "December", "december_en"),
+    ((0, "en"), "", "empty_en"),
+    ((0, "ru"), "", "empty_ru"),
+    ((2, "ru"), "Февраль", "fevral_ru"),
+    ((3, "ru"), "Март", "mart_ru"),
+    ((4, "ru"), "Апрель", "aprel_ru"),
+    ((5, "ru"), "Май", "may_ru"),
+    ((6, "ru"), "Июнь", "iyun_ru"),
+    ((8, "ru"), "Август", "avgust_ru"),
+    ((9, "ru"), "Сентябрь", "sentabr_ru"),
+    ((10, "ru"), "Октябрь", "oktyabr_ru"),
+    ((11, "ru"), "Ноябрь", "noyabr_ru"),
+    ((12, "ru"), "Декабрь", "dekabr_ru"),
+    (("2", "en"), "February", "february_en_string"),
+    (("3", "en"), "March", "march_en_string"),
+    (("4", "en"), "April", "april_en_string"),
+    (("5", "en"), "May", "may_en_string"),
+    (("6", "en"), "June", "june_en_string"),
+    (("8", "en"), "August", "august_en_string"),
+    (("9", "en"), "September", "september_en_string"),
+    (("10", "en"), "October", "october_en_string"),
+    (("11", "en"), "November", "november_en_string"),
+    (("12", "en"), "December", "december_en_string"),
+    (("2", "ru"), "Февраль", "fevral_ru_string"),
+    (("3", "ru"), "Март", "mart_ru_string"),
+    (("4", "ru"), "Апрель", "aprel_ru_string"),
+    (("5", "ru"), "Май", "may_ru_string"),
+    (("6", "ru"), "Июнь", "iyun_ru_string"),
+    (("8", "ru"), "Август", "avgust_ru_string"),
+    (("9", "ru"), "Сентябрь", "sentabr_ru_string"),
+    (("10", "ru"), "Октябрь", "oktyabr_ru_string"),
+    (("11", "ru"), "Ноябрь", "noyabr_ru_string"),
+    (("12", "ru"), "Декабрь", "dekabr_ru_string"),
+]
+
+e_test_data = [
+    ("1 2 3 4 5", (1, 2, 3, 4, 5), "first open test"),
+    ("1 -2 3 -4 5", (1, -2, 3, -4, 5), "second open test"),
+    ("1 1 1 1 1 1", (1, 1, 1, 1, 1, 1), "the same numbers"),
+    ("0", (0,), "one number"),
+    ("0 " * 10**5, tuple(map(int, str.split("0 " * 10**5))), "big line"),
+    (
+        "-99 " * 10**5,
+        tuple(map(int, str.split("-99 " * 10**5))),
+        "big negative line",
+    ),
+]
+
+f_test_data = [
+    (
+        ("Hello!", "Hello!", "How do you do?", "Hello!"),
+        "Hello!\nHow do you do?\n",
+        "first open test",
+    ),
+    (
+        (
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+        ),
+        "Ало!\nЯ тебя не слышу\nПозвони когда сможешь\n",
+        "second open test",
+    ),
+    (
+        (
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Я тебя не слышу",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+            "Ало!",
+            "Ало!",
+            "Позвони когда сможешь",
+            "Позвони когда сможешь",
+            "Я тебя не слышу",
+        ),
+        "Ало!\nЯ тебя не слышу\nПозвони когда сможешь\n",
+        "second open test max-mode",
+    ),
+]
+
+g_test_data = [
+    (((2, 1), (4, 2)), True, "first open test"),
+    (((5, 5), (6, 6)), False, "second open test"),
+    (
+        ((21231415, 3312421321325), (64213132421, 6641231654)),
+        False,
+        "random numbers",
+    ),
+    (((1, 5), (2, 8)), False, "one"),
+    (((5, 5), (6, 7)), True, "capture one"),
+    (((5, 5), (6, 3)), True, "capture two"),
+    (((5, 5), (7, 6)), True, "capture three"),
+    (((5, 5), (3, 6)), True, "capture four"),
+]
+
+h_test_data = [
+    (123, False, "first open test"),
+    ([1, 2, 1, 2, 1], True, "second open test"),
+    ([1, 2, 1, 2, 1, 212], False, "List[int]"),
+    ((1, 2, 1, 2, 1), True, "Tuple[int] true"),
+    ((1, 2, 1, 2, 1, 99), False, "Tuple[int] false"),
+    (["1", "2", "1", "2", "1"], True, "List[str] true"),
+    (["1", "2", "1", "2", "1", "212"], False, "List[str] false"),
+    (("1", "2", "1", "2", "1"), True, "Tuple[int] true"),
+    (("1", "2", "1", "2", "1", "99"), False, "Tuple[str] false"),
+    ("RaCecAr", True, "racecar string camel"),
+    (["R", "a", "c", "E", "C", "a", "r"], True, "racecar list"),
+    (("R", "a", "c", "E", "C", "a", "r"), True, "racecar tuple"),
+    ("RaCecArs", False, "racecars string camel"),
+    (["RaCecArs"], True, "racecars List[str] one element true"),
+    (("RaCecArs",), True, "racecars Tuple[str] one element true"),
+    (["RaCecAr"], True, "racecars List[str] one element true"),
+    (("RaCecAr",), True, "racecars Tuple[str] one element true"),
+    ((123454321,), True, "Tuple[int] one number true"),
+    ((123454321, 123454321), True, "Tuple[int] true"),
+    ((123454321, 1234543212), False, "Tuple[int] true"),
+    ((-123454321,), True, "Tuple[-int] true"),
+    ([-123454321], True, "List[-int] true"),
+    (-123454321, False, "-int false"),
+]
+
+i_test_data = [
+    (1001459, True, "first open test"),
+    (79701, False, "second open test"),
+    (1, False, "one"),
+    (2, True, "small prime 2"),
+    (3, True, "small prime 3"),
+    (5, True, "small prime 5"),
+    (10**9, False, "big even number"),
+    (10000000019, True, "big prime"),
+    (25, False, "odd number"),
+]
+
+j_test_data = [
+    (((1, 2), (3, 4, 5)), (1, 2, 3, 4, 5), "first open test"),
+    (((7, 12), (1, 9, 50)), (1, 7, 9, 12, 50), "second open test"),
+    (((1, 2, 3), (4, 5)), (1, 2, 3, 4, 5), "first longer"),
+    (
+        ((7, 12, 13, 49), (1, 9, 50)),
+        (1, 7, 9, 12, 13, 49, 50),
+        "first longer mix",
+    ),
+    (
+        ((7, 12, 13, 49), (1, 4, 9, 50)),
+        (1, 4, 7, 9, 12, 13, 49, 50),
+        "same length",
+    ),
+    (((1, 2, 3), (4, 5, 6)), (1, 2, 3, 4, 5, 6), "same length decreasing"),
+    (((-3, -2, -1), (0,)), (-3, -2, -1, 0), "negative numbers and one lement"),
+    (((0,), (-3, -2, -1)), (-3, -2, -1, 0), "one lement and negative numbers"),
+    (
+        ((0, 100, 2000, 3000), (-3, -2, -1, 30001)),
+        (-3, -2, -1, 0, 100, 2000, 3000, 30001),
+        "mix of numbers",
+    ),
+    (
+        (tuple(range(10**4)), tuple(range(10**4))),
+        tuple(sorted(list(range(10**4)) + list(range(10**4)))),
+        "the same tuples",
+    ),
+]