Программирование на Python

Множественное наследование

Разбить на страницы
Показывать лекцию целиком

До сих пор мы рассматривали построение иерархии классов, связанных одиночным наследованием. У класса потомка был только один родитель, не считая прародителя - класса object. Конечно, намного полезней при моделировании многих практических задач иметь более мощный механизм множественного наследования. Недаром у каждого человека есть два родителя.

Реализация множественного наследования значительно сложнее, чем одиночного наследования, поскольку возникают коллизии, где и что наследовать от каждого родителя. Поэтому, например, в языке C# нет множественного наследования классов, а допускается только множественное наследование интерфейсов.

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

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

Построение класса CreatingStr

Практически все классы определяют метод __str__. В большинстве случаев этот метод строится по стандартной схеме. Результатом работы метода является строка, содержащая для каждого атрибута метода имя атрибута и его значение. Можно было бы код, реализующий эту стратегию, встраивать в каждый метод, следуя разработанному шаблону. Но намного полезнее избежать дублирования кода, а использовать возможности наследования. Наш класс, который мы будем строить, содержит единственный метод __str__, который будут наследовать все потомки этого класса. Для построения метода класс будет вызывать метод потомка, которого он знать не знает, но уверен, что все потомки имеют вызываемый метод __ dict__. Этот встроенный метод, имеющийся у всех классов, представляет словарь, содержащий для каждого атрибута метода пару, где ключом является имя атрибута, а вторым элементом пары является значение атрибута, что и позволяет построить стандартную реализацию метода __str__. Заметьте, эта реализация основана на специфическом полиморфизме Python. Потомок вызывает метод родителя, родитель в свою очередь вызывает метод потомка. В отличие от классического полиморфизма, где все потомки принадлежат некоторой иерархии классов, связанных отношением наследования, в данной ситуации все потомки могут быть классами, не связанными никакими отношениями. Для успеха работы требуется лишь, чтобы не возникали синтаксические проблемы при вызове метода потомка. Метод __dict__ имеется у всех классов, так что проблемы не возникают. Приведу достаточно простую и понятную реализацию этого класса:

class CreatingStr(object):
    """
    Реализует наследуемый метод __str__, отображающий
    атрибуты класса потомка в виде пар имя = значение. 
    Может добавляться в качестве родителя в любые классы.
    """
    def __str__(self):
        attributs = []
        for key in sorted(self.__dict__):
            attributs.append('%s = %s' % (key, self.__dict__[key]))
            #attributs.append('%s = %s' % (key, getattr(self, key)))
        s = ', '.join(attributs)
        return '[%s:  %s]' % ("Атрибуты объекта: ", s)

Метод __str__ реализуется в несколько строчек кода. Вызываемый метод потомка __dict__ возвращает словарь, содержащий атрибуты потомка. Метод sorted сортирует ключи атрибутов в алфавитном порядке. В цикле по отсортированным ключам формируется список атрибутов, содержащий пары из ключа и значения атрибутов. Метод join преобразует список в строку, которая и возвращается в качестве результата с предшествующим текстом "Атрибуты объекта:". Закомментированная строка кода показывает другой возможный вариант получения значения атрибута, используя вызов метода getattr.

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

Проектирование иерархии классов с множественным наследованием

Создадим вначале два класса - Pa и Ma, представляющие родителей их потомка - класса Child.

Вот код класса Pa:

from CreatingStr import CreatingStr
class Pa(CreatingStr):
    """один из родителей - отец"""
    #конструктор 
    #поля класса
    #fam: string - фамилия
    #sex: string - пол
    #height: string - рост
    #persistance: string - настойчивость 
    def __init__(self, fam, sex, height = 'высокий', 
                 persistance = 'настойчив в достижении цели'):
        self.fam = fam 
        self.sex = sex
        self.height = height
        self.persistance = persistance
    #Методы 
    def smart(self, task):
        return "оригинальное решение задачи: " + task
    def common(self):
        return 'Работает Pa'

