Статические методы в Python

Ранее было сказано, что классы можно рассматривать как модули, содержащие переменные со значениями (в данном случае речь идет о тех, что находятся вне методов) и функции. Только здесь переменные называются полями или свойствами класса, а функции – методами класса. Вместе поля и методы мы называем атрибутами.

Однако в случае классов, когда метод применяется к объекту, этот экземпляр передается в метод в качестве первого аргумента:

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

Вызов a.meth() на самом деле преобразуется к A.meth(a), то есть мы идем к "модулю A" и в его пространстве имен ищем атрибут meth. Там оказывается, что meth это функция, принимающая один обязательный аргумент. Тогда ничего не мешает сделать так:

>>> 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 классу A не принадлежит. Поэтому интерпретатор никогда не найдет метод meth.

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

>>> 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, di, hi):
        self.dia = diameter
        self.h = high
        self.area = self.make_area(di, hi)
 
 
a = Cylinder(1, 2)
print(a.area)
 
print(a.make_area(2, 2))

В примере вызов make_area() за пределами класса возможен в том числе через экземпляр. При этом свойство area самого объекта a не меняется. Мы просто вызываем функцию, находящуюся в пространстве имен класса.

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

Приведенный в конце урока пример плохой. Мы можем менять значения полей dia и h объекта за пределами класса простым присваиванием (например, a.dia = 10). При этом площадь никак не будет пересчитываться. Также мы можем назначить новое значение для площади, как простым присваиванием, так и вызовом функции make_area() с последующим присваиванием. Например, a.area = a.make_area(2, 3). При этом не меняются высота и диаметр.

Защитите код от возможных логических ошибок следующим образом:

Подсказка: вспомните про метод __setattr__.

Курс с примерами решений практических работ:
pdf-версия


Объектно-ориентированное программирование на Python




Все разделы сайта