Статьи

Полное руководство по использованию статических, классовых или абстрактных методов в Python

Проверка кода — отличный способ обнаружить вещи, которые люди могут изо всех сил пытаться понять. Во время проверки корректуры OpenStack недавно я заметил, что люди неправильно используют различные декораторы, которые Python предоставляет для методов. Итак, вот моя попытка предоставить мне ссылку для отправки в моих следующих обзорах кода. ?

Как методы работают в Python

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

>>> class Pizza(object):
...     def __init__(self, size):
...         self.size = size
...     def get_size(self):
...         return self.size
...
>>> Pizza.get_size
<unbound method Pizza.get_size>

Python сообщает вам, что атрибут get_size класса Pizza является несвязанным методом . Что это значит? Мы узнаем, как только мы попытаемся это назвать:

>>> Pizza.get_size()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method get_size() must be called with Pizza instance as first argument (got nothing instead)

Мы не можем назвать это, потому что это не связано ни с одним экземпляром Пиццы . Методу нужен экземпляр в качестве первого аргумента (в Python 2 это должен быть экземпляр этого класса, а в Python 3 это может быть что угодно). Давайте попробуем сделать это, тогда:

>>> Pizza.get_size(Pizza(42))
42

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

Поэтому Python делает для нас то, что он связывает все методы класса Pizza с любым экземпляром этого класса. Это означает, что атрибут get_size экземпляра Pizza является связанным методом: методом, для которого первым аргументом будет сам экземпляр.

>>> Pizza(42).get_size
<bound method Pizza.get_size of <__main__.Pizza object at 0x7f3138827910>>
>>> Pizza(42).get_size()
42

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

>>> m = Pizza(42).get_size
>>> m()
42

На самом деле, вам даже не нужно хранить ссылку на объект Pizza . Его метод привязан к объекту, поэтому метод сам по себе достаточен.

Но что, если вы хотите знать, с каким объектом связан этот связанный метод? Вот маленький трюк:

>>> m = Pizza(42).get_size
>>> m.__self__
<__main__.Pizza object at 0x7f3138827910>
>>> # You could guess, look at this:
...
>>> m == m.__self__.get_size
True

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

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

>>> class Pizza(object):
...     def __init__(self, size):
...         self.size = size
...     def get_size(self):
...         return self.size
...
>>> Pizza.get_size
<function Pizza.get_size at 0x7f307f984dd0>

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

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

class Pizza(object):
    @staticmethod
    def mix_ingredients(x, y):
        return x + y
 
    def cook(self):
        return self.mix_ingredients(self.cheese, self.vegetables)

В таком случае написание mix_ingredients в качестве нестатического метода также будет работать, но оно предоставит аргумент self , который не будет использоваться. Здесь декоратор @staticmethod покупает нам несколько вещей:

  • Python не должен создавать экземпляр привязанного метода для каждого объекта Pizza, который мы запускаем. Связанные методы также являются объектами, и создание их имеет свою стоимость. Наличие статического метода позволяет избежать:
    >>> Pizza().cook is Pizza().cook
    False
    >>> Pizza().mix_ingredients is Pizza.mix_ingredients
    True
    >>> Pizza().mix_ingredients is Pizza().mix_ingredients
    True
  • Это облегчает читабельность кода: видя @staticmethod , мы знаем, что метод не зависит от состояния самого объекта.

  • Это позволяет нам переопределить метод mix_ingredients в подклассе. Если бы мы использовали функцию mix_ingredients , определенные на верхнем уровне нашего модуля, класс наследующий Pizza wouln’t быть в состоянии изменить способ , которым мы смешивать ингредиенты для нашей пиццы без перекрывая готовить себя.

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

Сказав это, что такое методы класса? Методы класса — это методы, которые связаны не с объектом, а с… классом!

>>> class Pizza(object):
...     radius = 42
...     @classmethod
...     def get_radius(cls):
...         return cls.radius
... 
>>> 
>>> Pizza.get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
>>> Pizza().get_radius
<bound method type.get_radius of <class '__main__.Pizza'>>
>>> Pizza.get_radius is Pizza().get_radius
True
>>> Pizza.get_radius()
42

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

Когда использовать этот метод? Ну, методы класса в основном полезны для двух типов методов:

  • Методы фабрики, которые используются для создания экземпляра класса с использованием, например, некоторой предварительной обработки. Если вместо этого мы используем @staticmethod , нам придется жестко закодировать имя класса Pizza в нашей функции, чтобы любой класс, унаследованный от Pizza, не мог использовать нашу фабрику для собственного использования.
    class Pizza(object):
        def __init__(self, ingredients):
            self.ingredients = ingredients
     
        @classmethod
        def from_fridge(cls, fridge):
            return cls(fridge.get_cheese() + fridge.get_vegetables())
  • Статические методы, вызывающие статические методы: если вы разбиваете статический метод на несколько статических методов, вам не нужно жестко кодировать имя класса, но вы должны использовать методы класса. Используя этот способ для объявления вашего метода, на имя Pizza никогда не ссылаются напрямую, а наследование и переопределение метода будут работать без нареканий.
    class Pizza(object):
        def __init__(self, radius, height):
            self.radius = radius
            self.height = height
     
        @staticmethod
        def compute_circumference(radius):
             return math.pi * (radius ** 2)
     
        @classmethod
        def compute_volume(cls, height, radius):
             return height * cls.compute_circumference(radius)
     
        def get_volume(self):
            return self.compute_volume(self.height, self.radius)

Абстрактные методы

Абстрактный метод — это метод, определенный в базовом классе, но он может не обеспечивать какую-либо реализацию. В Java это описывало бы методы интерфейса.

Итак, самый простой способ написать абстрактный метод в Python:

class Pizza(object):
    def get_radius(self):
        raise NotImplementedError

Любой класс, унаследованный от Pizza, должен реализовать и переопределить метод get_radius , иначе возникнет исключение.

Этот конкретный способ реализации абстрактного метода имеет недостаток. Если вы напишите класс, унаследованный от Pizza, и забудете реализовать get_radius , ошибка возникнет только при попытке использовать этот метод.

>>> Pizza()
<__main__.Pizza object at 0x7fb747353d90>
>>> Pizza().get_radius()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in get_radius
NotImplementedError

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

import abc
 
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    @abc.abstractmethod
    def get_radius(self):
         """Method that should do something."""

Используя abc и его специальный класс, как только вы попытаетесь создать экземпляр BasePizza или любого класса, унаследованного от него, вы получите TypeError .

>>> BasePizza()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class BasePizza with abstract methods get_radius

Смешивание статических, классовых и абстрактных методов

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

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

import abc
 
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    @abc.abstractmethod
    def get_ingredients(self):
         """Returns the ingredient list."""
 
class Calzone(BasePizza):
    def get_ingredients(self, with_egg=False):
        egg = Egg() if with_egg else None
        return self.ingredients + egg

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

import abc
 
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    @abc.abstractmethod
    def get_ingredients(self):
         """Returns the ingredient list."""
 
class DietPizza(BasePizza):
    @staticmethod
    def get_ingredients():
        return None

Это также правильно и соответствует контракту, который мы имеем с нашим абстрактным классом BasePizza . Тот факт, что методу get_ingredients не нужно знать об объекте для возврата результата, является деталью реализации, а не критерием выполнения нашего контракта.

Следовательно, вы не можете заставить реализацию вашего абстрактного метода быть обычным, классовым или статическим методом, и, возможно, не следует. Начиная с Python 3 (это не будет работать так, как вы ожидаете в Python 2, см. Выпуск 5867 ), теперь можно использовать декораторы @staticmethod и @classmethod поверх @abstractmethod :

import abc
 
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    ingredient = ['cheese']
 
    @classmethod
    @abc.abstractmethod
    def get_ingredients(cls):
         """Returns the ingredient list."""
         return cls.ingredients

Не читайте неправильно: если вы думаете, что это заставит ваши подклассы реализовать get_ingredients как метод класса, вы ошибаетесь. Это просто означает, что ваша реализация get_ingredients в классе BasePizza является методом класса.

Реализация в абстрактном методе? Да! В Python, в отличие от методов в интерфейсах Java, вы можете иметь код в своих абстрактных методах и вызывать его через super () :

import abc
 
class BasePizza(object):
    __metaclass__  = abc.ABCMeta
 
    default_ingredients = ['cheese']
 
    @classmethod
    @abc.abstractmethod
    def get_ingredients(cls):
         """Returns the ingredient list."""
         return cls.default_ingredients
 
class DietPizza(BasePizza):
    def get_ingredients(self):
        return ['egg'] + super(DietPizza, self).get_ingredients()

В таком случае каждая пицца, которую вы построите путем наследования от BasePizza, должна будет переопределить метод get_ingredients , но сможет использовать механизм по умолчанию для получения списка ингредиентов с помощью super () .