---
# System prepended metadata

title: Практика 9. Помилки та винятки. Обробка винятків
tags: [Python Advanced]

---

---
tags: Python Advanced
---

# Практика 9. Помилки та винятки. Обробка винятків

## Базовий рівень. Просте налагодження програм

Для кожного завдання:

* використовуючи візуальний перегляд коду або відладчик в IDE:
    * розберіться в алгоритмі розв'язання;
    * знайдіть номери рядків, у яких є помилки, та вкажіть їх у рядку документації;
* виправте помилки, мінімально змінивши код (зазвичай, замінивши рядок на рядок); помилки виправляються за ходом програми, зверху вниз.
 
Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл): !!!
"""


def even_mult(nums):
    """Повернути добуток парних чисел у списку 'nums'."""
    p = 0
    for item in nums:
        if item % 2 != 0:
            p *= item

    return p
```

Виправлена програма:

```python=
"""
Помилки (номери рядків через пробіл): 8 10
"""


def even_mult(nums):
    """Повернути добуток парних чисел у списку 'nums'."""
    p = 1
    for item in nums:
        if item % 2 == 0:
            p *= item

    return p
```

### Задача 1

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def sum_of_digits(n):
    """Повернути суму цифр менших 5 для позитивного цілого числа `n`.
    Якщо таких цифр немає, повернути 0."""
    с = 0
    while n > 0:
        digit = n // 10
        if digit < 5:
            c = с + 1
        n //= 10
    return digit
```

:::spoiler Виправлена програма:
![](https://i.imgur.com/k1wPtkW.png) 
<!--
-->
:::

### Задача 2

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def primes(a, b):
    """Повернути список простих чисел на відрізку від 'a' до 'b'."""
    res = []
    c = 0
    for i in range(a, b):
        for j in range(i):
            if i % (j + 1) == 0:
                c += 1
            if c == 2:
                res.append(i)

    return res
```

:::spoiler Виправлена програма:
![](https://i.imgur.com/ITGKYen.png) 
<!--
-->
:::

### Задача 3

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def first_vacant_row(seats):
    """Повернути перший ряд, в якому є найбільше
    вільних місць та їх кількість.

    Повертається нумерація рядів із 1. Якщо вільних місць немає, повернути 0, 0.

    Параметры:
        - seats (list of list): інформація про продані квитки
                                (1 - продано, 0 - ні).

    Результат:
        - tuple (ряд, кількість місць).
    """
    max_count = 0
    max_row = 0
    for row_index, row in enumerate(seats):
        available_seats_count = row.count(0)  # 0 - пусто
        if available_seats_count >= max_count:
            max_row = row_index
            max_count = available_seats_count

    return max_row, max_count
```

:::spoiler Виправлена програма:
![](https://i.imgur.com/FKAZAqf.jpg) 
<!--
-->
:::

### Задача 4

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def min_pair(nums):
    """Повернути мінімальну суму сусідніх 2-х чисел у списку 'nums'."""
    min = nums[0] * nums[1]
    for i in range(2, len(nums)):
        min = min(nums[i] + nums[i + 1], min)

    return min

# import random
#
# random.seed(50)
#
# N_MAX = 10
# RANGE_MIN = 1
# RANGE_MAX = 100
# nums = random.sample(range(RANGE_MIN, RANGE_MAX), N_MAX)
#
# print(nums)
#
# print(min_pair(nums))
```

:::spoiler Виправлена програма:
![](https://i.imgur.com/JYL9JsK.png) 
<!--
-->
:::

### Задача 5

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def non_negatives(nums):
    """Удалить из списка чисел 'nums' отрицательные элементы и вернуть
    измененный список."""
    for i in range(len(nums)):
        if nums[i] < 0:
            del nums[i]

    return nums

# import random
#
# n = 10
# nums = [round(random.uniform(-10, 10), 2) for i in range(n)]
# print(nums)
#
# non_negatives(nums)
# print(nums)
```

:::spoiler Виправлена програма:
![](https://i.imgur.com/ljGm2pA.png) 
<!--
-->
:::

## Середній рівень. Обробка винятків та cтвердження

Задачі 6-10 написані правильно, але містять місця потенційних помилок.

Для кожного завдання:

* знайдіть потенційні джерела помилок (вкажіть номери рядків у рядку документації);
* використовуючи конструкцію `try` додайте в код [обробку відповідних винятків](https://hackmd.io/@YaRo/python-exceptions#/3).

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл): !!!
"""


def avg(a, b):
    """Повернути середнє геометричне чисел 'a' та 'b'.

    Параметри:
        - a, b (int або float).

    Результат:
        - float.
    """
    return (a * b) ** 0.5


a = float(input("a = "))
b = float(input("b = "))
c = avg(a, b)
print(f"Середнє геометричне = {c:.2f}")
```

:::spoiler Виправлена програма:
```python=
"""
Помилки (номери рядків через пробіл): 15 18 19
"""


