Определение методов
Классы, которые содержат только атрибуты данных, не очень отличаются от словарей. Ниже вы найдете два способа смоделировать банковский счет: сначала с помощью определения класса, а затем с помощью словаря.
# 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
Можно скрыть атрибуты данных от клиентского кода, что помогает решить эту проблему. Мы вернемся к этой теме в следующей части.
В завершение этого раздела посмотрим на класс, который моделирует личный рекорд игрока. Определение класса содержит отдельные методы-валидаторы, которые проверяют, что переданные аргументы допустимы. Эти методы вызываются уже внутри конструктора. Это гарантирует, что созданный объект внутренне корректен.
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-01235 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})"Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.