Срыв покровов от Дэна Бадэра: экземпляры, классы и статичные методы в 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'

 

Заметка пользователям Python2: декораторы  @staticmethod и  @classmethod доступны начиная с версии 2.4 и данный пример будет работать как есть. Вместо объявления класса  class MyClass: вы можете определить новый класс, наследуясь от  object

class MyClass(object):

Теперь можно продолжить.

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

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

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

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

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

Теперь рассмотрим  MyClass.classmethod

Я пометил этот метод декоратором   @classmethod, чтобы обозначить, что это метод класса.

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

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

Статичные методы

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

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

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

Смотрим в действии!

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

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

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

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

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

Когда метод вызывается, Python подставляет вместо  self экземпляр объекта,  obj. Мы можем избавиться от синтаксического сахара ( 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 всё объекты, даже сами классы)

Заметьте, Python автоматически передаёт класс как первый аргумент, когда мы вызываем  MyClass.classmethod(). Вызов метода через точку определяет такое поведение. Параметр  self в методах экземпляра работает аналогично.

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

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

>>> 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() вызываются без проблем, но попытка вызова метода экземпляра не удалась ( 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'])

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

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

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

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

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

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

Хороший способ сделать это — использовать методы класса в качестве фабричных функций для различных типов пиццы:

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 напрямую.

Это трюк, который вы можете использовать, чтобы следовать принципу “Не повторяйся”. Если мы решим переименовать класс, нам не нужно помнить об обновлении имени конструктора во всех фабричных функциях  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.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669

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

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

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

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

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

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

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

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

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

Поскольку метод  circle_area() полностью независим от остального в классе, его гораздо проще тестировать.

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

Ключевые моменты

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

Оригинал

Ссылка на основную публикацию