Ранее мы упоминали, что с определенной оговоркой классы можно трактовать как модули, которые содержат переменные и функции. В контексте классов эти переменные известны как поля или свойства, а функции называются методами, и вместе они составляют атрибуты.
Тем не менее, когда функции (методы) класса используют объект, этот объект передается в метод как первый аргумент:
>>> 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__(), обсуждавшийся в теме инкапсуляции.