def avg(a, b):
    """Повернути середнє геометричне чисел 'a' та 'b'.

    Параметри:
        - a, b (int або float).

    Результат:
        - float.

    Винятки:
        - ValueError: обчислення неможливе.
    """
    if a * b >= 0:
        return (a * b) ** 0.5
    else:
        raise ValueError("Неможливо визначити середнє геометричне "
                         "введених чисел.")

try:
    a = float(input("a = "))
    b = float(input("b = "))
    c = avg(a, b)
    print(f"Середнє геометричне = {c:.2f}")
except ValueError as exc:
    print("Помилка:", exc, ". Перевірте введені числа.")
except Exception as exc:
    print("Помилка:", exc)
```
<!--
-->
:::

### Задача 6

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def unemployment_rate(unemployed, employed):
    """Повернути рівень безробіття (РБ) як частку від 1.

       Розрахунок за формулою: РБ = безробітні / (зайняті + безробітні).
    """
    return unemployed / (unemployed + employed)


unemployed = int(input("Введіть кількість безробітних (людей): "))
employed = int(input("Введіть кількість зайнятих (людей): "))
rate = unemployment_rate(unemployed, employed)
print(f"Рівень безробіття = {rate:.1%}")
```

:::spoiler Підказка
1.  **Аналіз потенційних помилок**:
    *   Користувач може ввести нечислові дані, що викличе `ValueError` при перетворенні `int(input(...))`.
    *   Якщо сума безробітних та зайнятих буде нуль, у функції `unemployment_rate` відбудеться ділення на нуль (`ZeroDivisionError`).

2.  **Обробка помилок**:
    *   Оберніть блок введення даних та виклику функції у `try...except`.
    *   Створіть обробники для `ValueError` (неправильне введення) та `ZeroDivisionError` (ділення на нуль).
    *   Додайте загальний `except Exception` для інших непередбачених помилок.
