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

Тем не менее, когда функции (методы) класса используют объект, этот объект передается в метод как первый аргумент:

>>> class A:
...     def meth(self):
...         print('meth')
...
>>> a = A()
>>> a.meth()
meth
>>> A.meth(a)
meth

Когда вызывается a.meth(), это фактически преобразуется в A.meth(a). Мы ищем метод meth в пространстве имен класса A, где обнаруживаем, что он – функция, требующая один аргумент. Благодаря этому можно вызвать его так:

>>> b = 10
>>> A.meth(b)
meth

В данном случае метод может быть вызван без передачи объекта класса A. Однако недопустимо осуществить такое выполнение:

>>> b = 10
>>> b.meth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute 'meth'

При вызове метода через объект, метод должен быть определен в самом классе объекта или в его родительских классах. В данном примере класс int не располагает методом meth, поэтому интерпретатор пытается найти его сначала у объекта b, а затем в его классе. Поскольку b не является экземпляром класса A, интерпретатор ничего не найдет.

Что, если нам нужен метод, который не принимает объект класса как аргумент? В Python можно описать метод без параметров и вызывать его только через сам класс:

>>> class A:
...     def meth():
...         print('meth')
...
>>> A.meth()
meth
>>> a = A()
>>> a.meth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: meth() takes 0 positional arguments but 1 was given

Это создает двусмысленность: meth() может быть вызван как через класс, так и через его объекты, но второй случай приведет к ошибке. Кроме того, иногда требуется метод с параметрами без использования объектного аргумента.

Во многих языках программирования, таких как Java, для таких ситуаций предусмотрены статические методы. Они помечаются ключевым словом static и вызываются без передачи самого объекта как аргумента.

В Python статические методы не столь необходимы, так как код можно писать вне классов. Однако, если требуется просто функция, можно объявить её за пределами класса. В отличие от Java, где весь код, за исключением импортов, находится внутри классов, в Python статические методы можно создать с помощью декоратора @staticmethod:

>>> class A:
...     @staticmethod
...     def meth():
...         print('meth')
...
>>> a = A()
>>> a.meth()
meth
>>> A.meth()
meth

Пример с использованием параметра:

>>> class A:
...     @staticmethod
...     def meth(value):
...         print(value)
...
>>> a = A()
>>> a.meth(1)
1
>>> A.meth('hello')
hello

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

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

from math import pi
 
class Cylinder:
    @staticmethod
    def make_area(d, h):
        circle = pi * d ** 2 / 4
        side = pi * d * h
        return round(circle*2 + side, 2)
 
    def __init__(self, diameter, high):
        self.dia = diameter
        self.h = high
        self.area = self.make_area(diameter, high)
 
 
a = Cylinder(1, 2)
print(a.area)
 
print(a.make_area(2, 2))
    

В этом примере make_area() может быть вызвана из-за пределов класса через экземпляр, показывая, что метод работает независимо от объекта, но в рамках пространства имен класса.

Практическая работа

Финальный пример содержит недочеты. Всё из-за того, что можно изменять значения полей dia и h напрямую (например, a.dia = 10), не вызывая пересчет площади. Также возможно назначить новое значение для площади через прямое присваивание или вызов make_area() с присваиванием снова (например, a.area = a.make_area(2, 3)), что не изменит диаметр и высоту.

Обеспечьте защиту кода от логических ошибок следующим образом:

  • Хотя поля dia и h объекта все еще можно изменять извне, изменение теперь приведет к автоматическому пересчету площади, то есть обновлению значения area.

  • Площадь нельзя изменять напрямую за пределами класса, но можно считывать её значение.

Подсказка: вспомните про метод __setattr__(), обсуждавшийся в теме инкапсуляции.

Вопросы для самопроверки:

  1. Что произойдет, если попытаться вызвать метод объекта для встроенного типа Python, который этого метода не имеет?
  2. Как можно вызывать методы, которые не требуют объекта класса в качестве аргумента?
  3. Какой декоратор в Python используется для создания статических методов в классе?
  4. Опишите пример, когда использование статического метода в классе может быть полезным.