Три принципа ООП: главное - безопасность! Приручаем инкапсуляцию.

Введение

На самом деле, для того, чтобы называться объектно-ориентированным, языку недостаточно иметь возможность описывать пользовательские типы данных. Кроме это, он должен соответствовать трем принципам объектно-ориентированного программирования.

Три принципа ООП утверждают, что каждый ООП язык должен поддерживать инкапсуляцию, наследование и полиморфизм.

Три принципа ООП

Инкапсуляция

Инкапсуляция - это разделение прав доступа к элементам класса (а значит объекта этого класса) для его пользователей - других программистов (и вас).

В классических ООП языках существует три уровня доступа: 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))