Cat

ООП на Python. ч. 2. Статические методы

Продолжаем цикл постов об ООП на Python. В прошлый раз мы говорили об ООП как таковом, об объекте self и о методе __init__. На сей раз мы поведем речь о методах классов. И начнём мы со статических и классовых методов.

Нюансы Python rusheslav 13 Март 2024 Просмотров: 313

Что такое статический метод?

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

Всем ли методам класса нужен объект 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__.

    Нет комментариев

    Реклама