Три принципа ООП: главное - безопасность! Приручаем инкапсуляцию.
Введение
На самом деле, для того, чтобы называться объектно-ориентированным, языку недостаточно иметь возможность описывать пользовательские типы данных. Кроме это, он должен соответствовать трем принципам объектно-ориентированного программирования.
Три принципа ООП утверждают, что каждый ООП язык должен поддерживать инкапсуляцию, наследование и полиморфизм.
Инкапсуляция
Инкапсуляция - это разделение прав доступа к элементам класса (а значит объекта этого класса) для его пользователей - других программистов (и вас).
В классических ООП языках существует три уровня доступа: private, protected и public.
- Элементы класса в секции private доступны только из методов этого класса.
- Элементы класса в секции protected доступны только из методов класса и его наследников (об этом позже).
- Элементы класса в секции public доступны извне.
В python, к счастью или к сожалению, инкапсуляция частично существует на уровне договоренности: если свойство или метод начинается с двух нижних подчеркиваний, то его можно менять / вызывать только изнутри класса. Но физически (в отличие от, например, С++), ошибки не возникнет, если вы так сделаете. Но делать так не надо! :)
class IncapsulationExample:
def __init__(self):
# это публичное свойство
self.a = None
# это приватное свойство
self.__b = None
# это публичный метод
def setB(self, value):
self.__b = value
self.__say()
# это приватный метод
def __say(self):
print(self.a, self.__b)
if __name__ == "__main__":
variable = IncapsulationExample()
# так можно!
variable.a = 5
# так нельзя!
variable.__b = 6
# а так совсем нельзя!
# variable.__say()
# а вот так можно!
variable.setB(7)
"Геттеры" и "сеттеры" для свойств
Давайте рассмотрим класс Account
- дебетовый банковский счет. Чтобы баланс был всегда положительным, сделаем это свойство приватным, а для его получения и изменения сделаем два метода - get_balance
и change_balance_by
.
class Account:
"""Описывает банковский счет с положительным балансом."""
def __init__(self):
self.__balance = 0
def get_balance(self):
"""Возвращает баланс счета."""
return self.__balance
def change_balance_by(self, amount: int) -> bool:
"""Изменяет баланс на указанную сумму (положительную или отрицательную)."""
if self.__balance + amount >= 0:
self.__balance += amount
return True
return False
if __name__ == "__main__":
account = Account()
# с помощью assert можно проверить, истинно ли выражение
# если нет - программа выдаст исключение
# иными словами, ниже - пример "юнит теста" для класса
assert account.change_balance_by(10) == True
assert account.get_balance() == 10
assert account.change_balance_by(-5) == True
assert account.get_balance() == 5
assert account.change_balance_by(-5) == True
assert account.get_balance() == 0
assert account.change_balance_by(-5) == False
assert account.change_balance_by(10) == True
assert account.get_balance() == 10
Все работает, но проходится вызывать довольно "долговязые" методы, хотя по сути мы просто хотим безопасно изменить свойство. Можно сделать и подругому - спрятать методы для изменения и получения баланса за публичным свойством с помощью@property
.
import pytest
class Account:
"""Описывает банковский счет с положительным балансом."""
def __init__(self):
self.__balance = 0
@property
def balance(self) -> int:
"""Возвращает баланс счета."""
return self.__balance
@balance.setter
def balance(self, amount: int):
"""Задает баланс."""
if amount >= 0:
self.__balance = amount
else:
raise Exception("Balance cant be negative")
if __name__ == "__main__":
account = Account()
assert account.balance == 0
account.balance += 10
assert account.balance == 10
account.balance -= 5
assert account.balance == 5
# Проверяем, что код порождает исключение
with pytest.raises(Exception):
account.balance -= 100
assert account.balance == 10
Статические свойства и методы
Статический метод - это метод класса, который можно вызывать без создания объекта. Также существуют статические свойства.
В полностью ООП языках, таких, как Java, нельзя создать обычную функцию вне какого-либо класса. Для этого приходится использовать статические методы. Python - это мультипарадигмальный язык (можно писать код в разных парадигмах), но при написании ООП кода использование статических методов вместо функций иногда хорошим тоном.
class Math:
"""Описывает математические операции."""
# это статическое свойство - его можно использовать, не создавая объект
pi: int = 3.14159
# это статический метод - его можно вызывать, не создавая объект
@staticmethod
def product(a: float, b: float) -> float:
"""Вычисляет произведение двух чисел."""
return float(a * b)
# это фишка питона, "классовый" метод метод,
# единственное отличие от статического - он получает свой класс первым параметром
@classmethod
def circle_area(cls, r: float) -> float:
"""Вычисляет площадь круга с заданным радиусом."""
# вместо cls.pi можно было бы написать Math.pi
return float(cls.pi * r ** 2)
if __name__ == "__main__":
print(Math.pi)
print(Math.product(3, 5))
print(Math.circle_area(4))