Экземпляры, классы и статические методы в Python

Оглавление

В этом учебном пособии я расскажу, что скрывается за методами классов, статическими методами и обычными методами экземпляров.

Если вы выработаете интуитивное понимание их различий, то сможете писать объектно-ориентированный 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, который при вызове метода указывает на класс, а не на экземпляр объекта.

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

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

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

Этот тип метода не принимает ни self, ни cls параметров (но, конечно, он может принимать произвольное количество других параметров).

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

Давайте посмотрим на них в действии!

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

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

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

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

>>>

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

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

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

>>>

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

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

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

Далее опробуем метод класса :

>>>

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

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

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

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

Время вызова статического метода now:

>>>

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

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

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

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

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

>>>

>>> 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'])

Примечание: В данном примере и последующих примерах используется Python 3.6 f-strings для построения строки, возвращаемой командой __repr__. В 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 напрямую.

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

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

>>>

>>> 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.
  • Методы класса не нуждаются в экземпляре класса. Они не могут получить доступ к экземпляру (self), но имеют доступ к самому классу через cls.
  • Статические методы не имеют доступа ни к cls, ни к self. Они работают как обычные функции, но принадлежат пространству имен класса.
  • Статические методы и методы класса передают и (в определенной степени) обеспечивают реализацию намерений разработчика относительно дизайна класса. Это может принести пользу в обслуживании.
Вернуться на верх