ООП на Python. ч. 2. Статические методы
Продолжаем цикл постов об ООП на Python. В прошлый раз мы говорили об ООП как таковом, об объекте self
и о методе __init__
. На сей раз мы поведем речь о методах классов. И начнём мы со статических и классовых методов.
Реклама
Что такое статический метод?
Если выражаться просто, статический метод – это такой метод, который может выполнять свою работу, не имея доступа к информации, хранящейся в атрибутах экземпляра класса. То есть по сути статический метод не привязан к экземплярам класса. Данные, которые имеют отношение к конкретному объекту, никак не влияют на работу статического метода этого объекта.
Всем ли методам класса нужен объект self?
После нашего первого поста вы могли подумать, что объект self
необходимо передавать в качестве аргумента всем без исключения методам класса. Но если в методе никак не используется ни сам этот объект, ни какой-либо из его атрибутов, зачем тогда этот объект туда передавать? Метод прекрасно справится со своей задачей и без self
.
Представим, что у нас есть класс, описывающий марку сигарет:
class Cigarette:
def __init__(self, name="", nicotine_amount=0.0):
self.name = name
self.nicotine_amount = nicotine_amount
def describe_cigarette(self):
message = f'В сигаретах марки "{self.name}" содержится {self.nicotine_amount:} мг никотина.'
print(message)
my_cigarette = Cigarette("Ява", 0.8)
my_cigarette.describe_cigarette()
Мы описали класс, объект которого – марка сигарет. При инициализации объекта указывается название марки и содержание никотина в миллиграммах на сигарету. Единственный метод этого класса, не считая __init__
, – describe_cigarette
выводит в консоль сообщение о содержании никотина в сигаретах данной марки.
Теперь давайте представим, что мы хотим демонстрировать пользователю стандартное предупреждение о вреде курения. Для этого создадим отдельный метод в том же самом классе:
class Cigarette:
def __init__(self, name="", nicotine_amount=0.0):
self.name = name
self.nicotine_amount = nicotine_amount
def describe_cigarette(self):
message = f'В сигаретах марки "{self.name}" содержится {self.nicotine_amount:} мг никотина.'
print(message)
def show_disclaimer(self):
message = 'Курение вредит вашему здоровью.'
print(message)
my_cigarette = Cigarette("Ява", 0.8)
my_cigarette.describe_cigarette()
my_cigarette.show_disclaimer()
В результате работы нашего кода одно за другим выведутся два сообщения: о содержании никотина в сигаретах марки “Ява” и о вреде курения.
Как и всегда, мы добавили объект self
в качестве аргумента во вновь созданный нами метод show_disclaimer
. При этом важно заметить, что ни сам этот объект self, ни его атрибуты никак в методе не задействованы. И это диктуется самим смыслом выводимого предупреждения о вреде курения: оно одинаково для любой марки сигарет.
Избавимся от self
Если мы просто возьмём и выбросим из кода ненужный нам аргумент self
из метода show_disclaimer
, выполнение кода завершится следующим исключением:
TypeError: Cigarette.show_disclaimer() takes 0 positional arguments but 1 was given
Суть ошибки в том, что наш метод теперь не принимает никаких аргументов, но Python автоматически передает self
в каждый метод класса Cigarette
.
Статические методы
Методы, которые не используют данные конкретного объекта (то есть его атрибуты), встречаются довольно часто. И когда это происходит, нет никакой необходимости передавать в них лишнюю ссылку на объект self
. Чтобы этого не делать, необходимо перед методом вставить декоратор @staticmethod
:
class Cigarette:
def __init__(self, name="", nicotine_amount=0.0):
self.name = name
self.nicotine_amount = nicotine_amount
def describe_cigarette(self):
message = f'В сигаретах марки "{self.name}" содержится {self.nicotine_amount:} мг никотина.'
print(message)
@staticmethod
def show_disclaimer():
message = 'Курение вредит вашему здоровью.'
print(message)
my_cigarette = Cigarette("Ява", 0.8)
my_cigarette.describe_cigarette()
my_cigarette.show_disclaimer()
Вывод у этого кода будет точно таким же, как в предыдущем случае с self
.
Благодаря тому, что работа статических методов никак не связана с информацией о конкретном объекте, вызывать их можно прямо из класса, не создавая при этом объект. Для этого достаточно указать название класса и сам статический метод через точечную нотацию:
Cigarette.show_disclaimer()
В такой версии код выведет на печать только само предупреждение о вреде курения.
Для чего использовать статические методы?
А в чем вообще смысл этого действия? Почему нельзя просто вставлять ссылку на self
в качестве аргумента во все методы, даже если сам объект не будет в итоге использован? Здесь можно выделить два важных для понимания момента:
Классы, в которых статические методы выделены надлежащим образом, понятнее для восприятия. Когда вы выделяете тот или иной метод декоратором @staticmethod
, вы не только подсказываете Python’у, как ему работать с этим методом. Вы также посылаете сигнал другим разработчикам о том, что этот метод не меняет ничего в самом объекте, а доступ к нему можно получить, не создавая объектов класса вообще.
Классы, в которых используются статические методы, делают код более эффективным. Представьте себе огромный класс с большим количеством атрибутов и код, в котором создано множество экземпляров данного класса. И все эти объекты по много раз вызывают один и тот же метод. Добавление простого декоратора @staticmethod
позволит значительно сократить объем необязательной работы, которую будет совершать Python.
Футбольный клуб
Напишем класс для футбольного клуба:
class FootballClub:
def __init__(self, name, description=""):
self.name = name
self.description = description
def describe_club(self):
msg = f"{self.name}: {self.description}"
print(msg)
tree = FootballClub("Манчестер Юнайтед")
tree.description = "Манчестер, Англия"
tree.describe_tree()
Создавая экземпляр этого класса, мы задаем название футбольного клуба и описание. Вывод у нашего кода будет следующий:
Манчестер Юнайтед: Манчестер, Англия
Получился довольно типичный для ООП код: у нас есть класс, при помощи которого мы создаем объекты. Каждый из них обладает своими атрибутами, а также методом, который работает с информацией из этих атрибутов. Но что, если мы хотим иметь доступ к информацим о классе, которая связана более, чем с одним объектом?
Футбольная лига
Допустим, нам нужно создать футбольную лигу. Добавим в неё ещё пару клубов, используя тот же класс:
clubs = []
club = FootballClub("Манчестер Юнайтед")
club.description = "Манчестер, Англия"
clubs.append(club)
club = FootballClub("Реал")
club.description = "Мадрид, Испания"
clubs.append(club)
club = FootballClub("Ювентус")
club.description = "Турин, Италия"
clubs.append(club)
for club in clubs:
club.describe_club()
Мы создали три футбольных клуба и добавили их в список, после чего для каждого из них поочередно вызвали метод describe_club
. Вывод будет таким:
Манчестер Юнайтед: Манчестер, Англия
Реал: Мадрид, Испания
Ювентус: Турин, Италия
Допустим, нам нужно отслеживать количество клубов в нашей футбольной лиге. Это число будет иметь отношение ко всему классу в целом, а не к какому-то конкретному его экземпляру. Соответственно, подобная информация должна храниться в атрибуте класса, а не его экземпляра. Вот как мы поступим:
class FootballClub:
num_clubs = 0
@classmethod
def count_clubs(cls):
msg = f"Клубов в нашей лиге: {cls.num_clubs}."
print(msg)
def __init__(self, name, description=""):
self.name = name
self.description = description
FootballClub.num_clubs += 1
def describe_club(self):
msg = f"{self.name}: {self.description}"
print(msg)
clubs = []
club = FootballClub("Манчестер Юнайтед")
club.description = "Манчестер, Англия"
clubs.append(club)
club = FootballClub("Реал")
club.description = "Мадрид, Испания"
clubs.append(club)
club = FootballClub("Ювентус")
club.description = "Турин, Италия"
clubs.append(club)
for club in clubs:
club.describe_club()
FootballClub.count_clubs()
Сначала вне метода __init__
создадим атрибут класса num_clubs
. Обратим внимание, что никакой приставки в виде self перед этим атрибутом у нас нет, потому что он связан не с экземпляром класса, а с классом в целом.
Далее пишем классовый метод, предварив его декоратором @classmethod
. При помощи этого декоратора в метод передается весь класс. По умолчанию в классовых методах сам класс обозначается параметром cls
. Внутри самого метода доступ к атрибуту класса можно получить через точечную нотацию. Так, к числу клубов в нашей лиги мы получаем доступ следующим образом: cls.num_clubs
.
Чтобы количество клубов всегда было актуальным, при создании каждого нового экземпляра класса нам нужно увеличивать число, записанное в num_clubs
, на единицу. Мы сделали это в методе __init__
:
def __init__(self, name, description=""):
self.name = name
self.description = description
FootballClub.num_clubs += 1
Так как метод __init__
не принимает cls
в качестве аргумента, доступ к атрибуту num_clubs
мы получаем через название класса.
В конце мы вызываем метод класса, снова обратившись к нему по названию:
FootballClub.count_clubs()
Вывод будем следующим:
Манчестер Юнайтед: Манчестер, Англия
Реал: Мадрид, Испания
Ювентус: Турин, Италия
Клубов в нашей лиге: 3.
Обращаться к атрибутам класса и классовым методам можно несколькими способами, не только так, как мы показали выше.
Доступ к атрибуту класса по названию класса
Несмотря на то, что класс передается в классовый метод автоматически, получить доступ к атрибутам класса можно и через его название:
@classmethod
def count_clubs(cls):
msg = f"Клубов в нашей лиге: {FootballClub.num_clubs}."
print(msg)
Делать так не имеет никакого смысла, ведь можно использовать cls
, но ошибки при таком подходе не случится.
Вызов классового метода из экземпляра класса
Вызвать классовый метод можно и из экземпляра класса. Следующий код вполне рабочий:
club = FootballClub("Манчестер Юнайтед")
club.description = "Манчестер, Англия"
club.count_clubs()
Результат будет таким же, как и в случае с FootballClub.count_clubs()
.
Вызов классового метода через экземпляр класса может пригодиться, если вы работаете с объектом класса в модуле, который не имеет прямого доступа к самому классу. В таком случае можно не импортировать класс в модуль целиком, а обойтись вызовом метода напрямую из объекта, который у вас уже есть.
Подобная гибкость облегчает работу, поскольку разработчику необязательно думать о том, что происходит под капотом в том или ином классе. Можно просто пользоваться результатами труда тех, кто разработал и поддерживает саму библиотеку.
Доступ к атрибуту класса из обычного метода класса
Атрибутами класса можно пользоваться и в обычных методах класса. Например, если нам нужно расширить описание футбольного клуба, включив в него информацию о количестве команд в его лиге:
def describe_club(self):
msg = (f"{self.name}: {self.description}\n"
f"Клубов в лиге: {FootballClub.num_clubs}")
print(msg)
Это самый обычный метод, связанный с конкретным экземпляром класса, поэтому в него передается объект self
. Но и в нём можно получить доступ к атрибутам всего класса, используя название класса.
Можно ли получить доступ к атрибутам класса через self?
Ответ: да, можно. Если во всём классе у нас есть только один атрибут с данным именем, то ошибки не будет:
def describe_club(self):
msg = (f"{self.name}: {self.description}\n"
f"Клубов в лиге: {self.num_clubs}")
print(msg)
Этот код будет работать так же, как и код из примера выше.
На что обращать внимание?
Сложности начинаются при попытке записать новое значение в атрибут класса с помощью объекта self. Дело в том, что, используя self
, вы создаете новый атрибут именно экземпляра, если такой атрибут не был создан ранее. В такой редакции код не будет давать нам искомый результат:
class FootballClub:
num_clubs = 0
@classmethod
def count_clubs(cls):
msg = f"Клубов в нашей лиге: {cls.num_clubs}."
print(msg)
def __init__(self, name, description=""):
self.name = name
self.description = description
self.num_clubs += 1
def describe_club(self):
msg = (f"{self.name}: {self.description}\n"
f"Клубов в лиге: {self.num_clubs}")
print(msg)
Мы, как и раньше, увеличиваем число клубов на единицу, когда инициализируем новый объект класса. Но так как теперь мы делаем это через объект self
, а не через название класса, мы не меняем атрибут класса num_clubs
, а создаём новый атрибут экземпляра с таким же точно названием. И теперь у нас два атрибута с одинаковым названием num_clubs
: один относится к классу, а другой - к конкретному его экземпляру. И вывод у кода будет следующий:
Манчестер Юнайтед: Манчестер, Англия
Клубов в лиге: 1
Реал: Мадрид, Испания
Клубов в лиге: 1
Ювентус: Турин, Италия
Клубов в лиге: 1
Клубов в нашей лиге: 0.
В атрибуте num_clubs
каждого экземпляра класса теперь записано по единице, но атрибут num_clubs
класса так и не изменился: это по-прежнему ноль. Если логика в этом случае не очень понятно, можно просто запомнить важный вывод. Работая с атрибутом класса внутри самого класса, всегда используйте cls
, если речь о классовом методе, и название класса, если речь об обычном методе. И постарайтесь не создавать атрибутов класса и атрибутов экземпляра с одинаковым названием.
В следующий раз поговорим о методах __str__
и __repr__
.
Оглавление
- Что такое статический метод?
- Всем ли методам класса нужен объект self?
- Избавимся от self
- Статические методы
- Для чего использовать статические методы?
- Футбольный клуб
- Футбольная лига
- Доступ к атрибуту класса по названию класса
- Вызов классового метода из экземпляра класса
- Доступ к атрибуту класса из обычного метода класса
- Можно ли получить доступ к атрибутам класса через self?
- На что обращать внимание?
Все статьи