У класса Pa четыре поля, задающие свойства объектов этого класса. Два атрибута класса заданы позиционными параметрами конструктора класса, два - именованными параметрами, имеющими значение по умолчанию. Класс также определяет уникальный метод smart, которая в данном модельном проекте представляет строку текста. У класса есть также метод common. Метод с таким же именем имеется и у класса Ma, но реализации этого метода в обоих классах различны.

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

Приведем теперь код класса Ma, схожий во многом с кодом класса Pa:

from CreatingStr import CreatingStr
class Ma(CreatingStr):
    """один из родителей - мать"""
    #конструктор 
    #поля класса
    #fam: string - фамилия
    #sex: string - пол
    #height: string - рост
    #goodness: string - доброта, отзывчивость 
    def __init__(self, fam, sex, height = 'средний', 
                 goodness = 'Готовность помогать'):
        self.fam = fam 
        self.sex = sex
        self.height = height
        self.goodness = goodness
    #Методы 
    def beauty(self, task):
        return "красивое решение задачи: " + task
    def common(self):
        return 'Работает Ma'

У класса Ma, также как и у класса Pa, четыре поля и два метода. Этот класс также является потомком класса CreatingStr. Заметьте, первые три поля у обоих классов совпадают, но значение по умолчанию поля height - разные.

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

Давайте, первым делом, приведем код класса Child, имеющего двух прямых родителей и общего предка:

from CreatingStr import CreatingStr
from Pa import Pa
from Ma import Ma
class Child(Pa, Ma):
    """потомок служебного класса и двух родителей """
    def __init__(self, fam, sex, age, 
       height = 'соответствует возрасту', 
       persistence = 'как у Ра', goodness = 'как у Ма' ):        
        Pa.__init__(self,fam, sex)
        Ma.__init__(self, fam, sex)
        self.age = age
    
    def skill(self):
        return("Умелые ручки!")

Заметьте, первым делом, что в списке родителей нет класса CreatingStr. Его нельзя включать в этот список, поскольку он наследуется классами Pa и Ma, а, следовательно, и их потомком - классом Child. Попытка включить этот класс в список непосредственных родителей класса Child приведет к появлению ошибки.

Класс Child помимо тех свойств и методов, которые он наследует от своих прямых родителей и предков, имеет собственное свойство - age и собственный метод - skill. Давайте на примере этого класса разберемся как устроено множественное наследование. Мы понимаем, что множественное наследование позволяет строить сложную иерархию классов. Граф, задающий структуру множественного наследования для нашего класса потомка, представляет собой сеть с одним источником - классом object и одним стоком - классом потомка.

Список атрибутов и методов класса потомка. Рекурсивный алгоритм