```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): 11 14 15
"""


def unemployment_rate(unemployed, employed):
    """Повернути рівень безробіття (РБ) як частку від 1.

       Розрахунок за формулою: РБ = безробітні / (зайняті + безробітні).
    """
    if unemployed + employed == 0:
        raise ZeroDivisionError("Загальна кількість людей не може бути нульовою.")
    return unemployed / (unemployed + employed)


try:
    unemployed = int(input("Введіть кількість безробітних (людей): "))
    employed = int(input("Введіть кількість зайнятих (людей): "))
    rate = unemployment_rate(unemployed, employed)
    print(f"Рівень безробіття = {rate:.1%}")
except ValueError:
    print("Помилка: кількість людей має бути цілим числом.")
except ZeroDivisionError as e:
    print(f"Помилка розрахунку: {e}")
except Exception as e:
    print(f"Виникла непередбачена помилка: {e}")
```
<!--
![image](https://hackmd.io/_uploads/BkxRZJHFkg.png)
-->
:::

### Задача 7

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def power(x, y=2):
    """Повернути x^y."""
    if y == 0:
        return 1
    else:
        return x * power(x, y - 1)


x = int(input("x="))
y = int(input("y="))
print(power(x, y))
```

:::spoiler Підказка
1.  **Аналіз потенційних помилок**:
    *   Функція `power` є рекурсивною. Якщо `y` буде великим, глибина рекурсії може перевищити ліміт, що спричинить `RecursionError`.
    *   Користувач може ввести нечислові дані, що викличе `ValueError`.

2.  **Обробка помилок**:
    *   Оберніть блок введення даних та виклику функції `power` у `try...except`.
    *   Додайте обробник для `ValueError`, щоб повідомити про некоректне введення.
    *   Додайте обробник для `RecursionError`, щоб повідомити про завелике значення `y`.
    *   Додайте загальний `except Exception` для інших помилок.

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): 11 14 15
"""


def power(x, y=2):
    """Повернути x^y."""
    if y == 0:
        return 1
    else:
        return x * power(x, y - 1)


try:
    x = int(input("x="))
    y = int(input("y="))
    print(power(x, y))
except ValueError:
    print("Помилка: x та y мають бути цілими числами.")
except RecursionError:
    print("Помилка: занадто велике значення y для обчислення.")
except Exception as e:
    print(f"Виникла непередбачена помилка: {e}")
```
<!--
![image](https://hackmd.io/_uploads/SJdXXyHY1e.png)
-->
:::

### Задача 8

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2):
"""


# Наведено список ПІБ. Знайдіть найпоширеніше по-батькові.
# Якщо немає по-батькові, людина не враховується в розрахунку.

n = int(input("Введіть кількість людей: "))

middle_names = {}
for i in range(n):
    fio = input("Введіть ПІБ через пробіл: ").split()

    middle_name = fio[2]
    middle_names[middle_name] = middle_names.get(middle_name, 0) + 1

print(sorted(middle_names.items(), key=lambda item: item[1])[-1][0])
print("Людей брало участь у розрахунку:", n)
```

:::spoiler Підказка
1.  **Аналіз потенційних помилок**:
    *   Якщо користувач введе неповне ПІБ (менше трьох слів), спроба доступу до `fio[2]` викличе `IndexError`.
    *   Якщо після обробки всіх введених даних словник `middle_names` залишиться порожнім (наприклад, ніхто не ввів по-батькові), спроба доступу до `[-1]` у порожньому списку також викличе `IndexError`.
    *   Введення кількості людей `n` може спричинити `ValueError`.

2.  **Обробка помилок**:
    *   Всередині циклу `for` оберніть рядок `middle_name = fio[2]` у блок `try...except IndexError`. У блоці `except` можна пропустити ітерацію (`continue`).
    *   Після циклу, перед виведенням результату, перевірте, чи не порожній словник `middle_names`. Якщо він порожній, виведіть відповідне повідомлення.
    *   Оберніть початкове введення `n` у `try...except ValueError`.

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): 9 15 18
"""


# Наведено список ПІБ. Знайдіть найпоширеніше по-батькові.
# Якщо немає по-батькові, людина не враховується в розрахунку.
try:
    n = int(input("Введіть кількість людей: "))
except ValueError:
    print("Кількість людей має бути числом.")
    n = 0

middle_names = {}
count = 0
for i in range(n):
    fio = input("Введіть ПІБ через пробіл: ").split()
    try:
        middle_name = fio[2]
        middle_names[middle_name] = middle_names.get(middle_name, 0) + 1
        count += 1
    except IndexError:
        print("Попередження: ПІБ введено не повністю, запис проігноровано.")

if middle_names:
    most_common = sorted(middle_names.items(), key=lambda item: item[1])[-1][0]
    print("Найпоширеніше по-батькові:", most_common)
else:
    print("Не було введено жодного повного ПІБ.")

print("Людей брало участь у розрахунку:", count)
```
<!--
![image](https://hackmd.io/_uploads/Bkgi2yBtJe.png)
-->
:::

### Задача 9

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""


def f(x):
    """Повернути значення функції.

    Функція не обробляє винятки.
    """
    return x**2 / (x + 2) - 3


k = int(input("Введіть межу інтервалу [-k; k]: "))
h = float(input("Введіть крок: "))

x = -k
print("x".rjust(10), "f(x)".rjust(10))
while x <= k:
    # Якщо помилка виникає під час обчислення функції,
    # Необхідно вивести тире "-"
    # Цей випадок повинен обробити другий вкладений блок try
    print(f"{x:10.2f} {f(x):10.2f}")

    x += h


# --------------
# Приклад роботи:
#
# Введіть межу інтервалу [-k; k]: 5
# Введіть крок: 0.5
#          x       f(x)
#      -5.00     -11.33
#      -4.50     -11.10
#      -4.00     -11.00
#      -3.50     -11.17
#      -3.00     -12.00
#      -2.50     -15.50
#      -2.00          -
#      -1.50       1.50
#      -1.00      -2.00
#       ...
#       4.50       0.12
#       5.00       0.57
```

:::spoiler Підказка
1.  **Аналіз потенційних помилок**:
    *   Функція `f(x)` містить ділення на `x + 2`. Якщо `x` дорівнюватиме `-2`, виникне `ZeroDivisionError`.
    *   При введенні меж інтервалу та кроку користувач може ввести нечислові дані, що спричинить `ValueError`.

2.  **Обробка помилок**:
    *   Оберніть блок введення даних у `try...except ValueError`.
    *   Всередині циклу `while` оберніть виклик функції `print(f"{x:10.2f} {f(x):10.2f}")` у вкладений блок `try...except ZeroDivisionError`.
    *   У блоці `except` для `ZeroDivisionError` виведіть рядок з прочерком замість значення функції, як зазначено в умові.

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): 11 14 15
"""


def f(x):
    """Повернути значення функції.

    Функція не обробляє винятки.
    """
    return x**2 / (x + 2) - 3


try:
    k = int(input("Введіть межу інтервалу [-k; k]: "))
    h = float(input("Введіть крок: "))

    x = -k
    print("x".rjust(10), "f(x)".rjust(10))
    while x <= k:
        # Якщо помилка виникає під час обчислення функції,
        # Необхідно вивести тире "-"
        # Цей випадок повинен обробити другий вкладений блок try
        try:
            print(f"{x:10.2f} {f(x):10.2f}")
        except ZeroDivisionError:
            print(f"{x:10.2f} {'-':>10}")

        x += h
except ValueError:
    print("Помилка: межа та крок мають бути числами.")
except Exception as e:
    print(f"Виникла непередбачена помилка: {e}")
```
<!--
![image](https://hackmd.io/_uploads/rkynmeBY1l.png)
-->
:::

### Задача 10

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): !!!
"""

# Програма містить словник результатів НМТ іспит = бал.
# Дізнайтеся, чи заявник зможе вступити, якщо вам потрібно сдати
# Усі ці іспити не менше мінімальної кількості балів.

