питон. ООП_на_Python_Учебное пособие_var5a. Методическое пособие по дисциплине Введение в компьютерные технологии Москва Физический факультет мгу имени м в ломоносова 2022
Скачать 0.51 Mb.
|
12. Наследование Наследование является, пожалуй, самым главным понятием ООП. 22 Предположим, у нас есть класс (например, Class1). При помощи наследования мы можем создать новый класс (например, Class2), в котором будет реализован доступ ко всем атрибутам и методам класса Сlass1: class Class1 : # Базовый класс def funс1( self ): print( "Метод funс1() класса Class1" ) def func2( self ) : print( "Метод func2() класса Class1" ) class Class2 (Classl): # Класс Class2 наследует класс Classl def func3( self ): print( "Метод func3() класса Class2" ) c = Class2 () # Создаем экземпляр класса Class2 c.funс1() # Выведет: Метод funcl() класса Classl c.func2() # Выведет: Метод func2() класса Classl c.func3() # Выведет: Метод func3() класса Class2 Как видно из примера, класс Classl указывается внутри круглых скобок в определении класса Class2. Таким образом, класс Class2 наследует все атрибуты и методы класса Classl. Класс Classl называется базовым или суперклассом, а класс Class2 – производным или подклассом. Если имя метода в классе Class2 совпадает с именем метода класса Classl, то будет использоваться метод из класса Class2. Чтобы вызвать одноименный метод из базового класса, перед методом следует через точку написать название базового класса, а в первом параметре метода – явно указать ссылку на экземпляр класса. Рассмотрим это на примере class Class1 : # Базовый класс def __init__( self ): print( "Конструктор базового класса" ) def funс1( self ): print ( "Метод funс1() класса Class1" ) class Class2 ( Class1 ): # Класс Class2 наследует класс Classl def __init__( self ): print( "Конструктор производного класса" ) Class1 .__init__( self ) # Вызываем конструктор базового класса def funс1( self ): print ( "Метод func1() класса Class2" ) Class1 .funс1( self ) # Вызываем метод базового класса с = Class2 () # Создаем экземпляр класса Class2 с.funс1() # Вызываем метод funс1() Вывод: Конструктор производного класса Конструктор базового класса Метод func1() класса Class2 Метод func1() класса Classl Обратите внимание, что конструктор базового класса автоматически не вызывается, если он переопределен в производном классе. Поэтому его нужно вызывать явно либо так, как в приведенном примере, либо используя метод super(): super ().__init__() # Вызываем конструктор базового класса 23 или так: super (Class2, self).__init__() # Вызываем конструктор базового класса При использовании функции super() не нужно явно передавать указатель self в вызываемый метод. Кроме того, в первом параметре функции super() указывается производный класс, а не базовый. 13. Множественноенаследование В определении класса в круглых скобках можно указать сразу несколько базовых классов через запятую. Рассмотрим пример class Classl : # Базовый класс для класса Class2 def func1( self ): print ( "Метод funс1() класса Classl" ) class Class2 ( Classl ): # Класс Class2 наследует класс Classl def func2( self ): print( "Метод func2() класса Class2" ) class Class3 ( Classl ): # Класс Class3 наследует класс Classl def func1( self ): print( "Метод funс1() класса Class3" ) def func2( self ): print( "Метод func2() класса Class3" ) def func3( self ): print( "Метод func3() класса Class3" ) def func4( self ): print( "Метод func4() класса Class3" ) class Class4 ( Class2 , Class3 ): # Множественное наследование def func4( self ): print( "Метод func4() класса Class4" ) c = Class4 () # Создаем экземпляр класса Class4 c.func1() # Вывод: Метод func1() класса Class3 c.func2() # Вывод: Метод func2() класса Class2 c.func3() # Вывод: Метод func3() класса Class3 c.func4() # Вывод: Метод func4() класса Class4 Метод funс1() определен в двух классах: Class1 и Class3. Так как вначале просматриваются все базовые классы, непосредственно указанные в определении текущего класса, то метод funс1() будет найден в классе Class3 (поскольку он указан в числе базовых классов в определении Class4), а не в классе Classl. Метод func2() также определен в двух классах: Class2 и Class3. Так как класс Class2 стоит первым в списке базовых классов, то метод будет найден именно в нем. Чтобы наследовать метод из класса Class3, следует указать это явным образом: class Class4 (Class2, Class3): # Множественное наследование func2 = Class3.func2 # Наследуем func2() из класса Class3, а не из класса Class2 def func4( self ): print( "Метод func4() класса Class4" ) 14. Перегрузкаоператоров Перегрузка операторов позволяет экземплярам классов участвовать в обычных операциях. Чтобы перегрузить оператор, необходимо в классе определить метод со специальным названием. В результате, для выполнения действия соответствующего данной операции будет вызываться этот метод. 24 Перегрузка математических операторов производится с помощью следующих методов: Выражение Операция Метод x+y сложение x.__add__(у) y+x сложение (экземпляр класса справа) х .__radd__(у) x+=y сложение и присваивание х.__iadd__(у) x-y вычитание х.__sub__(у) y-x вычитание (экземпляр класса справа х.__rsub__(у) x-=y вычитание и присваивание х.__isub__(у) x*y умножение x.__mul__(у) y*x умножение (экземпляр класса справа): х.__rmul__(у) x*=y умножение и присваивание х .__imul__(у) x@y матричное умножение x.__matmul__(y) y@x матричное умножение (экземпляр класса справа) x.__rmatmul__(y) x@=y Матричное умножение и присваивание x.__imatmul__(y) x/y деление x.__truediv__(у y/x деление (экземпляр класса справа):); х.__rtruediv__(у x/=y деление и присваивание x.__itruediv__(у) x//y деление с округлением вниз x.__floordiv__(у) y/=x деление с округлением вниз (экз. класса справа): х.__rfloordiv __(у) x/=y деление с округлением вниз и присваивание x.__ifloordiv __(у) x%y остаток от деления х.__mod__(у) y%x остаток от деления (экземпляр класса справа): х.__rmod__(у) x%=y остаток от деления и присваивание х.__imod__(у) x**y возведение в степень x.__pow__(у) y**x возведение в степень (экземпляр класса справа): х.__rpow__(у) x**=y возведение в степень и присваивание х.__ipow__(у) -x унарный минус х.__neg__() +x унарный плюс х.__pos__() abs(x) абсолютное значение х.__abs__(). Пример перегрузки математических операторов: class MyClass : def __init__( self , у ): self .x = у def __add__( self , y ): print( "Экземпляр слева" ) return self .x + y def __radd__( self , y ): print( "Экземпляр справа" ) return self .x + y def __iadd__( self , y ): print( "Сложение с присваиванием" ) self .x += y return self c = MyClass (50) print( c + 10 ) # Вывод: Экземпляр слева 60 print( 20 + c ) # Вывод: Экземпляр справа 70 c += 30 # Вывод: Сложение с присваиванием print( c.x ) # Вывод: 80 Перегрузка операторов сравнения производится с помощью следующих методов: 25 Выражение Операция Метод x==y равно x.__eq__(у) x!=y не равно х .__ne__(у) x Пример перегрузки операторов сравнения: class MyClass : def __init__( self ): self .x = 50 self .arr = [1, 2, 3, 4, 5] def __eq__( self , y ): # Перегрузка оператора == return self .x == y def __contains__( self , у ) : # Перегрузка оператора in return у in self .arr c = MyClass () print( "Равно" if c == 50 else "He равно" ) # Вывод: Равно print( "Равно" if c == 51 else "He равно" ) # Вывод: He равно print( "Есть" if 5 in c else "Нет" ) # Вывод: Есть Возможность перегрузки операторов обеспечивает схожесть пользовательского класса со встроенными классами Python. Ведь все встроенные типы данных Питона – это классы. В результате все объекты могут иметь одинаковые интерфейсы. Так если ваш класс предполагает обращение к элементу объекта по индексу, например a[0], то это нужно обеспечить. Пусть будет класс-агрегат B, содержащий в списке объекты класса A: class A : def __init__( self , arg ): self .arg = arg def __str__( self ): return str ( self .arg) class B : def __init__( self , * args ): self .aList = [] for i in args : self .aList.append( A (i)) group = B (5, 10, 'abc' ) Чтобы получить элемент списка, несомненно, мы можем обратиться по индексу к полю aList: print(group.aList[1]) Однако куда интереснее извлекать элемент по индексу из самого объекта, а не из его поля: 26 class B : def __init__( self , * args ): self .aList = [] for i in args : self .aList.append(A(i)) def __getitem__( self , i ): return self .aList[ i ] group = B (5, 10, 'abc' ) print(group.aList[1]) # вывод: 10 print(group[0]) # 5 print(group[2]) # abc Это делает объекты класса B похожими на объекты встроенных в Python классов- последовательностей (списков, строк, кортежей). Здесь метод __getitem__() перегружает операцию извлечения элемента по индексу. Другими словами, этот метод вызывается, когда к объекту применяется операция извлечения элемента: объект[индекс]. Бывает необходимо, чтобы объект вел себя как функция. Это значит, если у нас есть объект a , то мы можем обращаться к нему в нотации функции, т. е. ставить после него круглые скобки и даже передавать в них аргументы: a = A() a() a(3, 4) Метод __call__() автоматически вызывается, когда к объекту обращаются как к функции. class Changeable: def __init__( self , color ): self .color = color def __call__( self , newcolor ): self .color = newcolor def __str__( self ): return "%s" % self .color canvas = Changeable( "green" ) frame = Changeable( "blue" ) canvas( "red" ) frame( "yellow" ) print(canvas, frame) В этом примере с помощью конструктора класса при создании объектов устанавливается их цвет. Если требуется его поменять, то достаточно обратиться к объекту как к функции и в качестве аргумента передать новый цвет. Такой обращение автоматически вызовет метод __call__(), который, в данном случае, изменит атрибут color объекта. 15. Абстрактныеметоды Абстрактные методы содержат только определение метода без реализации. Предполагается, что производный класс должен переопределить метод и реализовать его функциональность. Чтобы такое предположение сделать более очевидным, часто внутри абстрактного метода возбуждают исключение: 27 class Class1 : def __init__( self , val ): self .x= val def func( self ): # Абстрактный метод # Возбуждаем исключение raise NotImplementedError ( "Нельзя вызывать абстрактный метод" ) class Class2 ( Class1 ): # Наследуем абстрактный метод def func( self ): # Переопределяем метод print( self .x) с2 = Class2 (10) с2.func() # Вывод: 10 c1 = Class1 (20) try : # Перехватываем исключения c1.func() # Ошибка. Метод func() не переопределен except NotImplementedError as msg: print(msg) # Вывод: Нельзя вызывать абстрактный метод Модуль стандартной библиотеки abc (аббревиатура от Abstract Base Class) предоставляет дополнительные возможности. Наследуя от класса “abc.ABC”, вы явно указываете, что объявляемый класс — абстрактный базовый класс, т.е. создание его экземпляров не предполагается. Такой класс лишь задаёт интерфейс, который должен быть реализован в производных классах. Если хотя бы один из специальным образом помеченных методов абстрактного базового класса не переопределен, то Python бросит ошибку TypeError при попытке создания экземпляра этого класса. Такое поведение позволит отловить ошибку на более раннем этапе, если программист забыл про какой-то из абстрактных методов. import abc class Class1 ( abc ABC ): def __init__( self , val ): self .x = val @ abc .abstractmethod # Абстрактный метод def func( self ): raise NotImplementedError ( "Нельзя вызывать абстрактный метод" ) class Class2 ( Class1 ): # Наследуем абстрактный метод def another_func( self ): # Определяем другой метод print(- self .x) class Class3 ( Class2 ): # Наследуем два метода def func( self ): # Переопределяем абстрактный метод метод print( self .x) try : # Перехватываем исключения c = Class1 (10) # Ошибка. Метод func() не переопределен except TypeError as msg: print(msg) # вывод: Can't instantiate abstract class Class1 with abstract … try : # Перехватываем исключения c = Class2 (10) # Ошибка. Метод func() не переопределен except TypeError as msg: print(msg) # вывод: Can't instantiate abstract class Class1 with abstract … c = Class3 (30) c.func() # вывод: 30 c.another_func() # вывод: -30 В рассмотренном примере, экземпляры классов Class1 и Class2 невозможно создать, т.к. у обоих из них есть метод func, помеченный абстрактным с помощью декоратора @abc.abstractmethod. Класс Class3 переопределяет этот метод, что позволяет создать его экземпляры. 28 16. Ограничениедоступакатрибутамкласса В Python нет истинно закрытых атрибутов. Какие бы усилия вы не приложили, пользователь всегда сможет получить доступ к любому атрибуту вашего класса. По этой причине в Python обычно отдают предпочтение более простому открытому коду, нежели чем усложненному, который пытается скрыть детали имплементации. Вместо этого программисты на python стараются следовать общепринятого соглашения, что все идентификаторы, имя которых не начинается с символов нижнего подчеркивания, считаются публичными. Пользователи класса смело могут напрямую обращаться к таким идентификаторам, а разработчики берут на себя ответственность сохранять наличие и роль таких идентификаторов в ближайших обновлениях. Совокупность таких идентификаторов часто называют интерфейсом класса. Следует обратить внимание, что идентификаторы, имена которых начинаются с двух символов нижнего подчеркивания, не видны напрямую: class MyClass : def __init__( self , x ): self .__x = x self .y = x ** 2 a = MyClass (2) print(a.y) # вывод 4 print(a.__x) # ошибка, объект не имеет атрибута __x Тем не менее, к ним тоже можно обратиться извне добавив имя класса с предшествующим символом подчеркивания: print(a._MyClass__x) # вывод: 2 17. Полиморфизм В качестве примера предположим, что нужно реализовать приложение с базой данных сотрудников. Имеет смысл начать с создания универсального суперкласса, в котором определены стандартные линии поведения, общие для всех типов сотрудников в организации. class Employee : # Универсальный суперкласс сотрудников def computeSalary ( self ) : . . . # Cтандартный расчет зарплаты После написания кода общего поведения можно специализировать его для каждого индивидуального типа сотрудника, отражая его отличия от нормы. То есть можно создавать подклассы, настраивающие только те фрагменты поведения, которые отличаются в зависимости от типа сотрудника; остальное поведение будет унаследовано от более универсального класса. Скажем, если с инженерами связано уникальное правило подсчета заработной платы, то можно заменить в подклассе только один метод: class Engineer (Employee ) : # Специализированный подкласс инженеров def computeSalary( self ) : ... # Специальный метод расчета зарплаты Из-за того, что версия computeSalary находится ниже в дереве классов, она заместит (переопределит) универсальную версию в Employee. Теперь можно создавать экземпляры разновидностей классов сотрудников, к которым принадлежат реальные сотрудники, чтобы получить корректное поведение: bob = Employee() # Стандартное поведение sue = Employee() # Стандартное поведение tom = Engineer() # Специальный расчет заработной платы Обратите внимание, что можно создавать экземпляры любого класса в дереве (за исключением абстрактных), а не только классов в нижней части — класс, из которого вы создаете экземпляр, определяет уровень, откуда будет начинаться поиск атрибутов, и соответственно то, какие версии методов он будет задействовать. В конце концов, эти объекты могут оказаться встроенными в более крупный контейнерный объект (например, 29 список или экземпляр другого класса), который представляет отдел или компанию. Когда нужно будет запросить заработные платы сотрудников, они будут рассчитываться в соответствии с классами, из которых создавались объекты, благодаря принципам поиска в иерархии наследования: company = [bob, sue, tom] # Список сотрудников for emp in company: print( emp.computeSalary() ) # Метод computeSalary() из соответствующего класса 18. Композиция Еще одной особенностью объектно-ориентированного программирования является возможность реализовывать так называемый композиционный подход. Заключается он в том, что есть класс-контейнер, он же агрегатор, который включает в себя вызовы других классов. В результате получается, что при создании объекта класса-контейнера, также создаются объекты других классов. Чтобы понять, зачем нужна композиция в программировании, проведем аналогию с реальным миром. Большинство биологических и технических объектов состоят из более простых частей, также являющихся объектами. Например, животное состоит из различных органов (сердце, желудок), компьютер — из различного "железа" (процессор, память). Не следует путать композицию с наследованием, в том числе множественным. Наследование предполагает принадлежность к какой-то общности (похожесть), а композиция – формирование целого из частей. Наследуются атрибуты, т. е. возможности, другого класса, при этом, объектов непосредственно родительского класса не создается. При композиции же класс-агрегатор создает объекты других классов. Рассмотрим на примере реализацию композиции в Python. Пусть, требуется написать программу, которая вычисляет площадь обоев для оклеивания помещения. При этом окна, двери, пол и потолок оклеивать не надо. Прежде, чем писать программу, займемся объектно-ориентированным проектированием. То есть разберемся, что к чему. Комната – это прямоугольный параллелепипед, состоящий из шести прямоугольников. Его площадь представляет собой сумму площадей составляющих его прямоугольников. Площадь прямоугольника равна произведению его длины на ширину. По условию задачи обои клеятся только на стены, следовательно, площади верхнего и нижнего прямоугольников нам не нужны. Кроме того, надо будет вычесть общую площадь дверей и окон, поскольку они не оклеиваются. Можно выделить три типа объектов – окна, двери и комнаты. Получается три класса. Окна и двери являются частями комнаты, поэтому пусть они входят в состав объекта- помещения. Для данной задачи существенное значение имеют только два свойства – длина и ширина. Поэтому классы «окна» и «двери» можно объединить в один. Если бы были важны другие свойства (например, толщина стекла, материал двери), то следовало бы для окон создать один класс, а для дверей – другой. Пока обойдемся одним, и все что нам нужно от него – площадь объекта: class WinDoor : def __init__( self , x , y ): self .square = x * y Класс "комната" – это класс-контейнер для окон и дверей. Он должен содержать экземпляры класса WinDoor Хотя помещение не может быть совсем без окон и дверей, но может быть чуланом, дверь которого также оклеивается обоями. Поэтому имеет смысл в конструктор класса вынести только размеры самого помещения, без учета элементов "дизайна", а последние 30 добавлять вызовом специально предназначенного для этого метода, который будет добавлять объекты-компоненты в список. class Room : def __init__( self , x , y , z ): self .square = 2 * z * ( x + y ) self .wd = [] def add_wd( self , w , h ): self .wd.append(WinDoor( w , h )) def work_surface( self ): new_square = self .square for i in self .wd: new_square -= i.square return new_square #----------------------------------- r1 = Room (6, 3, 2.7) print(r1.square) # вывод: 48.6 r1.add_wd(1, 1) r1.add_wd(1, 1) r1.add_wd(1, 2) print(r1.work_surface()) #вывод: 44.6 |