Простое объяснение методов класса, экземпляра класса и статических методов в Python

Перевод статьи «Python’s Instance, Class, and Static Methods Demystified».

В этой статье я постараюсь объяснить, что стоит за методами класса, статическими методами и обычными методами экземпляра класса.

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

Обзор статических методов, методов класса и экземпляра класса

Давайте начнем с написания класса (на Python 3), который будет содержать простые примеры методов всех трех видов:

class MyClass:
    def method(self):
        return 'instance method called', self
    @classmethod
    def classmethod(cls):
        return 'class method called', cls
    @staticmethod
    def staticmethod():
        return 'static method called'

Примечание для пользователей Python 2. Декораторы @staticmethod и @classmethod доступны в Python начиная с версии 2.4, а значит, этот пример тоже будет работать. Вместо использования простого объявления класса class MyClass: вы можете объявить класс нового стиля, который будет наследоваться от object, с синтаксисом class MyClass(object):. Все остальное не требует каких-то уточнений.

Методы экземпляра класса

Первый метод в MyClass, под названием method, это обычный метод экземпляра класса. Это самый базовый вид методов, которым вы будете пользоваться чаще всего. Как видите, этот метод принимает один параметр (self), который при вызове метода указывает на экземпляр MyClass (хотя методы могут принимать и больше одного параметра).

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

Методы экземпляра также могут иметь доступ к самому классу — при помощи атрибута self.__class__. Это означает, что они могут менять состояние не только объекта, но и класса.

Методы класса

Давайте сравним то, что мы узнали о методах экземпляра класса, со вторым методом — MyClass.classmethod. Я обозначил этот метод при помощи декоратора @classmethod, чтобы было видно, что это метод класса.

Методы класса вместо параметра self принимают параметр cls. Этот параметр при вызове метода указывает не на экземпляр объекта, а на класс.

Поскольку метод класса имеет доступ только к аргументу cls, он не может изменять состояние экземпляра объекта. Для этого нужен доступ к self. Но, тем не менее, методы класса могут изменять состояние класса в целом, что затронет и все экземпляры этого класса.

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

Третий метод, MyClass.staticmethod, обозначен при помощи декоратора @staticmethod, чтобы показать, что этот метод статический.

Методы такого типа не принимают в качестве параметра ни self, ни cls (хотя, безусловно, они свободно могут принимать другие параметры в любых количествах).

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

Давайте посмотрим, как все это работает!

Пока что все изложенное было чистой теорией. Но я считаю, что вы должны научиться интуитивно улавливать разницу между методами на практике. Сейчас мы разберем несколько более конкретных примеров.

Давайте посмотрим, как все эти методы ведут себя, когда мы их вызываем. Начнем с создания экземпляра класса, а затем вызовем все три метода.

В MyClass реализация каждого метода возвращает сведения, благодаря которым мы можем понимать, к каким частям класса или объекта может иметь доступ метод, а также отслеживать происходящее.

Вот что происходит при вызове метода экземпляра класса:

>>> obj = MyClass()
>>> obj.method()
('instance method called', <MyClass instance at 0x101a2f4c8>)

Мы видим, что method (т. е., метод экземпляра класса) имеет доступ к экземпляру объекта (это видно по выводу <MyClass instance>) при помощи аргумента self.

При вызове этого метода Python замещает аргумент self экземпляром объекта (obj). Мы можем проигнорировать синтаксический сахар dot-call синтаксиса (obj.method()) и получить тот же результат, передав экземпляр объекта вручную:

>>> MyClass.method(obj)
('instance method called', <MyClass instance at 0x101a2f4c8>)

Можете догадаться, что произойдет, если вы попытаетесь вызвать этот метод без первоначального создания экземпляра класса?

Кстати, методы экземпляра класса при помощи атрибута self.__class__ также могут иметь доступ и к самому классу. Это делает данные методы особенно полезными в условиях ограничений доступа: они могут изменять состояние экземпляра объекта и самого класса.

