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

Наследование и вложенность

Показывать лекцию целиком

Особенности классов Python

До сих пор я старался показать, что на Python можно работать с классами в стиле, привычном для работы с классами в таких языках как, например, C# или Java. В этой модели класс является декларацией - описанием некоторого множества объектов. Это описание включает описание свойств объектов и их поведения. Свойства объектов задаются полями класса, поведение - методами. Каждый класс имеет конструктор объектов. Если конструктор явно не задан, то в класс добавляется конструктор по умолчанию. Создать объект можно только вызвав конструктор. Все объекты класса в момент создания получают один и тот же набор свойств, значения свойств могут отличаться. Изменить описание класса и заданный набор свойств динамически невозможно.

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

Модель класса Pythonсущественно богаче. Вот основные отличия:

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

  • Атрибуты, принадлежащие классу. Они существуют в единственном экземпляре, не входят в состав атрибутов объекта, но доступны объектам как для чтения, так и для записи. Они могут играть роль счетчиков, фиксируя, например, число объектов, создаваемых конструктором класса, или играть роль доски объявлений, что будет продемонстрировано в нижеследующем примере. Эти атрибуты являются аналогом static полей для классов C#. Атрибуты этого типа могут быть заданы и инициализированы на этапе описания класса.
  • Атрибуты, принадлежащие всем объектам класса. Эти атрибуты создаются конструктором класса. Конструктор, если он задается для класса, имеет фиксированное имя __init__. В конструкторе можно задать произвольное число аргументов, которые становятся атрибутами, входящими в состав любого создаваемого объекта класса. В момент создания объекта вызывается конструктор класса, которому нужно передать значения атрибутов. При объявлении конструктора можно задать значения атрибутов по умолчанию. Если конструктор определен, то задание атрибутов, определяемых конструктором, обязательно. Однако Pythonне требует обязательного включения конструктора в описание класса, так что общих атрибутов у объектов может и не быть.
  • Атрибуты, принадлежащие объекту по его желанию. В классе, помимо конструктора могут быть определены set методы, устанавливающие атрибуты. Эти атрибуты включаются в состав объекта динамически в тот момент, когда объект вызывает соответствующий set метод.
  • Индивидуальные атрибуты, не определяемые описанием класса. Эти атрибуты создаются динамически объектом, характеризуя его индивидуальность.
  • О методах класса Pythonуже многое было сказано, в частности об особенности реализации полиморфизма. Сейчас на примере класса Person и его объектов рассмотрим задание атрибутов всех четырех типов. Вот описание класса Person:

    from CreatingStr import CreatingStr
    class Person(CreatingStr):
        """
        Класс с динамически создаваемыми атрибутами 4-х типов:
        Атрибут messageBoard - один для всех экземпляров. 
        Он может создаваться в момент объявления класса,    
        либо класс может создавать его динамически. 
        В данном контексте играет роль доски объявлений (аналог static поля C#)
        Атрибуты fam и age создаются конструктором класса.
        Эти атрибуты всегда присутствуют в каждом экземпляре класса
        и должны быть заданы при создании экземпляра.
        Атрибут hobby, создается методом класса setHobby.
        Атрибут появляется у экземпляра, если он вызывает метод setHobby.
        Не все объекты класса обязаны иметь это свойство.
        Индивидуальный атрибут, такой как, например, loveBook
        создается экземпляром класса.
        """
        messageBoard = "Это доска объявлений. Пишите!"
        def __init__(self, fam, age):
            self.fam = fam
            self.age = age
        def setHobby(self, hobby):
            self.hobby = hobby
        def writeMessage(self, mes, new = "new"):
            if new == "new":
                Person.messageBoard = mes
            else:
                #new = "добавляем сообщение на доску"
                Person.messageBoard = Person.messageBoard + "\r\n " + mes
        
    

    Класс Person является наследником инструментального класса CreatingStr, наследуемый метод str которого позволяет выдавать информацию об атрибутах, связанных с объектами класса.

    Атрибут messageBoard, заданный на уровне класса, является атрибутом класса. Он не задает свойство, значения которого индивидуальны для каждого из объектов класса. Значение этого свойства доступны всем объектам как для чтения, так и для записи. В данном примере это свойство играет роль доски объявлений.

    Конструктор класса init задает два атрибута - fam и age, обязательных для всех объектов.

    Метод setHobby позволяет задавать объектам возможный, но не обязательный атрибут Hobby.

    Объект класса может иметь и индивидуальный атрибут, не предусмотренный классом.

    В класс включен метод writeMessage, позволяющий записывать или дописывать сообщения объектов в доску объявлений - атрибут messageBoard.

    Давайте создадим несколько объектов класса Person, у которых наборы атрибутов будут отличаться:

    from Person import Person
    def test8():
        петр = Person("Петр Петров", 18)
        анна = Person("Анна Ананьева", 19)
        иван = Person("Иван Иванов", 20)
        анна.loveBook = "Мастер и Маргарита"
        петр.setHobby("Хоккей")
        company = [анна, иван, петр]
        for pers in company:
            print(pers)
        print("message: ", Person.messageBoard)
        анна.writeMessage("Анна: Приглашаю на встречу 23.01 в 15.00.")
        петр.writeMessage("Петр: Приду!", "add")
        иван.writeMessage("Иван: Буду!", "add")
        print("message: ", Person.messageBoard)
    

    При создании объектов: петр, анна, иван вызывается конструктор класса, позволяющий задать индивидуальные значения обязательных свойств объектов - fam и age.

    Объект петр динамически задает предусмотренное классом свойство, вызывая метод setHobby:

    петр.setHobby("Хоккей")

    Объект anna динамически создает индивидуальный атрибут, выполняя вызов:

    анна.loveBook = "Мастер и Маргарита"

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

    Давайте выполним этот тест.

    Вот результаты:

    Как видите, все три объекта имеют атрибуты age и fam, но состав атрибутов отличается. Объект "анна" создал индивидуальный атрибут, а "петр" - указал хобби. Используя общий атрибут, объекты смогли обменяться сообщениями.

    Два важнейших отношения между классами - наследование и вложенность

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

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

    Полезно понимать, что отношение наследования представляет отношение "is a" (является), а отношение вложенности - "has a" (имеет).

    Если есть два класса - "квадрат" и "прямоугольник", то они связаны отношением наследования. Класс "прямоугольник" - родительский класс, а "квадрат" - его потомок, поскольку каждый квадрат является прямоугольником.

    Если есть два класса - "автомобиль" и "двигатель", то они связаны отношением вложенности. Класс "автомобиль" - клиент, а "двигатель" - поставщик, поскольку каждый автомобиль имеет двигатель.

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

  • объекты поставщика могут быть локальными объектами, определенными в одном из методов класса клиента,
  • объекты поставщика могут появляться в качестве фактических параметров при вызове методов класса клиента,
  • эти объекты могут создаваться "на лету".
  • Стоит отметить еще одну важную особенность этого отношения. Класс может быть сам себе клиентом. Атрибутом класса может быть объект этого же класса. Такая структура типична для классов, описывающих динамические структуры данных - деревья, графы и другие подобные типы данных. Возможны ситуации, когда класс А является клиентом класса В, а класс В, в свою очередь является клиентом класса А, то есть классы одновременно являются клиентами и поставщиками друг для друга. Представьте два класса husband и wife, описывающих "мужа" и "жену". Каждый из них в качестве атрибута имеет объект другого класса и каждый из них использует сервисы другого класса. В реальном мире объектов отношение "клиент - поставщик" появляется гораздо чаще чем отношение наследования.

    Давайте перейдем к рассмотрению некоторого примера, где классы связаны как отношением наследования, так и отношением вложенности. Пусть у нас есть три класса: Person, описывающий людей, Car, описывающий автомобили, и QwnerOfCar, задающий множество людей, владеющих автомобилями.

    Напомним, что в Pythonклассы являются не только описаниями некоторого множества, но и сами могут рассматриваться как объекты. Но для нашего рассмотрения этот факт не имеет особого значения. Понятно, что люди и автомобили - это разные сущности, вообще говоря, не связанные друг с другом. Однако некоторые люди владеют автомобилями, и это множество людей можно задать отдельным классом QwnerOfCar. Объекты этого класса являются людьми и имеют автомобили. Поэтому класс QwnerOfCar является наследником класса Person и клиентом класса Car. Рассмотрим подробнее, как устроены эти три класса, построим модели этих сущностей.

    Класс Person

    Выше этот класс уже рассмотрен. Добавим в него еще пару специализированных set методов:

    def setIfStudent(self, status = "student", Institute = "TverTechnicalUniversity"):
            self.status = status
            self.Institute = Institute
    
        	def setIfEmployee(self, status = "employee", position = "engineer"):
            self.status = status
            self.position = position
    

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

    Класс Car

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

    Два метода - AddMiles и AddOwner позволяют изменять свойства объектов odometer и owners, добавляя в показания спидометра пройденные километры, в список владельцев - нового владельца.

    Вот описание класса:

    from CreatingStr import CreatingStr
    class Car(CreatingStr):
        """автомобиль"""
        
        def __init__(self, model = "Lada", release_year = 2018, price = 1000000,
            odometer = 0, current_owner = "shop", owners = ["shop"]):
            self.model = model
            self.release_year = release_year
            self.price = price
            self.odometer = odometer
            self.current_owner = current_owner
            self.owners = owners
    
        def Add_miles(self,distance):
            self.odometer +=distance
    
        def Add_owner(self, owner):
            self.owners.append(owner)
    

    Класс OwnerOfCar

    Этот класс, используя отношения наследования и вложенности, объединяет классы Person и Car, создавая единую систему взаимодействующих классов.

    Приведу описание этого класса:

    from Person import Person
    from Car import Car
    class OwnerOfCar(Person):
        """
        персона - владеет автомобилем - объектом car
        """    
        def __init__(self,  fam, age, car = None):        
            Person.__init__(fam, age)
            self.car = car
    
        def BuyCar(self, car, price):
            """ покупка автомобиля """
            self.car = car       
            car.price = price
            car.current_owner = self.fam
            car.Add_owner(self.fam)
           
        def SellCar(self):
            """ продажа автомобиля """
            self.car = None
    
        def setMyTrips(self, myTrips):
            """ список путешествий """
            self.myTrips = myTrips
            
        def NewTrip(self, route, distance):
            """ новое путешествие """
            self.myTrips.append(route)
            self.car.Add_miles(distance)
    

    Приведем некоторый сценарий, в котором участвуют две персоны, владеющие автомобилями, и их автомобили. Персоны покупают и продают автомобили, путешествуют на своих любимых авто. Вот соответствующий код сценария:

    def test9():
        from Car import Car
        car_Lada = Car(release_year = 2017, price = 800000, owners = ["shop"])
        car_Merc = Car("Mercedes", 2019, 1900000, owners = ["shop"])
    
        from OwnerOfCar import OwnerOfCar
        owner_manager = OwnerOfCar("Тригорин Алексей", 35)
        owner_manager.setIfEmployee(position = "major manager")
        owner_manager.BuyCar(car_Lada, 900000)
        owner_manager.setMyTrips([])
        owner_manager.NewTrip("Praga", 2500)
        owner_manager.SellCar()
        owner_manager.BuyCar(car_Merc, 1800000)
        owner_manager.NewTrip("Rome", 4500)
    
        owner_stud = OwnerOfCar("Миронов Андрей", 21)
        owner_stud.setIfStudent();
        owner_stud.BuyCar(car_Lada, 600000)
        owner_stud.setMyTrips([])
        owner_stud.NewTrip("Moscow", 500)
    
        print('Тригорин: ', owner_manager)
        print('Миронов: ',owner_stud)
        print('Lada: ', car_Lada)
        print('Merc: ',car_Merc)
    

    Вот результаты работы:

    Этот пример завершает тему проектирования иерархии классов, связанных отношениями наследования и вложенности.

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