{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Практикум по ООП - Лекции 6 и 7\n\nВсе темы объектно-ориентированного программирования из лекций 6 и 7: классы, наследование, полиморфизм, инкапсуляция, методы уровня класса и магические методы.\n\n**Как работать на занятии:**\n1. Прочитайте короткое напоминание темы.\n2. Запустите ячейку с примером и разберите вывод.\n3. Выполните задание в пустой ячейке. Сверьте результат с `# Ожидаемый вывод`.\n4. В конце - мини-проект «Платёжная карта», объединяющий все темы.\n\nИспользуется только стандартная библиотека Python."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Содержание\n\n1. Класс и экземпляр  ·  *Лекция 6*\n2. Методы - действия объекта  ·  *Лекция 6*\n3. Метод __str__  ·  *Лекция 6*\n4. Наследование и super()  ·  *Лекция 6*\n5. Полиморфизм - переопределение методов  ·  *Лекция 7*\n6. Утиная типизация (Duck Typing)  ·  *Лекция 7*\n7. MRO - порядок разрешения методов  ·  *Лекция 7*\n8. Инкапсуляция: _ и __  ·  *Лекция 7*\n9. @property и сеттер с валидацией  ·  *Лекция 7*\n10. Вычисляемые свойства  ·  *Лекция 7*\n11. Атрибут класса vs атрибут экземпляра  ·  *Лекция 7*\n12. @classmethod - фабричные конструкторы  ·  *Лекция 7*\n13. @staticmethod - утилиты внутри класса  ·  *Лекция 7*\n14. Магические методы (dunder)  ·  *Лекция 7*\n\n**Капстоун.** Мини-проект «Платёжная карта» - все темы вместе."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 1. Класс и экземпляр\n\n*Лекция 6*\n\n**Класс** - описание (чертёж): какие данные и какое поведение. **Экземпляр** - конкретный объект, созданный по классу. По одному классу можно создать сколько угодно независимых объектов."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Client:\n    def __init__(self, name, city):\n        self.name = name\n        self.city = city\n\n\na = Client(\"Олег\", \"Ташкент\")\nb = Client(\"Asel\", \"Самарканд\")\nprint(a.name, \"-\", a.city)\nprint(b.name, \"-\", b.city)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 1\n\nСоздайте класс `Card` с атрибутами `holder` и `balance`. Создайте **две** карты с разными значениями и выведите атрибуты каждой."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Олег 50000\n#   Asel 120000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 2. Методы - действия объекта\n\n*Лекция 6*\n\n**Метод** - функция внутри класса. Первый параметр `self` - сам объект. Метод может читать и менять атрибуты этого объекта."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Account:\n    def __init__(self, owner, balance):\n        self.owner = owner\n        self.balance = balance\n\n    def deposit(self, amount):\n        self.balance += amount\n\n    def withdraw(self, amount):\n        self.balance -= amount\n\n\nacc = Account(\"Олег\", 10000)\nacc.deposit(5000)\nacc.withdraw(3000)\nprint(acc.balance)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 2\n\nДобавьте в класс `Card` метод `pay(self, amount)`, уменьшающий `balance` на `amount`. Создайте карту с балансом 100000, проведите две оплаты (30000 и 25000) и выведите остаток."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   45000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 3. Метод __str__\n\n*Лекция 6*\n\nМетод `__str__` задаёт читаемое представление объекта. Его вызывают `print(obj)` и `str(obj)`. Без него print покажет адрес в памяти."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Account:\n    def __init__(self, owner, balance):\n        self.owner = owner\n        self.balance = balance\n\n    def __str__(self):\n        return f\"Счёт {self.owner}: {self.balance} сум\"\n\n\nprint(Account(\"Олег\", 12000))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 3\n\nДобавьте в `Card` метод `__str__`, возвращающий строку вида `Карта Олег - 45000 сум`. Выведите объект через `print()`."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Карта Олег - 45000 сум"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 4. Наследование и super()\n\n*Лекция 6*\n\nКласс-наследник переиспользует код родителя. `super().__init__(...)` вызывает конструктор родителя - чтобы не дублировать установку общих атрибутов."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Card:\n    def __init__(self, holder, balance):\n        self.holder = holder\n        self.balance = balance\n\n\nclass PremiumCard(Card):\n    def __init__(self, holder, balance, cashback):\n        super().__init__(holder, balance)   # атрибуты родителя\n        self.cashback = cashback            # своё поле\n\n\nvip = PremiumCard(\"Asel\", 200000, 0.05)\nprint(vip.holder, vip.balance, vip.cashback)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 4\n\nНа основе `Card` создайте класс `CorporateCard` с дополнительным атрибутом `company`. В `__init__` сначала вызовите `super().__init__(...)`. Создайте объект и выведите `holder` и `company`."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Олег ООО \"Барака\""
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 5. Полиморфизм - переопределение методов\n\n*Лекция 7*\n\n**Полиморфизм** - один и тот же вызов работает для разных классов. Наследник задаёт свою версию метода (переопределение). Вызывающий код не знает конкретный класс."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class PaymentGateway:\n    def process(self, amount):\n        print(\"Базовая обработка:\", amount)\n\n\nclass UzCardGateway(PaymentGateway):\n    def process(self, amount):\n        print(\"UzCard списывает:\", amount)\n\n\nclass HumoGateway(PaymentGateway):\n    def process(self, amount):\n        print(\"Humo списывает:\", amount)\n\n\nfor gw in [UzCardGateway(), HumoGateway()]:\n    gw.process(50000)        # вызов один - поведение разное"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 5\n\nДобавьте класс `MillyBankGateway` - наследник `PaymentGateway`. Переопределите `process`, чтобы он печатал `Milly Bank: онлайн-платёж {amount}`. Прогоните `UzCardGateway` и `MillyBankGateway` в одном цикле с суммой 75000."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   UzCard списывает: 75000\n#   Milly Bank: онлайн-платёж 75000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 6. Утиная типизация (Duck Typing)\n\n*Лекция 7*\n\n**Утиная типизация**: наследование не обязательно. Если у объекта есть нужный метод - он подойдёт. «Крякает как утка - значит утка»."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class ApplePay:\n    def charge(self, amount):\n        print(\"Apple Pay:\", amount)\n\n\nclass GooglePay:\n    def charge(self, amount):\n        print(\"Google Pay:\", amount)\n\n\ndef pay(service, amount):\n    service.charge(amount)      # важно лишь наличие метода charge()\n\n\npay(ApplePay(), 50000)\npay(GooglePay(), 50000)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 6\n\nСоздайте класс `QrPayService` с методом `charge(self, amount)`, печатающим `QR: оплата {amount}`. Класс ничего **не наследует**. Передайте объект в функцию `pay` с суммой 40000."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   QR: оплата 40000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 7. MRO - порядок разрешения методов\n\n*Лекция 7*\n\nПри множественном наследовании Python ищет метод по списку `__mro__` слева направо. Порядок родителей в `class C(A, B)` определяет, чей метод сработает первым."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Base:\n    def check(self):\n        print(\"Base\")\n\n\nclass Left(Base):\n    def check(self):\n        print(\"Left\")\n\n\nclass Right(Base):\n    def check(self):\n        print(\"Right\")\n\n\nclass Both(Left, Right):\n    pass\n\n\nBoth().check()                              # Left - он первый по MRO\nprint([c.__name__ for c in Both.__mro__])"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 7\n\nПоменяйте порядок родителей: `class Both(Right, Left)`. Сначала **предположите** в комментарии, что выведет `Both().check()`, затем запустите и проверьте `__mro__`."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Right\n#   ['Both', 'Right', 'Left', 'Base', 'object']"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 8. Инкапсуляция: _ и __\n\n*Лекция 7*\n\n`_имя` - соглашение «внутреннее, снаружи не трогать». `__имя` - Name Mangling: Python переименовывает атрибут в `_Класс__имя`, поэтому прямой доступ `obj.__имя` даёт ошибку."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class User:\n    def __init__(self, role, pin):\n        self._role = role          # внутреннее по соглашению\n        self.__pin = pin           # name mangling\n\n\nu = User(\"client\", \"7721\")\nprint(u._role)                     # технически доступно\ntry:\n    print(u.__pin)                 # имени __pin не существует\nexcept AttributeError as e:\n    print(\"Ошибка:\", e)\nprint(u._User__pin)                # реальное имя после mangling"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 8\n\nСоздайте класс `Account` с публичным атрибутом `owner` и приватным `__balance` (начальное значение 0). Выведите `owner`. Через `try/except` убедитесь, что обращение к `acc.__balance` вызывает `AttributeError`. Затем выведите реальное имя атрибута."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Олег\n#   Ошибка: 'Account' object has no attribute '__balance'\n#   Реальное имя: 0"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 9. @property и сеттер с валидацией\n\n*Лекция 7*\n\n`@property` превращает метод в чтение «как атрибут» (без скобок). `@<имя>.setter` перехватывает запись - можно проверить значение **до** того, как оно сохранится."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class CreditLimit:\n    def __init__(self, limit):\n        self._limit = limit\n\n    @property\n    def limit(self):\n        return self._limit\n\n    @limit.setter\n    def limit(self, value):\n        if value < 0:\n            raise ValueError(\"Лимит не может быть отрицательным\")\n        self._limit = value\n\n\nc = CreditLimit(500000)\nc.limit = 800000\nprint(c.limit)\ntry:\n    c.limit = -100\nexcept ValueError as e:\n    print(\"Ошибка:\", e)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 9\n\nСоздайте класс `Temperature`: значение храните в `_celsius`. Сделайте `@property celsius` для чтения и сеттер `celsius`, отклоняющий значения ниже `-273.15` (`raise ValueError(\"Ниже абсолютного нуля\")`). Проверьте на корректном (36.6) и некорректном (-300) значении."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   36.6\n#   Ошибка: Ниже абсолютного нуля"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 10. Вычисляемые свойства\n\n*Лекция 7*\n\nСвойство может не хранить значение, а **вычислять** его при каждом обращении. Снаружи это выглядит как обычный атрибут, но всегда актуально."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Loan:\n    def __init__(self, principal, rate):\n        self.principal = principal\n        self.rate = rate\n\n    @property\n    def total(self):\n        return self.principal * (1 + self.rate)\n\n\nloan = Loan(1_000_000, 0.20)\nprint(loan.total)            # 1200000.0\nloan.principal = 2_000_000\nprint(loan.total)            # 2400000.0 - пересчиталось"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 10\n\nСоздайте класс `Card` с атрибутом `pan` (строка из 16 цифр). Добавьте вычисляемое свойство `masked`, возвращающее номер вида `8600 **** **** 1234` (первые 4 и последние 4 цифры, середина - звёздочки)."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   8600 **** **** 1234"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 11. Атрибут класса vs атрибут экземпляра\n\n*Лекция 7*\n\nАтрибут **класса** - один на всех экземпляров (общая ставка, счётчик). Атрибут **экземпляра** (`self.x`) - у каждого объекта свой."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Loan:\n    base_rate = 0.20                  # атрибут класса - общий\n\n    def __init__(self, client):\n        self.client = client          # атрибут экземпляра - свой\n\n\na = Loan(\"Тимур\")\nb = Loan(\"Елена\")\nLoan.base_rate = 0.22                 # меняем у класса\nprint(a.base_rate, b.base_rate)       # 0.22 0.22 - у обоих"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 11\n\nВ классе `Loan` добавьте атрибут класса `count = 0` и увеличивайте `Loan.count` в `__init__`. Создайте 3 кредита и выведите `Loan.count`."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   3"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 12. @classmethod - фабричные конструкторы\n\n*Лекция 7*\n\n`@classmethod` получает первым аргументом `cls` (сам класс). Используется для **фабричных конструкторов** - альтернативных способов создать объект (из словаря, JSON, строки)."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Client:\n    def __init__(self, name, city):\n        self.name = name\n        self.city = city\n\n    @classmethod\n    def from_dict(cls, data):\n        return cls(data[\"name\"], data[\"city\"])\n\n\npayload = {\"name\": \"Олег\", \"city\": \"Ташкент\"}\nc = Client.from_dict(payload)\nprint(c.name, c.city)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 12\n\nВ классе `Card` (атрибуты `holder`, `balance`) добавьте `@classmethod from_application(cls, app)`, создающий карту из словаря заявки `{'holder': ..., 'balance': ...}`. Создайте карту через этот метод."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   Asel 150000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 13. @staticmethod - утилиты внутри класса\n\n*Лекция 7*\n\n`@staticmethod` не получает ни `self`, ни `cls`. Это обычная функция-утилита, логически сгруппированная внутри класса (валидаторы, проверки формата)."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class CardUtils:\n    @staticmethod\n    def is_valid_amount(amount):\n        return isinstance(amount, (int, float)) and amount > 0\n\n\nprint(CardUtils.is_valid_amount(5000))    # True\nprint(CardUtils.is_valid_amount(-10))     # False"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 13\n\nДобавьте в класс `Card` статический метод `validate_pan(pan)`, возвращающий `True`, если `pan` - строка ровно из 16 цифр, иначе `False`. Проверьте на `\"8600123412341234\"` и `\"8600-1234\"`."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   True\n#   False"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 14. Магические методы (dunder)\n\n*Лекция 7*\n\nДандер-методы (`__eq__`, `__len__`, `__add__`, ...) подключают объект к стандартным операторам и функциям Python: `==`, `len()`, `+`."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Пример** - запустите и разберите вывод:"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "class Account:\n    def __init__(self, owner, balance):\n        self.owner = owner\n        self.balance = balance\n\n    def __eq__(self, other):\n        return self.balance == other.balance\n\n\na = Account(\"Олег\", 10000)\nb = Account(\"Asel\", 10000)\nprint(a == b)        # True - сравнили по балансу"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Задание 14\n\nСоздайте класс `History`, хранящий список транзакций `items` (метод `add` добавляет транзакцию). Реализуйте `__len__`, чтобы `len(history)` возвращал количество транзакций. Добавьте 3 транзакции и выведите длину."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# ваш код здесь\n\n\n# Ожидаемый вывод:\n#   3"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Капстоун - мини-проект «Платёжная карта»\n\nСоберите небольшую систему процессинга банковских карт. Она использует **все**\nизученные концепции ООП сразу. Стройте по шагам - после каждой части есть\nячейка-проверка с ожидаемым выводом.\n\nЧто будет задействовано: классы и `__init__`, наследование и `super()`,\nполиморфизм, инкапсуляция (`__private`), `@property` + сеттер, `@staticmethod`,\n`@classmethod`, своё исключение и магические методы."
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Часть A - класс `BankCard`\n\nРеализуйте исключение `CardBlockedError` и класс `BankCard`:\n\n- `__init__(self, holder, pan, pin, balance=0)` - сохраните `holder`, `pan`,\n  `balance`; PIN установите **через сеттер** (`self.pin = pin`); заведите\n  `self._pin_attempts = 0` и `self.blocked = False`; увеличьте счётчик `count`.\n- `count` - атрибут класса: сколько карт создано.\n- `@property pin` - возвращает строку `\"****\"` (наружу PIN не отдаём).\n- `@pin.setter` - принимает ровно 4 цифры, иначе `raise ValueError`.\n- `@staticmethod validate_pan(pan)` - `True`, если строка из 16 цифр.\n- `@classmethod from_application(cls, app)` - карта из словаря заявки.\n- `check_pin(self, attempt)` - сверяет PIN; после 3 неверных попыток\n  ставит `blocked = True` и делает `raise CardBlockedError`.\n- `process_payment(self, amount)` - списывает сумму и печатает остаток.\n- `__str__` - `Карта <holder> · 8600 **** **** 1234 · <balance> сум`.\n- `__eq__` - карты равны, если одинаковый `pan`.\n\n**Ожидаемый вывод проверки A:**\n```\nКарта Олег · 8600 **** **** 1234 · 50000 сум\nPIN наружу: ****\nКарт создано: 1\nvalidate_pan ok : True\nvalidate_pan bad: False\nОшибка PIN: PIN - ровно 4 цифры\nКарта Asel · 8600 **** **** 7777 · 200000 сум\nКарт создано: 2\nОдинаковые карты? False\n```"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# --- Капстоун, часть A ---\n\nclass CardBlockedError(Exception):\n    \"\"\"TODO: своё исключение - наследник Exception (тело можно оставить pass).\"\"\"\n    pass\n\n\nclass BankCard:\n    count = 0   # TODO: счётчик созданных карт (увеличивайте в __init__)\n\n    def __init__(self, holder, pan, pin, balance=0):\n        # TODO: сохраните holder, pan, balance\n        # TODO: установите PIN через сеттер -> self.pin = pin\n        # TODO: self._pin_attempts = 0 ; self.blocked = False\n        # TODO: BankCard.count += 1\n        pass\n\n    # TODO: @property pin            -> возвращает \"****\"\n    # TODO: @pin.setter              -> ровно 4 цифры, иначе raise ValueError(\"PIN - ровно 4 цифры\")\n    # TODO: @staticmethod validate_pan(pan)        -> True, если строка из 16 цифр\n    # TODO: @classmethod from_application(cls, app) -> карта из словаря заявки\n    # TODO: check_pin(self, attempt) -> 3 неверные попытки -> raise CardBlockedError\n    # TODO: process_payment(self, amount) -> списать amount и напечатать остаток\n    # TODO: __str__                  -> \"Карта <holder> · 8600 **** **** 1234 · <balance> сум\"\n    # TODO: __eq__                   -> карты равны при одинаковом pan"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Проверка части A:**"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# Проверка части A - запустите после реализации\ncard = BankCard(\"Олег\", \"8600123412341234\", \"1234\", 50000)\nprint(card)\nprint(\"PIN наружу:\", card.pin)\nprint(\"Карт создано:\", BankCard.count)\nprint(\"validate_pan ok :\", BankCard.validate_pan(\"8600123412341234\"))\nprint(\"validate_pan bad:\", BankCard.validate_pan(\"860012\"))\ntry:\n    card.pin = \"12\"\nexcept ValueError as e:\n    print(\"Ошибка PIN:\", e)\n\napp = {\"holder\": \"Asel\", \"pan\": \"8600999988887777\", \"pin\": \"0000\", \"balance\": 200000}\ncard2 = BankCard.from_application(app)\nprint(card2)\nprint(\"Карт создано:\", BankCard.count)\nprint(\"Одинаковые карты?\", card == card2)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "### Часть B - наследники и блокировка\n\nОпираясь на `BankCard` из части A, создайте два подкласса (полиморфизм):\n\n- `DebitCard(BankCard)` - переопределите `process_payment`: если `amount`\n  больше `balance`, делайте `raise ValueError`; иначе спишите и напечатайте\n  строку с пометкой `[Дебет]`.\n- `CreditCard(BankCard)` - в `__init__` добавьте параметр `credit_limit`\n  (вызовите `super().__init__`). В `process_payment` разрешите уйти в минус,\n  но не ниже `-credit_limit`; печатайте строку с пометкой `[Кредит]`.\n\nЗатем проверочная ячейка прогонит обе карты в одном цикле, проверит блокировку\nпо 3 неверным PIN и отказ дебетовой карты при нехватке средств."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# --- Капстоун, часть B ---\n\n# TODO: class DebitCard(BankCard)\n#   переопределите process_payment:\n#     если amount > self.balance -> raise ValueError(\"Недостаточно средств\")\n#     иначе спишите и напечатайте \"[Дебет] <holder>: оплата <amount>, остаток <balance>\"\n\n# TODO: class CreditCard(BankCard)\n#   __init__(self, holder, pan, pin, balance=0, credit_limit=1_000_000)\n#     -> вызовите super().__init__(...) и сохраните credit_limit\n#   process_payment:\n#     если self.balance - amount < -self.credit_limit -> raise ValueError(\"Превышен лимит\")\n#     иначе спишите и напечатайте \"[Кредит] <holder>: оплата <amount>, остаток <balance>\""
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Проверка части B:**"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# Проверка части B - запустите после реализации\n\n# 1) полиморфизм: один вызов process_payment - разное поведение\ncards = [\n    DebitCard(\"Олег\", \"8600111122223333\", \"1111\", 80000),\n    CreditCard(\"Asel\", \"8600444455556666\", \"2222\", 10000, credit_limit=200000),\n]\nfor c in cards:\n    c.process_payment(50000)\n\n# 2) блокировка карты по 3 неверным PIN\ntest = DebitCard(\"Тест\", \"8600000011112222\", \"9999\", 5000)\nfor attempt in [\"0000\", \"1234\", \"5555\"]:\n    try:\n        ok = test.check_pin(attempt)\n        print(\"PIN верный\" if ok else \"PIN неверный\")\n    except CardBlockedError as e:\n        print(\"БЛОКИРОВКА:\", e)\n\n# 3) отказ дебетовой карты при нехватке средств\ntry:\n    DebitCard(\"Бек\", \"8600777788889999\", \"3333\", 1000).process_payment(999999)\nexcept ValueError as e:\n    print(\"Отказ:\", e)"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Готово.** Если все три проверки прошли - вы применили классы, наследование,\n`super()`, полиморфизм, инкапсуляцию, `@property` + сеттер, `@staticmethod`,\n`@classmethod`, своё исключение и магические методы в одном проекте.\n\n**Идеи для расширения (по желанию):** добавьте `__len__` для истории транзакций,\nметод `transfer(other, amount)` между картами, класс `VirtualCard` со сроком\nдействия."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}