Рассмотрим, как формируются список атрибутов (свойств) и список методов, который потомок получает в результате наследования. Сформулируем рекурсивный алгоритм формирования этих списков - FormLists(P). Но прежде зададим два правила, которым подчиняется этот алгоритм:

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

    While(canUp(P)
    {
         Q = Up(P); FormLists(Q); 
    }
    AddLists(P);
    

    Булева функция canUp, определяющая возможность подъема для класса P возвращает значение false в двух случаях:

  • Класс P не имеет родителей. Такой класс единственный - это класс object - прародитель всех классов.
  • У класса P есть список родителей, но все родители из этого списка уже были просмотрены и их имена находятся в списке обработанных классов.
  • Функция Up(P) - функция подъема на следующий уровень наследования, возвращает в качестве результата класс Q - первый еще не просмотренный класс при проходе по списку родителей слева направо. Имя возвращаемого класса заносится в список обработанных классов. Для полученного класса Q рекурсивно вызывается метод FormLists.

    Цикл While заканчивает свою работу, когда будут обработаны все родительские классы, а, благодаря рекурсии, и все классы предков. Метод AddList выполняется для каждого класса уровня k после того, как будут обработаны все классы уровня k + 1 и выше. При выполнении этого метода каждый класс добавляет свою информацию в три глобальных списка:

  • List_at - список атрибутов, наследуемых потомком.
  • List_met - список методов, наследуемых потомком.
  • List_in - список встроенных атрибутов и методов, наследуемых потомком.
  • Здесь также выполняется правило, что метод или атрибут добавляется в глобальный список только тогда, когда его нет в списке, дублирование исключается.

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

    Рассмотрим, как будет выполняться метод FormLists(Child). Поскольку подъем возможен, то рекурсивно будут вызываться методы FormList(Pa) => FormList(CreateStr) => FormList(Object). У класса Object родителей нет, подъем будет завершен и будет вызван метод AddLists для этого класса. При выполнении этого метода все встроенные атрибуты и методы класса Object будут добавлены в список List_in. Фактически это означает, что класс Child наследует все эти свойства и методы. По завершении метода завершит работу и метод FormLists(Objects) и управление вернется в метод FormLists(CreateStr). Поскольку других путей подъема у этого класса нет, то для него будет вызван метод AddList, который переопределит встроенный метод __str__ . После чего управление возвратится к методу FormList(Pa). И у класса Pa есть единственный уже пройденный путь подъема, так что и для него будет вызван метод AddList. Класс Pa добавит свои четыре свойства в список List_at и два метода с их реализацией в список List_met. После чего управление вернется в метод FormList(Child). Здесь есть еще один путь подъема и будет вызван метод FormLists(Ma). У класса Ma нет путей подъема, ведущих к еще не посещаемых классах, поэтому для него сразу же будет вызван метод AddLists, который добавит в списки List_at, List_met уникальные свойства и методы класса Ma. После чего управление вновь вернется к методу FormListst(Child). Поскольку других путей подъема нет, то завершает работу метод AddLists(Child), который добавит свои собственные атрибуты и методы к унаследованным атрибутам свойих родителей и предков. Такова возможная реализация схемы множественного наследования в языке Python.

    Формирование конструктора класса потомка при множественном наследовании

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

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

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

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

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

         def __init__(self, fam, sex, age, 
           height = 'соответствует возрасту', 
           persistence = 'как у Ра', goodness = 'как у Ма' ):        
            Pa.__init__(self,fam, sex)
            Ma.__init__(self, fam, sex)
            self.age = age
    

    Заметьте, у классов Pa, Ma есть именованные параметры, но они не указаны при вызове конструкторов этих классов.

    Построю теперь тест в тестирующем классе Inheheritance, в котором создаются объекты классов Pa, Ma, Child:

    from CreatingStr import CreatingStr
    from Pa import Pa
    from Ma import Ma
    from Child import Child
    def test7():
        papa = Pa('Петров', 'муж')
        print(papa)
        mama = Ma('Петрова', 'жен')
        print(mama)
        childAnn = Child('Петрова', 'жен', 5)
        print (childAnn)
        s1 = childAnn.smart('поиск текста')
        s2 = childAnn.beauty('поиск текста')
        s3 = childAnn.common()
        s4 = childAnn.skill()
        print (s1); print(s2); print(s3); print(s4)
    

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

    Результаты нуждаются в пояснении.

    Прежде всего заметьте, все три класса не определяют собственный метод __str__, а наследуют его от класса CreateStr. Все прекрасно работает, - при печати объекта выводятся все его свойства в алфавитном порядке.

    С выводом позиционных параметров никаких проблем нет. Для именованных атрибутов используются значения по умолчанию. Если именованные параметры уникальны, то и здесь проблем нет. Но если есть общие параметры с разными значениями по умолчанию, то какое значение будет выбрано? В нашем примере первым будет вызван родитель класса Pa, и он полям по умолчанию даст свои значения. Но потом будет работать конструктор класса Ma, и он переопределит значения полей по умолчанию. При вызове конструкторов действует принцип "Последний- Победитель". Можно видеть по результатам теста, что для свойства height победителем является класс Ma, несмотря на то что значение по умолчанию задали все три класса, в том числе и сам потомок Child. Как быть, если потомок хочет, чтобы его значение становилось победителем в подобных ситуациях? Решение просто, - необходимо при вызове конструктора родителя включить соответствующий атрибут в список вызова, задав нужное значение по умолчанию. Но включить этот атрибут в вызов последнего родителя, обладающего этим атрибутом.

    Пусть в конструкторе класса Child вызов конструкторов родителя имеет вид:

     Pa.__init__(self,fam, sex, height = 'соответствует возрасту')
            Ma.__init__(self, fam, sex)  
    

    Победителем в этом случае по-прежнему будет класс Ма, работающий последним. Если же вызов конструкторов будет иметь вид:

                   Pa.__init__(self,fam, sex)
            Ma.__init__(self, fam, sex, height = 'соответствует возрасту')
    

    В этом случае победит потомок. Конструктор класса Ма задаст значение по умолчанию, определяемое потомком.

    Алмазное наследование

    Рассмотрим частный случай множественного наследования - алмазное (diamond) наследование. В этой схеме потомок наследует от двух родителей, которые оба являются наследниками одного и того же класса. Свое название эта схема получила из-за того, что граф наследования напоминает схему строения атома углерода (алмаза).

    У класса A два метода - f и q. Потомок B1 наследует метод q, переопределяет метод f и дает имя h родительскому методу f. Потомок B2 также наследует метод q, переопределяет метод f и дает имя g родительскому методу f. Что наследует класс C - потомок классов B1 и B2? От класса B1 он наследует все - метод f(x, y), метод q, унаследованный от класса A, метод h, под чьим именем скрывается метод f класса A. При наследовании от класса B2 методы с именами f и q будут проигнорированы, поскольку такие имена уже есть (действует принцип Первый - Победитель). Хотя под именем g скрывается уже унаследованный метод f, но у него другое, новое имя, поэтому он будет наследован классом C.

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

    Страницы:

    До сих пор мы рассматривали построение иерархии классов, связанных одиночным наследованием. У класса потомка был только один родитель, не считая прародителя - класса object. Конечно, намного полезней при моделировании многих практических задач иметь более мощный механизм множественного наследования. Недаром у каждого человека есть два родителя.

    Реализация множественного наследования значительно сложнее, чем одиночного наследования, поскольку возникают коллизии, где и что наследовать от каждого родителя. Поэтому, например, в языке C# нет множественного наследования классов, а допускается только множественное наследование интерфейсов.

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

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

    Построение класса CreatingStr

    Практически все классы определяют метод __str__. В большинстве случаев этот метод строится по стандартной схеме. Результатом работы метода является строка, содержащая для каждого атрибута метода имя атрибута и его значение. Можно было бы код, реализующий эту стратегию, встраивать в каждый метод, следуя разработанному шаблону. Но намного полезнее избежать дублирования кода, а использовать возможности наследования. Наш класс, который мы будем строить, содержит единственный метод __str__, который будут наследовать все потомки этого класса. Для построения метода класс будет вызывать метод потомка, которого он знать не знает, но уверен, что все потомки имеют вызываемый метод __ dict__. Этот встроенный метод, имеющийся у всех классов, представляет словарь, содержащий для каждого атрибута метода пару, где ключом является имя атрибута, а вторым элементом пары является значение атрибута, что и позволяет построить стандартную реализацию метода __str__. Заметьте, эта реализация основана на специфическом полиморфизме Python. Потомок вызывает метод родителя, родитель в свою очередь вызывает метод потомка. В отличие от классического полиморфизма, где все потомки принадлежат некоторой иерархии классов, связанных отношением наследования, в данной ситуации все потомки могут быть классами, не связанными никакими отношениями. Для успеха работы требуется лишь, чтобы не возникали синтаксические проблемы при вызове метода потомка. Метод __dict__ имеется у всех классов, так что проблемы не возникают. Приведу достаточно простую и понятную реализацию этого класса:

    class CreatingStr(object):
        """
        Реализует наследуемый метод __str__, отображающий
        атрибуты класса потомка в виде пар имя = значение. 
        Может добавляться в качестве родителя в любые классы.
        """
        def __str__(self):
            attributs = []
            for key in sorted(self.__dict__):
                attributs.append('%s = %s' % (key, self.__dict__[key]))
                #attributs.append('%s = %s' % (key, getattr(self, key)))
            s = ', '.join(attributs)
            return '[%s:  %s]' % ("Атрибуты объекта: ", s)
    

    Метод __str__ реализуется в несколько строчек кода. Вызываемый метод потомка __dict__ возвращает словарь, содержащий атрибуты потомка. Метод sorted сортирует ключи атрибутов в алфавитном порядке. В цикле по отсортированным ключам формируется список атрибутов, содержащий пары из ключа и значения атрибутов. Метод join преобразует список в строку, которая и возвращается в качестве результата с предшествующим текстом "Атрибуты объекта:". Закомментированная строка кода показывает другой возможный вариант получения значения атрибута, используя вызов метода getattr.

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

    Проектирование иерархии классов с множественным наследованием

    Создадим вначале два класса - Pa и Ma, представляющие родителей их потомка - класса Child.

    Вот код класса Pa:

    from CreatingStr import CreatingStr
    class Pa(CreatingStr):
        """один из родителей - отец"""
        #конструктор 
        #поля класса
        #fam: string - фамилия
        #sex: string - пол
        #height: string - рост
        #persistance: string - настойчивость 
        def __init__(self, fam, sex, height = 'высокий', 
                     persistance = 'настойчив в достижении цели'):
            self.fam = fam 
            self.sex = sex
            self.height = height
            self.persistance = persistance
        #Методы 
        def smart(self, task):
            return "оригинальное решение задачи: " + task
        def common(self):
            return 'Работает Pa'
    

    У класса Pa четыре поля, задающие свойства объектов этого класса. Два атрибута класса заданы позиционными параметрами конструктора класса, два - именованными параметрами, имеющими значение по умолчанию. Класс также определяет уникальный метод smart, которая в данном модельном проекте представляет строку текста. У класса есть также метод common. Метод с таким же именем имеется и у класса Ma, но реализации этого метода в обоих классах различны.

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

    Приведем теперь код класса Ma, схожий во многом с кодом класса Pa:

    from CreatingStr import CreatingStr
    class Ma(CreatingStr):
        """один из родителей - мать"""
        #конструктор 
        #поля класса
        #fam: string - фамилия
        #sex: string - пол
        #height: string - рост
        #goodness: string - доброта, отзывчивость 
        def __init__(self, fam, sex, height = 'средний', 
                     goodness = 'Готовность помогать'):
            self.fam = fam 
            self.sex = sex
            self.height = height
            self.goodness = goodness
        #Методы 
        def beauty(self, task):
            return "красивое решение задачи: " + task
        def common(self):
            return 'Работает Ma'
    

    У класса Ma, также как и у класса Pa, четыре поля и два метода. Этот класс также является потомком класса CreatingStr. Заметьте, первые три поля у обоих классов совпадают, но значение по умолчанию поля height - разные.

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

    Давайте, первым делом, приведем код класса Child, имеющего двух прямых родителей и общего предка:

    from CreatingStr import CreatingStr
    from Pa import Pa
    from Ma import Ma
    class Child(Pa, Ma):
        """потомок служебного класса и двух родителей """
        def __init__(self, fam, sex, age, 
           height = 'соответствует возрасту', 
           persistence = 'как у Ра', goodness = 'как у Ма' ):        
            Pa.__init__(self,fam, sex)
            Ma.__init__(self, fam, sex)
            self.age = age
        
        def skill(self):
            return("Умелые ручки!")
    

    Заметьте, первым делом, что в списке родителей нет класса CreatingStr. Его нельзя включать в этот список, поскольку он наследуется классами Pa и Ma, а, следовательно, и их потомком - классом Child. Попытка включить этот класс в список непосредственных родителей класса Child приведет к появлению ошибки.

    Класс Child помимо тех свойств и методов, которые он наследует от своих прямых родителей и предков, имеет собственное свойство - age и собственный метод - skill. Давайте на примере этого класса разберемся как устроено множественное наследование. Мы понимаем, что множественное наследование позволяет строить сложную иерархию классов. Граф, задающий структуру множественного наследования для нашего класса потомка, представляет собой сеть с одним источником - классом object и одним стоком - классом потомка.

    Список атрибутов и методов класса потомка. Рекурсивный алгоритм

    Рассмотрим, как формируются список атрибутов (свойств) и список методов, который потомок получает в результате наследования. Сформулируем рекурсивный алгоритм формирования этих списков - FormLists(P). Но прежде зададим два правила, которым подчиняется этот алгоритм:

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

    While(canUp(P)
    {
         Q = Up(P); FormLists(Q); 
    }
    AddLists(P);
    

    Булева функция canUp, определяющая возможность подъема для класса P возвращает значение false в двух случаях:

  • Класс P не имеет родителей. Такой класс единственный - это класс object - прародитель всех классов.
  • У класса P есть список родителей, но все родители из этого списка уже были просмотрены и их имена находятся в списке обработанных классов.
  • Функция Up(P) - функция подъема на следующий уровень наследования, возвращает в качестве результата класс Q - первый еще не просмотренный класс при проходе по списку родителей слева направо. Имя возвращаемого класса заносится в список обработанных классов. Для полученного класса Q рекурсивно вызывается метод FormLists.

    Цикл While заканчивает свою работу, когда будут обработаны все родительские классы, а, благодаря рекурсии, и все классы предков. Метод AddList выполняется для каждого класса уровня k после того, как будут обработаны все классы уровня k + 1 и выше. При выполнении этого метода каждый класс добавляет свою информацию в три глобальных списка:

  • List_at - список атрибутов, наследуемых потомком.
  • List_met - список методов, наследуемых потомком.
  • List_in - список встроенных атрибутов и методов, наследуемых потомком.
  • Здесь также выполняется правило, что метод или атрибут добавляется в глобальный список только тогда, когда его нет в списке, дублирование исключается.

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

    Рассмотрим, как будет выполняться метод FormLists(Child). Поскольку подъем возможен, то рекурсивно будут вызываться методы FormList(Pa) => FormList(CreateStr) => FormList(Object). У класса Object родителей нет, подъем будет завершен и будет вызван метод AddLists для этого класса. При выполнении этого метода все встроенные атрибуты и методы класса Object будут добавлены в список List_in. Фактически это означает, что класс Child наследует все эти свойства и методы. По завершении метода завершит работу и метод FormLists(Objects) и управление вернется в метод FormLists(CreateStr). Поскольку других путей подъема у этого класса нет, то для него будет вызван метод AddList, который переопределит встроенный метод __str__ . После чего управление возвратится к методу FormList(Pa). И у класса Pa есть единственный уже пройденный путь подъема, так что и для него будет вызван метод AddList. Класс Pa добавит свои четыре свойства в список List_at и два метода с их реализацией в список List_met. После чего управление вернется в метод FormList(Child). Здесь есть еще один путь подъема и будет вызван метод FormLists(Ma). У класса Ma нет путей подъема, ведущих к еще не посещаемых классах, поэтому для него сразу же будет вызван метод AddLists, который добавит в списки List_at, List_met уникальные свойства и методы класса Ma. После чего управление вновь вернется к методу FormListst(Child). Поскольку других путей подъема нет, то завершает работу метод AddLists(Child), который добавит свои собственные атрибуты и методы к унаследованным атрибутам свойих родителей и предков. Такова возможная реализация схемы множественного наследования в языке Python.

    Формирование конструктора класса потомка при множественном наследовании

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

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

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

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

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

         def __init__(self, fam, sex, age, 
           height = 'соответствует возрасту', 
           persistence = 'как у Ра', goodness = 'как у Ма' ):        
            Pa.__init__(self,fam, sex)
            Ma.__init__(self, fam, sex)
            self.age = age
    

    Заметьте, у классов Pa, Ma есть именованные параметры, но они не указаны при вызове конструкторов этих классов.

    Построю теперь тест в тестирующем классе Inheheritance, в котором создаются объекты классов Pa, Ma, Child:

    from CreatingStr import CreatingStr
    from Pa import Pa
    from Ma import Ma
    from Child import Child
    def test7():
        papa = Pa('Петров', 'муж')
        print(papa)
        mama = Ma('Петрова', 'жен')
        print(mama)
        childAnn = Child('Петрова', 'жен', 5)
        print (childAnn)
        s1 = childAnn.smart('поиск текста')
        s2 = childAnn.beauty('поиск текста')
        s3 = childAnn.common()
        s4 = childAnn.skill()
        print (s1); print(s2); print(s3); print(s4)
    

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

    Результаты нуждаются в пояснении.

    Прежде всего заметьте, все три класса не определяют собственный метод __str__, а наследуют его от класса CreateStr. Все прекрасно работает, - при печати объекта выводятся все его свойства в алфавитном порядке.

    С выводом позиционных параметров никаких проблем нет. Для именованных атрибутов используются значения по умолчанию. Если именованные параметры уникальны, то и здесь проблем нет. Но если есть общие параметры с разными значениями по умолчанию, то какое значение будет выбрано? В нашем примере первым будет вызван родитель класса Pa, и он полям по умолчанию даст свои значения. Но потом будет работать конструктор класса Ma, и он переопределит значения полей по умолчанию. При вызове конструкторов действует принцип "Последний- Победитель". Можно видеть по результатам теста, что для свойства height победителем является класс Ma, несмотря на то что значение по умолчанию задали все три класса, в том числе и сам потомок Child. Как быть, если потомок хочет, чтобы его значение становилось победителем в подобных ситуациях? Решение просто, - необходимо при вызове конструктора родителя включить соответствующий атрибут в список вызова, задав нужное значение по умолчанию. Но включить этот атрибут в вызов последнего родителя, обладающего этим атрибутом.

    Пусть в конструкторе класса Child вызов конструкторов родителя имеет вид:

     Pa.__init__(self,fam, sex, height = 'соответствует возрасту')
            Ma.__init__(self, fam, sex)  
    

    Победителем в этом случае по-прежнему будет класс Ма, работающий последним. Если же вызов конструкторов будет иметь вид:

                   Pa.__init__(self,fam, sex)
            Ma.__init__(self, fam, sex, height = 'соответствует возрасту')
    

    В этом случае победит потомок. Конструктор класса Ма задаст значение по умолчанию, определяемое потомком.

    Алмазное наследование

    Рассмотрим частный случай множественного наследования - алмазное (diamond) наследование. В этой схеме потомок наследует от двух родителей, которые оба являются наследниками одного и того же класса. Свое название эта схема получила из-за того, что граф наследования напоминает схему строения атома углерода (алмаза).

    У класса A два метода - f и q. Потомок B1 наследует метод q, переопределяет метод f и дает имя h родительскому методу f. Потомок B2 также наследует метод q, переопределяет метод f и дает имя g родительскому методу f. Что наследует класс C - потомок классов B1 и B2? От класса B1 он наследует все - метод f(x, y), метод q, унаследованный от класса A, метод h, под чьим именем скрывается метод f класса A. При наследовании от класса B2 методы с именами f и q будут проигнорированы, поскольку такие имена уже есть (действует принцип Первый - Победитель). Хотя под именем g скрывается уже унаследованный метод f, но у него другое, новое имя, поэтому он будет наследован классом C.

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

    Вернуться к учебному плану