Part 9

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

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

  1. использование класса и/или объектов должно быть как можно проще с точки зрения клиента
  2. целостность любого объекта должна сохраняться всегда

Целостность объекта означает, что состояние объекта всегда остается допустимым. На практике это означает, что значения атрибутов объекта всегда допустимы. Например, объект, представляющий дату, никогда не должен иметь значение 13 для месяца; объект, моделирующий студента, никогда не должен иметь отрицательное количество полученных учебных кредитов, и так далее.

Посмотрим на класс с именем Student:

class Student:
    def __init__(self, name: str, student_number: str):
        self.name = name
        self.student_number = student_number
        self.study_credits = 0

    def add_credits(self, study_credits):
        if study_credits > 0:
            self.study_credits += study_credits

Объект Student предлагает своим клиентам метод add_credits, который позволяет клиенту добавить указанное количество кредитов к общему числу кредитов студента. Метод проверяет, что значение, переданное как аргумент, больше нуля. Следующий код добавляет учебные кредиты три раза:

sally = Student("Sally Student", "12345")
sally.add_credits(5)
sally.add_credits(5)
sally.add_credits(10)
print("Study credits:", sally.study_credits)
Пример вывода

Study credits: 20

Несмотря на определение метода, к атрибуту study_credits все равно можно обратиться напрямую. Это может привести к ошибочному состоянию, где целостность объекта нарушена:

sally = Student("Sally Student", "12345")
sally.study_credits = -100
print("Study credits:", sally.study_credits)
Пример вывода

Study credits: -100

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

Обычная возможность в объектно-ориентированных языках программирования — классы обычно могут скрывать свои атрибуты от потенциальных клиентов. Скрытые атрибуты обычно называют приватными. В Python такая приватность достигается добавлением двух подчеркиваний __ в начало имени атрибута:

class CreditCard:
    # the attribute number is private, while the attribute name is accessible
    def __init__(self, number: str, name: str):
        self.__number = number
        self.name = name

Приватный атрибут не виден клиенту напрямую. Попытка обратиться к нему вызывает ошибку. В примере выше к атрибуту name можно легко обращаться и изменять его:

card = CreditCard("123456","Randy Riches")
print(card.name)
card.name = "Charlie Churchmouse"
print(card.name)
Пример вывода

Randy Riches Charlie Churchmouse

Однако попытка вывести номер карты вызывает ошибку:

card = CreditCard("123456","Randy Riches")
print(card.__number)
Пример вывода

AttributeError: 'CreditCard' object has no attribute '__number'

Сокрытие атрибутов от клиентов называется инкапсуляцией. Как следует из названия, атрибут как будто "заключен в капсулу". Затем клиенту предлагается подходящий интерфейс для доступа к данным, хранящимся в объекте, и их обработки.

Добавим еще один инкапсулированный атрибут: баланс кредитной карты. На этот раз мы также добавим публично видимые методы, которые позволяют клиенту обращаться к балансу и изменять его:

class CreditCard:
    def __init__(self, number: str, name: str, balance: float):
        self.__number = number
        self.name = name
        self.__balance = balance

    def deposit_money(self, amount: float):
        if amount > 0:
            self.__balance += amount

    def withdraw_money(self, amount: float):
        if amount > 0 and amount <= self.__balance:
            self.__balance -= amount

    def retrieve_balance(self):
        return self.__balance
card = CreditCard("123456", "Randy Riches", 5000)
print(card.retrieve_balance())
card.deposit_money(100)
print(card.retrieve_balance())
card.withdraw_money(500)
print(card.retrieve_balance())
# The following will not work because the balance is not sufficient
card.withdraw_money(10000)
print(card.retrieve_balance())
Пример вывода

5000 5100 4600 4600

Баланс нельзя изменить напрямую, потому что атрибут приватный, но мы включили методы deposit_money и withdraw_money для изменения значения. Метод retrieve_balance возвращает значение, хранящееся в балансе. Методы содержат некоторые простые проверки для сохранения целостности объекта: например, по карте нельзя уйти в минус.

Loading

Краткое замечание о приватных атрибутах, Python и объектно-ориентированном программировании

Существуют способы обойти запись с подчеркиваниями __ для скрытия атрибутов; вы можете встретить их, если будете искать материалы в интернете. Ни один атрибут Python не является по-настоящему приватным, и это намеренное решение создателей Python. С другой стороны, от программиста на Python обычно ожидается уважение к правилам видимости, заданным в классах, а для их обхода нужны специальные усилия. В других объектно-ориентированных языках программирования, таких как Java, приватные переменные часто действительно скрыты, и лучше думать о приватных переменных Python так же.

Геттеры и сеттеры

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

Итак, выше мы создали несколько публичных методов для доступа к приватным атрибутам, но есть более прямой, "питоничный" способ обращаться к атрибутам. Посмотрим на простой класс с именем Wallet с одним приватным атрибутом money:

class Wallet:
    def __init__(self):
        self.__money = 0

Мы можем добавить методы-геттеры и методы-сеттеры для доступа к приватному атрибуту с помощью декоратора @property:

class Wallet:
    def __init__(self):
        self.__money = 0

    # A getter method
    @property
    def money(self):
        return self.__money

    # A setter method
    @money.setter
    def money(self, money):
        if money >= 0:
            self.__money = money

Сначала мы определяем метод-геттер, который возвращает текущую сумму денег в кошельке. Затем мы определяем метод-сеттер, который устанавливает новое значение для атрибута money, проверяя, что новое значение не отрицательное.

Новые методы можно использовать так:

wallet = Wallet()
print(wallet.money)

wallet.money = 50
print(wallet.money)

wallet.money = -30
print(wallet.money)
Пример вывода

0 50 50

С точки зрения клиента использование этих новых методов ничем не отличается от прямого доступа к атрибуту. Скобки не нужны; вместо этого совершенно нормально написать wallet.money = 50, как если бы мы просто присваивали значение переменной. И действительно, цель состояла в том, чтобы скрыть (то есть инкапсулировать) внутреннюю реализацию атрибута, одновременно предложив простой способ доступа к данным, хранящимся в объекте, и их изменения.

В предыдущем примере есть небольшая проблема: клиент не получает уведомления о неудачной попытке установить отрицательное значение для атрибута money. Когда переданное значение явно неверно, обычно хорошая идея — вызвать исключение и таким образом сообщить клиенту. В этом случае исключение, вероятно, должно быть типа ValueError, чтобы указать, что переданное значение недопустимо.

Вот улучшенная версия класса вместе с кодом для ее проверки:

class Wallet:
    def __init__(self):
        self.__money = 0

    # A getter method
    @property
    def money(self):
        return self.__money

    # A setter method
    @money.setter
    def money(self, money):
        if money >= 0:
            self.__money = money
        else:
            raise ValueError("The amount must not be below zero")
wallet.money = -30
print(wallet.money)
Пример вывода

ValueError: The amount must not be below zero

Обратите внимание: метод-геттер, то есть декоратор @property, должен быть введен до метода-сеттера, иначе при выполнении класса возникнет ошибка. Причина в том, что декоратор @property определяет имя "атрибута", предлагаемого клиенту. Метод-сеттер, добавленный с помощью .setter, просто добавляет к нему новую функциональность.

Loading

В следующем примере есть класс с двумя приватными атрибутами, а также геттеры и сеттеры для обоих. Попробуйте запустить программу с разными значениями, переданными как аргументы:

class Player:
    def __init__(self, name: str, player_number: int):
        self.__name = name
        self.__player_number = player_number

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, name: str):
        if name != "":
            self.__name = name
        else:
            raise ValueError("The name may not be an empty string")

    @property
    def player_number(self):
        return self.__player_number

    @player_number.setter
    def player_number(self, player_number: int):
        if player_number > 0:
            self.__player_number = player_number
        else:
            raise ValueError("The player number must be a positive integer")
player = Player("Betty Ballmer", 10)
print(player.name)
print(player.player_number)

player.name = "Buster Ballmer"
player.player_number = 11
print(player.name)
print(player.player_number)
Пример вывода

Betty Ballmer 10 Buster Ballmer 11

В завершение этого раздела посмотрим на класс, который моделирует простой дневник. Все атрибуты приватные, но работа с ними идет через разные интерфейсы: для владельца дневника есть геттер и сеттер, а записи дневника обрабатываются "традиционными" методами. В этом случае имеет смысл запретить клиенту любой доступ к внутренней структуре данных дневника. Клиенту напрямую видны только публичные методы.

Инкапсуляция также гарантирует, что внутреннюю реализацию класса можно изменить когда угодно, если публичный интерфейс остается неизменным. Клиенту не нужно знать или беспокоиться, основана ли внутренняя структура данных на списках, словарях или чем-то совершенно другом.

class Diary:
    def __init__(self, owner: str):
        self.__owner = owner
        self.__entries = []

    @property
    def owner(self):
        return self.__owner

    @owner.setter
    def owner(self, owner):
        if owner != "":
            self.__owner = owner
        else:
            raise ValueError("The owner may not be an empty string")

    def add_entry(self, entry: str):
        self.__entries.append(entry)

    def print_entries(self):
        print("A total of", len(self.__entries), "entries")
        for entry in self.__entries:
            print("- " + entry)
diary = Diary("Peter")
diary.add_entry("Today I ate porridge")
diary.add_entry("Today I learned object oriented programming")
diary.add_entry("Today I went to bed early")
diary.print_entries()
Пример вывода

A total of 3 entries

  • Today I ate porridge
  • Today I learned object oriented programming
  • Today I went to bed early
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.