min_grades = {"Історія": 180, "Математика": 185, "Українська мова": 175}

print(
    """Щоб визначити можливість вступу, необхідна інформація про вас.

Щоб ввести іспит і бали, введіть їх через |: Історія | 140.
Щоб завершити введення, натисніть кнопку Enter.
"""
)

exams = {}
while True:
    line = input("").strip()
    if line == "":
        break

    exam, grade = [x.strip() for x in line.split("|")]
    exams[exam] = int(grade)

print("Ваші іспити:")
for i, (exam, grade) in enumerate(exams.items(), start=1):
    print(f"{i}) {exam} {grade}")

ok = False
for exam_name, min_grade in min_grades.items():
    if exams[exam_name] < min_grade:
        break
else:
    ok = True

print("Ви можете вступити до нас!" if ok else "На жаль...")
```

:::spoiler Підказка
1.  **Аналіз потенційних помилок**:
    *   При введенні даних `exam, grade = ... line.split("|")` може виникнути `ValueError`, якщо рядок не містить `|` або містить їх забагато.
    *   При перетворенні `int(grade)` може виникнути `ValueError`, якщо бал не є числом.
    *   У циклі перевірки `if exams[exam_name] < min_grade:` може виникнути `KeyError`, якщо абітурієнт не ввів результати по одному з обов'язкових іспитів.

2.  **Обробка помилок**:
    *   Всередині циклу `while True` оберніть блок обробки рядка у `try...except ValueError`.
    *   Оберніть цикл перевірки результатів у `try...except KeyError`. Якщо виникає `KeyError`, це означає, що не всі іспити складено, тому `ok` має бути `False`.

```python=
"""
Помилки (номери рядків через пробіл, цей рядок - №2): 25 26 34
"""

# Програма містить словник результатів НМТ іспит = бал.
# Дізнайтеся, чи заявник зможе вступити, якщо вам потрібно сдати
# Усі ці іспити не менше мінімальної кількості балів.

min_grades = {"Історія": 180, "Математика": 185, "Українська мова": 175}

print(
    """Щоб визначити можливість вступу, необхідна інформація про вас.

Щоб ввести іспит і бали, введіть їх через |: Історія | 140.
Щоб завершити введення, натисніть кнопку Enter.
"""
)

exams = {}
while True:
    line = input("").strip()
    if line == "":
        break
    try:
        exam, grade = [x.strip() for x in line.split("|")]
        exams[exam] = int(grade)
    except ValueError:
        print("Помилка формату. Введіть дані як 'Іспит | Бал'." )


print("Ваші іспити:")
for i, (exam, grade) in enumerate(exams.items(), start=1):
    print(f"{i}) {exam} {grade}")

ok = False
try:
    for exam_name, min_grade in min_grades.items():
        if exams[exam_name] < min_grade:
            break
    else:
        ok = True
except KeyError as e:
    print(f"Відсутня інформація про обов'язковий іспит: {e}")


print("Ви можете вступити до нас!" if ok else "На жаль...")
```
<!--
![image](https://hackmd.io/_uploads/SkxVUdxSt1g.png)
-->
:::

## Підвищений рівень. Створення власних винятків

### Задача 11

Вихідна програма:

```python=
"""
Помилки (номери рядків через пробіл): !!!
"""


class NoMoneyToWithdrawError(Exception):
    pass


class PaymentError(Exception):
    pass


def print_accounts(accounts):
    """Друк акаунтів."""
    print(f"Список клієнтів ({len(accounts)}): ")
    for i, (name, value) in enumerate(accounts.items(), start=1):
        print(f"{i}. {name} {value}")


def transfer_money(accounts, account_from, account_to, value):
    """Виконати переказ 'value' грошей з рахунку 'account_from' на 'account_to'.

    При переказі коштів необхідно враховувати:
        - чи вистачає грошей на рахунку, з якого здійснюється переказ;
        - переказ складається зі зменшення балансу першого рахунку та збільшення
          балансу другого; якщо хоча б на одному етапі відбувається помилка,
          облікові записи повинні бути приведені в початковий стан
          (механізм транзакції)
          см. https://uk.wikipedia.org/wiki/Транзакція_(бази_даних)

    Винятки (raise):
        - NoMoneyToWithdrawError: на рахунку 'account_from'
                                  не вистачає грошей для переказу;
        - PaymentError: помилка під час переказу.
    """
    # Видаліть коментар та допишіть код


if __name__ == "__main__":
    accounts = {
        "Василь Іванов": 100,
        "Іван Васильєв": 1500,
        "Петро Бондаренко": 400
    }
    print_accounts(accounts)

    payment_info = {
        "account_from": "Василь Іванов",
        "account_to": "Іван Васильєв"
    }

    print("Переказ від {account_from} для {account_to}...".
          format(**payment_info))

    payment_info["value"] = int(input("Сума = "))

    transfer_money(accounts, **payment_info)

    print("OK!")

    print_accounts(accounts)
```

:::spoiler Підказка
1.  **Реалізація `transfer_money`**:
    *   Спочатку перевірте, чи достатньо коштів на рахунку `account_from`. Якщо ні, згенеруйте виняток `NoMoneyToWithdrawError`.
    *   Збережіть початкові баланси рахунків `account_from` та `account_to`.
    *   Оберніть операції зі зміни балансів у блок `try...except`. Це потрібно для реалізації механізму транзакції.
    *   У блоці `try` зменште баланс `account_from` і збільште баланс `account_to`.
    *   У блоці `except` відновіть початкові баланси та згенеруйте новий виняток `PaymentError`, щоб повідомити про невдалу транзакцію.

2.  **Обробка винятків у головному коді**:
    *   Оберніть виклик `transfer_money` у блок `try...except`.
    *   Додайте обробники для `NoMoneyToWithdrawError` та `PaymentError`, виводячи відповідні повідомлення.
    *   Також обробіть `ValueError` для введення суми та `KeyError`, якщо вказаних рахунків не існує.

```python=
"""
Помилки (номери рядків через пробіл): 56 58
"""


class NoMoneyToWithdrawError(Exception):
    pass


class PaymentError(Exception):
    pass


def print_accounts(accounts):
    """Друк акаунтів."""
    print(f"Список клієнтів ({len(accounts)}): ")
    for i, (name, value) in enumerate(accounts.items(), start=1):
        print(f"{i}. {name} {value}")


def transfer_money(accounts, account_from, account_to, value):
    """Виконати переказ 'value' грошей з рахунку 'account_from' на 'account_to'.

    При переказі коштів необхідно враховувати:
        - чи вистачає грошей на рахунку, з якого здійснюється переказ;
        - переказ складається зі зменшення балансу першого рахунку та збільшення
          балансу другого; якщо хоча б на одному етапі відбувається помилка,
          облікові записи повинні бути приведені в початковий стан
          (механізм транзакції)
          см. https://uk.wikipedia.org/wiki/Транзакція_(бази_даних)

    Винятки (raise):
        - NoMoneyToWithdrawError: на рахунку 'account_from'
                                  не вистачає грошей для переказу;
        - PaymentError: помилка під час переказу.
    """
    if accounts[account_from] < value:
        raise NoMoneyToWithdrawError(f"На рахунку {account_from} недостатньо коштів.")

    # Зберігаємо стан для можливого відкату
    original_from_balance = accounts[account_from]
    original_to_balance = accounts[account_to]

    try:
        accounts[account_from] -= value
        # Для імітації помилки можна додати:
        # if account_to == "Іван Васильєв":
        #     raise Exception("Помилка зв'язку з банком одержувача")
        accounts[account_to] += value
    except Exception as e:
        # Відкат транзакції
        accounts[account_from] = original_from_balance
        accounts[account_to] = original_to_balance
        raise PaymentError(f"Не вдалося виконати переказ. Причина: {e}")


if __name__ == "__main__":
    accounts = {
        "Василь Іванов": 100,
        "Іван Васильєв": 1500,
        "Петро Бондаренко": 400
    }
    print_accounts(accounts)

    payment_info = {
        "account_from": "Василь Іванов",
        "account_to": "Іван Васильєв"
    }

    print("Переказ від {account_from} для {account_to}...".
          format(**payment_info))
    try:
        payment_info["value"] = int(input("Сума = "))

        transfer_money(accounts, **payment_info)

        print("OK!")

    except ValueError:
        print("Помилка: сума має бути цілим числом.")
    except KeyError as e:
        print(f"Помилка: рахунок {e} не знайдено.")
    except NoMoneyToWithdrawError as e:
        print(f"Помилка переказу: {e}")
    except PaymentError as e:
        print(f"Помилка платежу: {e}")
    except Exception as e:
        print(f"Виникла непередбачена помилка: {e}")
    finally:
        print_accounts(accounts)
```
<!--
-->
:::

### Задача 12

Створіть систему валідації користувацьких даних з власними винятками.

Вихідна програма:

```python=
"""
Створити систему реєстрації користувача з перевіркою даних.
Використати власні винятки для різних типів помилок.
"""


class ValidationError(Exception):
    """Базовий клас для помилок валідації."""
    pass


class InvalidEmailError(ValidationError):
    """Виняток для невалідної електронної пошти."""
    pass


class WeakPasswordError(ValidationError):
    """Виняток для слабкого пароля."""
    pass


class InvalidAgeError(ValidationError):
    """Виняток для невалідного віку."""
    pass


def validate_email(email):
    """Перевірити формат електронної пошти.
    
    Параметри:
        - email (str): адреса електронної пошти.
    
    Винятки:
        - InvalidEmailError: якщо email не містить '@' або '.':
    """
    # Допишіть код