Теперь давайте испытаем метод класса:

>>> obj.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)

Вызов classmethod() показал, что этот метод не имеет доступа к объекту <MyClass instance>. Но у него есть доступ к объекту <class MyClass>, который представляет сам класс (в Python вообще все является объектом, даже классы).

Стоит отметить, что при вызове MyClass.classmethod() Python автоматически передает класс в качестве первого аргумента функции. Это поведение Python запускается, если метод вызывается при помощи dot-синтаксиса. В методах экземпляра класса аналогично работает параметр self.

Пожалуйста, обратите внимание, что эти параметры именуются self и cls лишь в силу соглашений. С тем же успехом вы можете назвать их the_object и the_class. Важно то, что они идут первыми в списке параметров метода.

А теперь давайте вызовем статический метод:

>>> obj.staticmethod()
'static method called'

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

Просто когда при вызове статического метода с использованием dot-синтаксиса не передаются аргументы self или cls, Python применяет ограничения доступа.

Этот пример подтверждает, что статические методы не имеют доступа ни к состоянию экземпляра объекта, ни к состоянию класса. Они работают как обычные функции, но при этом относятся к пространству имен класса (и каждого его экземпляра).

Теперь давайте посмотрим, что произойдет, если мы попытаемся вызвать эти методы в самом классе, т. е., без предварительного создания экземпляра объекта:

>>> MyClass.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)
>>> MyClass.staticmethod()
'static method called'
>>> MyClass.method()
TypeError: unbound method method() must
    be called with MyClass instance as first
    argument (got nothing instead)

Нам прекрасно удалось вызвать classmethod() и staticmethod() , но попытка вызвать метод экземпляра класса method() провалилась (TypeError).

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

Это должно более четко разграничить три вида методов. Но я на этом не остановлюсь. В следующих двух разделах я разберу два немного более реалистичных примера использования разных видов методов.

Мои примеры будут основаны на классе Pizza:

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients
    def __repr__(self):
        return f'Pizza({self.ingredients!r})'
>>> Pizza(['cheese', 'tomatoes'])
Pizza(['cheese', 'tomatoes'])

Примечание. В этом примере кода (а также в последующих) для форматирования строки, возвращаемой при помощи __repr__, мы будем использовать Python 3.6 f-strings. В Python 2 и версиях Python 3 до 3.6 для форматирования строки следует использовать другие выражения, например:

def __repr__(self):
    return 'Pizza(%r)' % self.ingredients

Фабрики вкусной пиццы и @classmethod

Наверняка вы знаете, что существует множество вкусных вариантов пиццы:

Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)

Итальянцы придумали свои рецепты пиццы столетия назад, так что все они уже имеют какие-то устоявшиеся названия. Мы этим воспользуемся и дадим пользователям нашего класса Pizza лучший интерфейс для создания тех объектов пиццы, о которых они мечтают.

Это можно сделать красиво и чисто, используя методы класса в качестве фабричных функций для разных видов пиццы:

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients
    def __repr__(self):
        return f'Pizza({self.ingredients!r})'
    @classmethod
    def margherita(cls):
        return cls(['mozzarella', 'tomatoes'])
    @classmethod
    def prosciutto(cls):
        return cls(['mozzarella', 'tomatoes', 'ham'])

Обратите внимание, что я использую аргумент cls в фабричных методах margherita и prosciutto, а не вызываю конструктор Pizza напрямую.

Вы можете использовать этот прием, чтобы придерживаться принципа DRY (Don’t Repeat Yourself, «Не повторяйся»). Если в какой-то момент мы решим переименовать этот класс, нам не придется обновлять еще и имя конструктора во всех фабричных функциях classmethod.

Что же мы можем сделать при помощи этих фабричных методов? Давайте испытаем их:

>>> Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])
>>> Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])

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

