Part 8

Определение методов

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

# Example 1: bank account with class definition
class BankAccount:

    def __init__(self, account_number: str, owner: str, balance: float, annual_interest: float):
        self.account_number = account_number
        self.owner = owner
        self.balance = balance
        self.annual_interest = annual_interest

peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)
# Example 2: bank account with dictionary
peters_account = {"account_number": "12345-678", "owner": "Peter Python", "balance": 1500.0, "annual_interest": 0.015}

Со словарем реализация намного короче и прямолинейнее. Однако с классом структура «связана» плотнее, поэтому мы можем ожидать, что все объекты BankAccount будут структурно одинаковыми. У класса также есть имя. При создании нового банковского счета мы ссылаемся на класс BankAccount, и тип объекта — BankAccount, а не dict.

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

Методы в классах

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

Продолжим с классом BankAccount, представленным выше. Ниже у нас есть новый метод, который начисляет проценты на счет:

class BankAccount:

    def __init__(self, account_number: str, owner: str, balance: float, annual_interest: float):
        self.account_number = account_number
        self.owner = owner
        self.balance = balance
        self.annual_interest = annual_interest

    # This method adds the annual interest to the balance of the account
    def add_interest(self):
        self.balance += self.balance * self.annual_interest


peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)
peters_account.add_interest()
print(peters_account.balance)
Пример вывода

1522.5

Метод add_interest умножает баланс счета на годовую процентную ставку, а затем добавляет результат к текущему балансу. Метод действует только на тот объект, для которого он вызван.

Посмотрим, как это работает, когда мы создали несколько экземпляров класса:

# The class BankAccount is defined in the previous example

peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)
paulas_account = BankAccount("99999-999", "Paula Pythonen", 1500.0, 0.05)
pippas_account = BankAccount("1111-222", "Pippa Programmer", 1500.0, 0.001)

# Add interest on Peter's and Paula's accounts, but not on Pippa's
peters_account.add_interest()
paulas_account.add_interest()

# Print all account balances
print(peters_account.balance)
print(paulas_account.balance)
print(pippas_account.balance)
Пример вывода

1522.5 1575.0 1500.0

Как видно выше, годовые проценты добавляются только к тем счетам, для которых был вызван метод. Поскольку годовые процентные ставки для счетов Peter и Paula различаются, результаты для этих двух счетов тоже различаются. Баланс на счете Pippa не меняется, потому что метод add_interest не вызывается для объекта pippas_account.

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

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

Пример того, как это могло бы работать:

class BankAccount:

    def __init__(self, account_number: str, owner: str, balance: float, annual_interest: float):
        self.account_number = account_number
        self.owner = owner
        self.balance = balance
        self.annual_interest = annual_interest

    # This method adds the annual interest to the balance of the account
    def add_interest(self):
        self.balance += self.balance * self.annual_interest

    # This method "withdraws" money from the account
    # If the withdrawal is successful the method returns True, and False otherwise
    def withdraw(self, amount: float):
        if amount <= self.balance:
            self.balance -= amount
            return True

        return False

peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)

if peters_account.withdraw(1000):
    print("The withdrawal was successful, the balance is now", peters_account.balance)
else:
    print("The withdrawal was unsuccessful, the balance is insufficient")

# Yritetään uudestaan
if peters_account.withdraw(1000):
    print("The withdrawal was successful, the balance is now", peters_account.balance)
else:
    print("The withdrawal was unsuccessful, the balance is insufficient")
Пример вывода

The withdrawal was successful, the balance is now 500.0 The withdrawal was unsuccessful, the balance is insufficient

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

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

peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)

# Attempt to withdraw 2000
if peters_account.withdraw(2000):
    print("The withdrawal was successful, the balance is now", peters_account.balance)
else:
    print("The withdrawal was unsuccessful, the balance is insufficient")

    # "Force" the withdrawal of 2000
    peters_account.balance -= 2000

print("The balance is now:", peters_account.balance)
Пример вывода

The withdrawal was unsuccessful, the balance is insufficient The balance is now: -500.0

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

Loading

В завершение этого раздела посмотрим на класс, который моделирует личный рекорд игрока. Определение класса содержит отдельные методы-валидаторы, которые проверяют, что переданные аргументы допустимы. Эти методы вызываются уже внутри конструктора. Это гарантирует, что созданный объект внутренне корректен.

from datetime import date

class PersonalBest:

    def __init__(self, player: str, day: int, month: int, year: int, points: int):
        # Default values
        self.player = ""
        self.date_of_pb = date(1900, 1, 1)
        self.points = 0

        if self.name_ok(player):
            self.player = player

        if self.date_ok(day, month, year):
            self.date_of_pb = date(year, month, day)

        if self.points_ok(points):
            self.points = points

    # Helper methods to check the arguments are valid
    def name_ok(self, name: str):
        return len(name) >= 2 # Name should be at least two characters long

    def date_ok(self, day, month, year):
        try:
            date(year, month, day)
            return True
        except:
            # an exception is raised if the arguments are not valid
            return False

    def points_ok(self, points):
        return points >= 0

if __name__ == "__main__":
    result1 = PersonalBest("Peter", 1, 11, 2020, 235)
    print(result1.points)
    print(result1.player)
    print(result1.date_of_pb)

    # The date was not valid
    result2 = PersonalBest("Paula", 4, 13, 2019, 4555)
    print(result2.points)
    print(result2.player)
    print(result2.date_of_pb) # Tulostaa oletusarvon 1900-01-01
Пример вывода

235 Peter 2020-11-01 4555 Paula 1900-01-01

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

Имя параметра self используется только при обращении к свойствам объекта как экземпляра класса. К ним относятся и атрибуты данных, и методы, привязанные к объекту. Чтобы терминология стала еще запутаннее, атрибуты данных и методы вместе иногда называют просто атрибутами объекта; поэтому в этом материале мы часто уточняли атрибуты данных, когда имели в виду переменные, определенные внутри класса. Здесь терминология некоторых программистов Python немного отличается от терминологии, более широко используемой в объектно-ориентированном программировании, где атрибуты обычно означают только атрибуты данных объекта.

Также можно создавать локальные переменные внутри определений методов без обращения к self. Так стоит делать, если нет необходимости обращаться к переменным вне метода. У локальных переменных внутри методов нет специальных ключевых слов; они используются так же, как любые обычные переменные, с которыми вы уже сталкивались.

Например, это будет работать:

class BonusCard:
    def __init__(self, name: str, balance: float):
        self.name = name
        self.balance = balance

    def add_bonus(self):
        # The variable bonus below is a local variable.
        # It is not a data attribute of the object.
        # It can not be accessed directly through the object.
        bonus = self.balance * 0.25
        self.balance += bonus

    def add_superbonus(self):
        # The superbonus variable is also a local variable.
        # Usually helper variables are local variables because
        # there is no need to access them from the other
        # methods in the class or directly through an object.
        superbonus = self.balance * 0.5
        self.balance += superbonus

    def __str__(self):
        return f"BonusCard(name={self.name}, balance={self.balance})"
Loading
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

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