def validate_password(password):
    """Перевірити надійність пароля.
    
    Параметри:
        - password (str): пароль користувача.
    
    Винятки:
        - WeakPasswordError: якщо пароль коротший 8 символів
                            або не містить цифр.
    """
    # Допишіть код


def validate_age(age):
    """Перевірити вік користувача.
    
    Параметри:
        - age (int): вік користувача.
    
    Винятки:
        - InvalidAgeError: якщо вік менше 14 або більше 120.
    """
    # Допишіть код


def register_user(email, password, age):
    """Зареєструвати користувача після валідації всіх даних.
    
    Параметри:
        - email (str): електронна пошта.
        - password (str): пароль.
        - age (int): вік.
    
    Повертає:
        - dict: дані користувача.
    """
    # Допишіть код


if __name__ == "__main__":
    print("=== Реєстрація користувача ===")
    
    email = input("Email: ")
    password = input("Пароль: ")
    age_str = input("Вік: ")
    
    # Додайте обробку винятків
    # Допишіть код
```

:::spoiler Підказка
1. **Реалізація функцій валідації**:
   * У `validate_email` перевірте наявність '@' та '.' у рядку. Якщо їх немає, згенеруйте `InvalidEmailError`.
   * У `validate_password` перевірте довжину (>= 8) та наявність хоча б однієї цифри (`any(c.isdigit() for c in password)`). Якщо умови не виконані, згенеруйте `WeakPasswordError`.
   * У `validate_age` перевірте діапазон 14-120. Якщо вік поза діапазоном, згенеруйте `InvalidAgeError`.

2. **Реалізація `register_user`**:
   * Послідовно викликайте всі три функції валідації.
   * Якщо всі перевірки пройдено, поверніть словник з даними користувача.

3. **Обробка винятків у головному коді**:
   * Оберніть код реєстрації у `try...except`.
   * Додайте окремі обробники для `InvalidEmailError`, `WeakPasswordError`, `InvalidAgeError`.
   * Обробіть `ValueError` для перетворення віку.
   * Додайте загальний обробник для інших винятків.

```python=
"""
Створити систему реєстрації користувача з перевіркою даних.
Використати власні винятки для різних типів помилок.
"""


class ValidationError(Exception):
    """Базовий клас для помилок валідації."""
    pass


class InvalidEmailError(ValidationError):
    """Виняток для невалідної електронної пошти."""
    pass


class WeakPasswordError(ValidationError):
    """Виняток для слабкого пароля."""
    pass


class InvalidAgeError(ValidationError):
    """Виняток для невалідного віку."""
    pass


def validate_email(email):
    """Перевірити формат електронної пошти.
    
    Параметри:
        - email (str): адреса електронної пошти.
    
    Винятки:
        - InvalidEmailError: якщо email не містить '@' або '.':
    """
    if '@' not in email or '.' not in email:
        raise InvalidEmailError("Email має містити символи '@' та '.'")


def validate_password(password):
    """Перевірити надійність пароля.
    
    Параметри:
        - password (str): пароль користувача.
    
    Винятки:
        - WeakPasswordError: якщо пароль коротший 8 символів
                            або не містить цифр.
    """
    if len(password) < 8:
        raise WeakPasswordError("Пароль має містити щонайменше 8 символів")
    
    if not any(c.isdigit() for c in password):
        raise WeakPasswordError("Пароль має містити щонайменше одну цифру")


def validate_age(age):
    """Перевірити вік користувача.
    
    Параметри:
        - age (int): вік користувача.
    
    Винятки:
        - InvalidAgeError: якщо вік менше 14 або більше 120.
    """
    if age < 14 or age > 120:
        raise InvalidAgeError("Вік має бути в діапазоні від 14 до 120 років")


def register_user(email, password, age):
    """Зареєструвати користувача після валідації всіх даних.
    
    Параметри:
        - email (str): електронна пошта.
        - password (str): пароль.
        - age (int): вік.
    
    Повертає:
        - dict: дані користувача.
    """
    validate_email(email)
    validate_password(password)
    validate_age(age)
    
    return {
        "email": email,
        "age": age,
        "status": "зареєстровано"
    }


if __name__ == "__main__":
    print("=== Реєстрація користувача ===")
    
    email = input("Email: ")
    password = input("Пароль: ")
    age_str = input("Вік: ")
    
    try:
        age = int(age_str)
        user = register_user(email, password, age)
        print(f"\n✓ Користувача успішно зареєстровано!")
        print(f"  Email: {user['email']}")
        print(f"  Вік: {user['age']}")
        
    except ValueError:
        print("✗ Помилка: вік має бути цілим числом")
    except InvalidEmailError as e:
        print(f"✗ Помилка email: {e}")
    except WeakPasswordError as e:
        print(f"✗ Помилка пароля: {e}")
    except InvalidAgeError as e:
        print(f"✗ Помилка віку: {e}")
    except ValidationError as e:
        print(f"✗ Помилка валідації: {e}")
    except Exception as e:
        print(f"✗ Непередбачена помилка: {e}")
```
<!--
-->
:::

### Задача 13

Створіть систему управління бібліотечним каталогом з обробкою складних ситуацій.

Вихідна програма:

```python=
"""
Створити систему управління бібліотекою з можливістю
видачі та повернення книг з обробкою різних виняткових ситуацій.
"""


class LibraryError(Exception):
    """Базовий клас для помилок бібліотеки."""
    pass


class BookNotFoundError(LibraryError):
    """Виняток: книга не знайдена в каталозі."""
    pass


class BookNotAvailableError(LibraryError):
    """Виняток: всі примірники книги видані."""
    pass


class BookAlreadyReturnedError(LibraryError):
    """Виняток: книга вже повернута або не була видана."""
    pass


class InvalidQuantityError(LibraryError):
    """Виняток: некоректна кількість книг."""
    pass


class Library:
    """Клас для управління бібліотекою."""
    
    def __init__(self):
        """Ініціалізація бібліотеки.
        
        Структура каталогу:
        {
            "назва_книги": {
                "total": загальна_кількість,
                "available": доступна_кількість,
                "borrowed_by": [список_користувачів]
            }
        }
        """
        self.catalog = {}
    
    def add_book(self, title, quantity=1):
        """Додати книгу до каталогу.
        
        Параметри:
            - title (str): назва книги.
            - quantity (int): кількість примірників.
        
        Винятки:
            - InvalidQuantityError: якщо quantity <= 0.
        """
        # Допишіть код
    
    def borrow_book(self, title, user):
        """Видати книгу користувачу.
        
        Параметри:
            - title (str): назва книги.
            - user (str): ім'я користувача.
        
        Винятки:
            - BookNotFoundError: книга відсутня в каталозі.
            - BookNotAvailableError: всі примірники видані.
        """
        # Допишіть код
    
    def return_book(self, title, user):
        """Прийняти повернення книги.
        
        Параметри:
            - title (str): назва книги.
            - user (str): ім'я користувача.
        
        Винятки:
            - BookNotFoundError: книга відсутня в каталозі.
            - BookAlreadyReturnedError: користувач не брав цю книгу.
        """
        # Допишіть код
    
    def show_catalog(self):
        """Показати весь каталог."""
        # Допишіть код


if __name__ == "__main__":
    library = Library()
    
    # Приклад використання
    print("=== Система управління бібліотекою ===\n")
    
    # Додайте обробку винятків для наступних операцій:
    # 1. Додати книги до каталогу
    # 2. Видати книгу користувачу
    # 3. Повернути книгу
    # 4. Показати каталог
    
    # Допишіть код
```

:::spoiler Підказка
1. **Реалізація методу `add_book`**:
   * Перевірте, що `quantity > 0`, інакше згенеруйте `InvalidQuantityError`.
   * Якщо книга вже є в каталозі, збільште `total` та `available` на `quantity`.
   * Якщо книга нова, створіть новий запис у словнику.

2. **Реалізація методу `borrow_book`**:
   * Перевірте наявність книги в каталозі, інакше згенеруйте `BookNotFoundError`.
   * Перевірте, що є доступні примірники (`available > 0`), інакше згенеруйте `BookNotAvailableError`.
   * Зменште `available` на 1 та додайте користувача до списку `borrowed_by`.

3. **Реалізація методу `return_book`**:
   * Перевірте наявність книги в каталозі.
   * Перевірте, що користувач є у списку `borrowed_by`, інакше згенеруйте `BookAlreadyReturnedError`.
   * Збільште `available` на 1 та видаліть користувача зі списку.

4. **Головний код**:
   * Оберніть всі операції з бібліотекою у `try...except` блоки.
   * Створіть окремі обробники для кожного типу винятків.

<!--
```python=
"""
Створити систему управління бібліотекою з можливістю
видачі та повернення книг з обробкою різних виняткових ситуацій.
"""


class LibraryError(Exception):
    """Базовий клас для помилок бібліотеки."""
    pass


class BookNotFoundError(LibraryError):
    """Виняток: книга не знайдена в каталозі."""
    pass


class BookNotAvailableError(LibraryError):
    """Виняток: всі примірники книги видані."""
    pass


class BookAlreadyReturnedError(LibraryError):
    """Виняток: книга вже повернута або не була видана."""
    pass


class InvalidQuantityError(LibraryError):
    """Виняток: некоректна кількість книг."""
    pass


class Library:
    """Клас для управління бібліотекою."""
    
    def __init__(self):
        """Ініціалізація бібліотеки.
        
        Структура каталогу:
        {
            "назва_книги": {
                "total": загальна_кількість,
                "available": доступна_кількість,
                "borrowed_by": [список_користувачів]
            }
        }
        """
        self.catalog = {}
    
    def add_book(self, title, quantity=1):
        """Додати книгу до каталогу.
        
        Параметри:
            - title (str): назва книги.
            - quantity (int): кількість примірників.
        
        Винятки:
            - InvalidQuantityError: якщо quantity <= 0.
        """
        if quantity <= 0:
            raise InvalidQuantityError("Кількість книг має бути більше 0")
        
        if title in self.catalog:
            self.catalog[title]["total"] += quantity
            self.catalog[title]["available"] += quantity
        else:
            self.catalog[title] = {
                "total": quantity,
                "available": quantity,
                "borrowed_by": []
            }
        
        print(f"✓ Додано '{title}' ({quantity} шт.)")
    
    def borrow_book(self, title, user):
        """Видати книгу користувачу.
        
        Параметри:
            - title (str): назва книги.
            - user (str): ім'я користувача.
        
        Винятки:
            - BookNotFoundError: книга відсутня в каталозі.
            - BookNotAvailableError: всі примірники видані.
        """
        if title not in self.catalog:
            raise BookNotFoundError(f"Книга '{title}' відсутня в каталозі")
        
        if self.catalog[title]["available"] == 0:
            raise BookNotAvailableError(
                f"Всі примірники '{title}' видані. "
                f"Книгу взяли: {', '.join(self.catalog[title]['borrowed_by'])}"
            )
        
        self.catalog[title]["available"] -= 1
        self.catalog[title]["borrowed_by"].append(user)
        print(f"✓ '{title}' видано користувачу {user}")
    
    def return_book(self, title, user):
        """Прийняти повернення книги.
        
        Параметри:
            - title (str): назва книги.
            - user (str): ім'я користувача.
        
        Винятки:
            - BookNotFoundError: книга відсутня в каталозі.
            - BookAlreadyReturnedError: користувач не брав цю книгу.
        """
        if title not in self.catalog:
            raise BookNotFoundError(f"Книга '{title}' відсутня в каталозі")
        
        if user not in self.catalog[title]["borrowed_by"]:
            raise BookAlreadyReturnedError(
                f"Користувач {user} не брав книгу '{title}' "
                f"або вже її повернув"
            )
        
        self.catalog[title]["available"] += 1
        self.catalog[title]["borrowed_by"].remove(user)
        print(f"✓ Користувач {user} повернув '{title}'")
    
    def show_catalog(self):
        """Показати весь каталог."""
        if not self.catalog:
            print("Каталог порожній")
            return
        
        print("\n=== Каталог бібліотеки ===")
        for i, (title, info) in enumerate(self.catalog.items(), start=1):
            status = f"{info['available']}/{info['total']} доступно"
            borrowed = ""
            if info['borrowed_by']:
                borrowed = f" (взяли: {', '.join(info['borrowed_by'])})"
            print(f"{i}. {title}: {status}{borrowed}")
        print()


if __name__ == "__main__":
    library = Library()
    
    print("=== Система управління бібліотекою ===\n")
    
    # Демонстрація роботи з обробкою винятків
    
    # 1. Додавання книг
    try:
        library.add_book("Python для початківців", 3)
        library.add_book("Алгоритми та структури даних", 2)
        library.add_book("Чисте програмування", 1)
    except InvalidQuantityError as e:
        print(f"✗ Помилка: {e}")
    
    library.show_catalog()
    
    # 2. Видача книг
    try:
        library.borrow_book("Python для початківців", "Іван")
        library.borrow_book("Python для початківців", "Марія")
        library.borrow_book("Чисте програмування", "Петро")
    except BookNotFoundError as e:
        print(f"✗ {e}")
    except BookNotAvailableError as e:
        print(f"✗ {e}")
    
    library.show_catalog()
    
    # 3. Спроба взяти книгу, якої немає в наявності
    try:
        library.borrow_book("Чисте програмування", "Олена")
    except BookNotAvailableError as e:
        print(f"✗ {e}\n")
    
    # 4. Повернення книги
    try:
        library.return_book("Python для початківців", "Іван")
    except BookAlreadyReturnedError as e:
        print(f"✗ {e}")
    except BookNotFoundError as e:
        print(f"✗ {e}")
    
    library.show_catalog()
    
    # 5. Спроба повернути книгу, яку не брали
    try:
        library.return_book("Python для початківців", "Іван")
    except BookAlreadyReturnedError as e:
        print(f"✗ {e}\n")
    
    # 6. Спроба роботи з неіснуючою книгою
    try:
        library.borrow_book("Неіснуюча книга", "Василь")
    except BookNotFoundError as e:
        print(f"✗ {e}\n")
    
    # Загальна обробка помилок
    try:
        library.add_book("Нова книга", -5)
    except LibraryError as e:
        print(f"✗ Помилка бібліотеки: {e}")
    except Exception as e:
        print(f"✗ Непередбачена помилка: {e}")
```
-->
:::

---

(c) Яценко Р.М., 2021-2026 [Python Advanced]

[Навчальний центр комп'ютерних технологій](http://kit.kh.ua/) [![kit](https://i.imgur.com/Kh901c1.png =10%x)](http://kit.kh.ua/)
