Атрибут, который можно свободно менять снаружи, легко испортить: подарочной карте записать отрицательный баланс, заказу — несуществующий статус. Объект не может защитить себя, если любой код пишет в его атрибуты что угодно. Решение называется инкапсуляция (encapsulation): объект прячет своё внутреннее состояние и даёт менять его только через свои методы — а методы проверяют, что изменение допустимо.
Публичный интерфейс и внутренние атрибуты
Методы и атрибуты, которыми пользуются снаружи, образуют публичный интерфейс класса (public API).
Всё остальное — внутреннее устройство. В Python нет закрытых атрибутов, как private в Java или C++, — есть
соглашение: имя, которое начинается с подчёркивания, — внутренний атрибут (internal attribute). Его можно прочитать и
изменить, но код снаружи так не делает: это детали, которые автор класса вправе поменять.
class GiftCard:
def __init__(self, amount):
self._balance = amount # внутренний атрибут
def balance(self):
return self._balance
def pay(self, amount):
if amount > self._balance:
return False # денег не хватает — баланс не меняется
self._balance -= amount
return True
card = GiftCard(500)
print(card.pay(200), card.balance())
print(card.pay(400), card.balance())True 300 False 300
Снаружи с картой работают только через balance() и pay(). Метод pay проверяет, хватает ли денег, —
и баланс не уйдёт в минус, пока никто не лезет в _balance напрямую.
card._balance = -500 # так можно, но так не делают
print(card.balance())-500
Допишите класс GiftCard: у него уже есть balance() и pay(amount). Добавьте метод top_up(amount):
положительную сумму он прибавляет к балансу и возвращает True, а ноль и отрицательную сумму не принимает —
баланс не меняется, метод возвращает False.
amount > 0 — self._balance += amount и return True; иначе — return False.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class GiftCard:
def __init__(self, amount):
self._balance = amount
def balance(self):
return self._balance
def pay(self, amount):
if amount > self._balance:
return False
self._balance -= amount
return True
def top_up(self, amount):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class GiftCard:
def __init__(self, amount):
self._balance = amount
def balance(self):
return self._balance
def pay(self, amount):
if amount > self._balance:
return False
self._balance -= amount
return True
def top_up(self, amount):
if amount > 0:
self._balance += amount
return True
return FalseРабота через интерфейс
Код снаружи не знает, как карта хранит деньги, — и не должен. Ему хватает методов:
С карты card на 1000 ₽ оплачивают по очереди заказы из списка orders. Сохраните в paid список
результатов pay для каждого заказа (True — оплачен, False — денег не хватило), а в left — баланс после
всех оплат. Используйте только методы карты.
paid = [card.pay(x) for x in orders]; баланс — card.balance().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
card = GiftCard(1000)
orders = [450, 300, 400, 150]
paid = ...
left = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
card = GiftCard(1000)
orders = [450, 300, 400, 150]
paid = [card.pay(x) for x in orders]
left = card.balance()Два подчёркивания: искажение имён
Имя с двумя подчёркиваниями в начале и без двух подчёркиваний в конце — __code, но не __init__ —
Python внутри класса тихо переименовывает:
к нему приписывается имя класса — _Safe__code. Это называется искажение имён (name mangling):
class Safe:
def __init__(self, code):
self.__code = code # Python переименует в _Safe__code
def opens_with(self, code):
return code == self.__code # внутри класса имя то же
safe = Safe("0000")
print(safe.__dict__)
print(safe.opens_with("0000"))
print(safe._Safe__code){'_Safe__code': '0000'}
True
0000safe.__codeAttributeError: 'Safe' object has no attribute '__code'
Снаружи safe.__code не найдётся, но safe._Safe__code прочитать можно — это тоже не защита. Искажение имён нужно
для другого: чтобы подкласс случайно не перезаписал внутренний атрибут базового класса своим атрибутом с тем же
именем (о подклассах — в модуле 5). Поэтому для внутренних атрибутов обычно хватает одного подчёркивания, а два
пишут, когда важно не столкнуться с именами в подклассах.
Объявите класс Terminal: Terminal("1234") хранит ПИН-код в атрибуте __pin — с двумя подчёркиваниями.
Метод check(pin) возвращает True, если переданный код совпадает с сохранённым, иначе False.
__init__: self.__pin = pin; внутри класса имя пишется так же — self.__pin.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Terminal:
def __init__(self, pin):
...
def check(self, pin):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Terminal:
def __init__(self, pin):
self.__pin = pin
def check(self, pin):
return pin == self.__pinЧто будет, если снаружи класса выполнить card._balance = 1_000_000?
pay и top_up окажутся бесполезны. Поэтому внутренние атрибуты снаружи не трогают.Итак: публичный интерфейс — имена без подчёркивания, внутреннее — с _. Методы с проверками стерегут
состояние. Но у методов есть неудобство: баланс читается вызовом balance(), а хотелось бы — как атрибут,
card.balance. Как сделать атрибут, который нельзя испортить, — в следующем уроке.