Можно взглянуть на это применение методов класса и под другим углом: они позволяют вам определить альтернативные конструкторы для ваших классов.

Python допускает только один метод __init__ для каждого класса. Использование методов класса позволяет добавить столько альтернативных конструкторов, сколько нужно. Таким образом вы можете сделать интерфейс ваших классов самодокументированным (в определенной степени) и упростить их использование.

Когда стоит использовать статические методы

Здесь придумать хороший пример немного сложнее. Но знаете что? Я просто продолжу растягивать аналогию с пиццей.

Вот что я придумал:

import math
class Pizza:
    def __init__(self, radius, ingredients):
        self.radius = radius
        self.ingredients = ingredients
    def __repr__(self):
        return (f'Pizza({self.radius!r}, '
                f'{self.ingredients!r})')
    def area(self):
        return self.circle_area(self.radius)
    @staticmethod
    def circle_area(r):
        return r ** 2 * math.pi

Что здесь изменилось? Для начала, я изменил конструктор и __repr__, чтобы принимался дополнительный аргумент radius.

Также я добавил метод экземпляра класса area(), который вычисляет и возвращает площадь пиццы (это также хороший кандидат для @property, но пример у нас игрушечный).

Вместо вычисления площади непосредственно в area(), с использованием всем известной формулы площади круга, я вынес его в отдельный статический метод circle_area().

>>> p = Pizza(4, ['mozzarella', 'tomatoes'])
>>> p
Pizza(4, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669

Конечно, этот пример несколько простоват, но он хорошо помогает объяснить некоторые преимущества статических методов.

Как мы уже знаем, статические методы не имеют доступа к состоянию класса или объекта, потому что не принимают аргументы cls или self. Это большое ограничение, но также и отличный сигнал, показывающий, что данный метод ни от чего не зависит.

В приведенном выше примере ясно, что circle_area() никоим образом не может изменять класс или экземпляр класса. (Конечно, это всегда можно обойти при помощи глобальной переменной, но здесь мы не будем это разбирать).

Так в чем же польза?

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

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

Другими словами, использование статических методов и методов классов это способ сообщить о намерениях разработчика. Одновременно это способ обеспечить осуществление этих намерений в достаточной мере, чтобы избежать большинства ошибок, которые можно было бы допустить по невнимательности, и багов, которые разрушили бы дизайн.

Написание некоторых из ваших методов подобным образом (нечасто и там, где это имеет смысл) может принести пользу в плане поддерживаемости кода. Также это снижает вероятность того, что другие разработчики используют ваши классы неправильно.

Статические методы также имеют преимущество при тестировании кода.

Поскольку метод circle_area() совершенно не зависит от остального класса, его гораздо проще тестировать. Мы можем это сделать при помощи модульного теста, не беспокоясь об экземпляре класса в целом. То есть, этот метод тестируется, как обычная функция. Опять же, это облегчает поддержку кода в будущем.

Ключевые выводы

  • Для методов экземпляра класса наличие самого экземпляра класса является обязательным. Доступ к нему они могут иметь благодаря параметру self.
  • Методы класса в экземпляре класса не нуждаются. Они не имеют доступа к экземпляру класса, но имеют доступ к самому классу благодаря параметру cls.
  • Статические методы не имеют доступа к cls или self. Они действуют как обычные функции, но принадлежат к пространству имен класса.
  • Статические методы и методы класса сообщают о намерениях разработчика относительно дизайна класса (и в определенной степени принудительно их осуществляют). Это дает преимущества по части поддерживаемости кода.

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

4 комментария к “Простое объяснение методов класса, экземпляра класса и статических методов в Python”

  1. Анддрей

    Ай-ай-ай! Как не стыдно плагиатить чужой труд из книг. Это ж объяснение из книги «Чистый Python». В точь-точь.

    1. Это перевод статьи сайта RealPython, владельцем и прогером которого является автор этой самой книги «Чистый Python»

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх