четверг, 17 декабря 2015 г.

Переопределение методов в Python

переопределения методов (классы нового и старого стиля)

Мы рассмотрели с вами такие подходы, как наследование и расширение классов своими методами. Теперь поговорим о таком подходе, как переопределение методов при наследовании и кратко упомянем про классы нового и старого стиля.
Что касательно старого и нового стиля классов в Python, углубляться сильно не будем. Подчеркну только то, что в Python ветки 3x, все создаваемые классы, автоматически являются классами нового стиля и других вариантов не существует. В Python 2.7, существуют два типа стилей, старый и новый. Когда мы определяем класс и не наследуем его от какого либо класса Python, он является классом старого стиля. Если мы описываем класс и у нас нет необходимости наследовать его от какого либо класса Python, наследуем его всегда от класса "object". Класс "object", это некий абстрактный класс, который реализует механизмы, которые мы называем классом нового стиля. Детальную разницу между этими стилями, вы можете узнать в документации по Python 2.7, или из рекомендуемой к прочтению литературы. Я же предполагаю, что далее мы всегда будем говорить про классы нового стиля, Т.Е. Свои классы, мы будем наследовать от абстрактного класса "object".
Вернемся к переопределению методов. Что таит в себе это понятие? Давайте представим, что в классе от которого мы будем наследовать наш новый класс, уже реализован какой-то метод, который, впринципе, подходит нам по функциональности, но его реализации чего-то не хватает, или в реализацию требуется внести какие-то правки. Естественно, мы можем просто пересоздать этот метод в нашем классе, полностью переписав его код, но это приводит нас к извечной проблеме, а именно, к избыточному коду. У нас есть штатная возможность взять метод наследуемого класса, использовать его в создаваемом методе нового класса и переопределить/добавить часть функциональности. Для примера возьмем все тех же роботов, которые были в предыдущей теме. Насамом деле, нам тут хватит и одного робота, пусть это будет робот-спамер, на основе класса "Robot":

class Robot:
    def __init__(self, e):
        self.energy = e
    def say_energy(self):
        print u"Осталось {0} энергии".format(self.energy)

class RobotSpamer(Robot):
    def activate(self):    
        if self.energy != 0:
            print "Spam"*1000
            print u"Я заспамил весь экран!"
            self.energy -= 1
        else:
            print u"Моя энергия равна нулю и я не могу выполнить это действие!"

В нашем классе робота-спамера все хорошо, но мы бы хотели добавить в нашу новую версию робота-спамера новый функционал.
Во-первых, добавить в него метод включения/выключения робота, чтобы случайно не воспользоваться им, Т.Е. Пока робот будет в выключенном состоянии, мы не сможем воспользоваться его методом "activate", что может уберечь нас от случайного его применения.
Во-вторых, собственно, переопредлить метод "activate" таким образом, чтобы робот не спамил, если он находится в выключенном состоянии.
Для этого, мы опишем новый класс, который будет наследовать класс "RobotSpamer", а значит, будет наследовать его функционал и функционал тех классов, который наследуется классом "RobotSpamer", в нашем случае - это класс "Robot". Напишем необходимый код, а после детально разберем его:

class RobotSpamerToActivate(RobotSpamer):
    activated = False # Изначально робот выключен
    def power_on(self):
        self.activated = True
    def power_off(self):
        self.activated = False
    # Методы включения и выключения робота
    def activate(self):
        if not self.activated: # Если робот выключен
            print u"Я выключен и не могу выполнить это действие!"
        else:
            super(RobotSpamerToActivate, self).activate()

Основная часть кода вполне очевидна. Объявлен атрибут "activated", который отвечает за состояние робота - (True - включен), (False - выключен). Также объявлены 2 метода, которые позволяют изменять состояние робота. А вот дальше происходит переопределение метода "activate", с расширением функционала. Для начала, мы просто объявляем метод "activate" и первым делом, проверяем включен ли робот. Если робот выключен, мы выводим соответствующее статусное уведомление и больше ничего не делаем, робот же выключен. Если робот находится во включенном состоянии, мы при помощи функции "super()", вызываем к жизни метод наследуемого класса. Функция "super()", принимает два аргумента, первый - это имя класса, в котором мы будем вызывать необходимый метод, Т.Е. класс, в котором мы описываем перегружаемый метод. Второй аргумент - это (имя наследуемого класса.необходимый метод(все необходимые аргументы [self] обязателен)). Таким образом, когда робот будет включен, для нашего нового робота, будет вызван метод из наследуемого класса "RobotSpamer", который вполне устраивает нас своей функциональностью. Давайте посмотрим на работу нашего нового робота:

>>> spamer =RobotSpamerToActivate(2) # Передаем количество зарядов
>>> dir(spamer)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'activate', 'activated', 'energy', 'power_off', 'power_on', 'say_energy']
>>> spamer.activate()
Я выключен и не могу выполнить это действие!
>>> spamer.power_on()
>>> spamer.activate()
SpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpam
...

Я заспамил весь экран!
>>> spamer.activate()
SpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpamSpam
...

Я заспамил весь экран!
>>> spamer.activate()
Моя энергия равна нулю и я не могу выполнить это действие!
>>>

Подобным подходом, мы избежали избыточного кода и в полной мере реализовали поставленную задачу, используя концепцию наследования и переопределения методов. Незаметно, но постоянно, мы используем такие понятия, как инкапсуляция и полиморфизм. Инкапсуляция проявляется каждый раз, когда мы реализуем новый класс. Конечному пользователю наших классов совершенно ненужно знать, как они реализованы, ему необходимо знать только интерфейсы, Т.Е. какие методы он должен вызывать и какие аргументы передавать.
Полиморфизм выражается в том, что для любого из созданных роботов, конечному пользователю не нужно запоминать специфические методы активации робота, для всех он один - "activate". А вот сам объект, для которого вызывается этот метод, определяет, какие действия будут совершены в конечном счете.
Таким же образом, мы можем перегружать и наш конструктор класса. Допустим, всем новым роботам, мы хотим присваивать индивидуальное название, чтобы их можно было отличать между собой, Но хотим сохранить его изначальную функциональность, в нашем случае - это указание количества энергии. А для большего удобства, мы всегда будем присваивать, если не будет указано что-то другое, хотябы одну единицу энергии:

class RobotSpamerToActivate(RobotSpamer):
    def __init__(self, n, e=1):
        self.name = n
        super(RobotSpamerToActivate, self).__init__()
    activated = False # Изначально робот выключен
    def power_on(self):
        self.activated = True
    def power_off(self):
        self.activated = False
    # Методы включения и выключения робота
    def activate(self):
        if not self.activated: # Если робот выключен
            print u"Я выключен и не могу выполнить это действие!"
        else:
            super(RobotSpamerToActivate, RobotSpamer.activate(self))

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

>>> spamer = RobotSpamerToActivate("Спамер_001")
>>> spamer.name
'\x91\xaf\xa0\xac\xa5\xe0_001'
>>> print spamer.name
Спамер_001
>>> spamer.say_energy()
Осталось 1 энергии
>>>

6 комментариев:

  1. круто! спасибо большое, все понятно написано!

    ОтветитьУдалить
  2. Сбиваете новичков с толку. То, о чем вы пишите, называется переопределением метода. А перегрузки функций в питоне нет как таковой.
    https://en.wikipedia.org/wiki/Method_overriding
    https://en.wikipedia.org/wiki/Function_overloading

    ОтветитьУдалить
    Ответы
    1. Да, каюсь, терминология была неверной, руки не доходили поправить.
      Но, что интереснее, выправляя это, обнаружил грубейшие ошибки в использовании super, а вы их не заметили? :-)
      Спасибо за коммент, все было поправлено. Хотя материалы морально устарели и все нужно переписывать.

      Удалить
    2. Перегрузка функций на Питоне.
      Пример:
      def func(int1, int2 = None):
      if int2 is not None:
      print(int1 + int2)
      else:
      print(int1)

      Удалить
  3. Спасибо за ваш труд! Было бы очень круто видеть ваши публикации с подсветкой кода)

    ОтветитьУдалить
  4. перегрузка работает только так https://pythononline.ru/question/kak-peregruzit-metod-python

    ОтветитьУдалить