Сам_себе_программист._Как_научиться_программировать_и_устроиться. Guide to Programming Professionally
Скачать 3.94 Mb.
|
Глава 13. Четыре столпа объектно- ориентированного программирования Хороший дизайн приносит пользу быстрее, чем увеличивает расходы. Томас Гейл В объектно-ориентированном программировании есть четыре главные концеп- ции: инкапсуляция, абстракция, полиморфизм и наследование. Вместе они об- разовывают четыре столпа объектно-ориентированного программирования. Чтобы язык программирования действительно можно было назвать объектно- ориентированным, как Python, Java и Ruby , в нем должны присутствовать все че- тыре концепции. В этой главе вы узнаете о каждом из столпов объектно-ориен- тированного программирования. Инкапсуляция Инкапсуляция относится к двум концепциям. Первая заключается в том, что в объектно-ориентированном программировании переменные (состояние) и ме- тоды (для изменения состояния либо выполнения вычислений, использующих состояние) группируются в единое целое — объект. Python_ex245.py 1 class Rectangle(): 2 def __init__( self , w, l): 3 self .width = w 118 Часть II 4 self .len = l 5 def area( self ): 6 return self .width * self .len В этом случае переменные экземпляра класса len и width хранят состояние объекта. Состояние объекта сгруппировано в том же блоке (объекте), что и ме- тод area. Метод использует состояние объекта, чтобы вернуть площадь прямо- угольника. Вторая концепция, собственно инкапсуляция, заключается в сокрытии вну- тренних данных класса для предотвращения получения клиентом (кодом вне класса, который использует объект) прямого доступа к этим данным. Python_ex246.py 1 class Data: 2 def __init__( self ): 3 self .nums = [1, 2, 3, 4, 5] 4 def change_data( self , index, n): 5 self .nums[index] = n Класс Data имеет переменную nums экземпляра класса, содержащую список целых чисел. Как только вы создали объект Data, есть два способа изменить эле- менты в nums — при помощи метода change_data или напрямую получая доступ к переменной nums экземпляра с помощью объекта Data. Python_ex247.py 1 class Data: 2 def __init__( self ): 3 self .nums = [1, 2, 3, 4, 5] 4 def change_data( self , index, n): 5 self .nums[index] = n 6 data_one = Data() 7 data_one.nums[0] = 100 8 print(data_one.nums) 9 data_two = Data() 10 data_two.change_data(0, 100) 11 print(data_two.nums) >> [100, 2, 3, 4, 5] >> [100, 2, 3, 4, 5] 119 Введение в объектно-ориентированное программирование Оба способа изменять элементы в переменной экземпляра nums работают, но что же произойдет, если вы решите сделать переменную nums кортежем вме- сто списка? Если вы осуществите это изменение, любой код клиента, пытающий- ся изменить элементы в переменной nums, как с nums[0] = 100, больше не бу- дет работать, поскольку кортежи неизменяемы. Многие языки программирования решают эту проблему, разрешая програм- мистам определять закрытые переменные и закрытые методы — переменные и методы, к которым могут обращаться объекты в коде, реализующем различные методы, но клиент не может. Закрытые переменные и методы полезны, когда у вас есть метод или переменные, используемые внутри класса, но вы планиру- ете позже изменить реализацию своего кода (или желаете сохранить гибкость этой опции) и потому не хотите, чтобы тот, кто использует этот класс, полагал- ся на них (ведь они могут измениться и нарушить код клиента). Закрытые пере- менные являются примером второй концепции, к которой относится инкапсу- ляция; закрытые переменные скрывают внутренние данные класса от прямого доступа к ним клиента. В противоположность, открытые переменные — это те, к которым клиент может получить доступ. В Python нет закрытых переменных. Все его переменные — открытые. К ре- шению проблемы, с которой справляются закрытые переменные, Python под- ходит иным образом — используя конвенции (соглашения) имен. Если у вас есть переменная или метод, к которым вызывающий их не должен получить доступ, перед их именами необходимо добавить нижнее подчеркивание . Программисты Python знают, что если имя метода или переменной начинается с символа под- черкивания, их нельзя использовать (хотя они все еще могут попытаться сделать это на свой страх и риск). Python_ex248.py 1 class PublicPrivateExample: 2 def __init__( self ): 3 self .public = "B " 4 self ._unsafe = " " 5 def public_method( self ): 6 # B 7 pass 8 def _unsafe_method( self ): 9 # & B 10 pass 11 self.public = "B " 12 self._unsafe = " " Программисты, читающие этот код, понимают, что переменную self. public использовать безопасно, но они не должны использовать переменную self._unsafe , поскольку она начинается с подчеркивания, — а если все же решает ее использовать, то на свой страх и риск. Человек, занимающийся под- 120 Часть II держкой этого кода, не обязан содержать переменную self._unsafe, посколь- ку вызывающие ее не должны иметь к ней доступ. Программисты знают, что ме- тод public_method использовать безопасно, а метод _unsafe_method — нет, ведь имя последнего начинается с подчеркивания. Абстракция Абстракция — это процесс «отнятия или удаления у чего-то характеристик с целью сведения его к набору основных, существенных характеристик» 8 . В объ- ектно-ориентированном программировании абстракция используется, когда объекты моделируются с использованием классов, а ненужные подробности опус каются. Скажем, вы создаете модель человека. Человек комплексен — у него есть цвет волос, цвет глаз, рост, вес, этническая принадлежность, пол и многое другое. Если для представления человека вы создадите класс, некоторые из этих данных могут оказаться не важными в контексте проблемы, которую вы пытаетесь ре- шить. Примером абстракции может быть создание класса Person с опущением некоторых свойств человека, например, цвета глаз или веса. Объекты Person, которые создает ваш кла сс, являются абстракциями людей. Это представление человека, урезанное до основных характеристик, необходимых для решения конкретной проблемы. Полиморфизм Полиморфизмом называют «способность (в программировании) представлять один и тот же интерфейс для разных базовых форм (типов данных)» 9 . Интер- фейс — это функция или метод. Ниже приведен пример представления одного и того же интерфейса для разных типов данных. Python_ex249.py 1 print(", !") 2 print(200) 3 print(200.1) >> , ! >> 200 >> 200.1 Вы представили один интерфейс, функцию print, для трех разных типов данных: строки , целого числа и числа с плавающей точкой. Вам не нужно опре- делять и вызывать три отдельных функции (вроде print_string для вывода строк, print_int для вывода целых чисел и print_ oat для вывода чисел с плавающей точкой), чтобы вывести три разных типа данных; вместо этого вы использовали функцию print для представления одного интерфейса, чтобы вы- вести их все. 8 whatis.techtarget.com/de nition/abstraction 9 stackover ow.com/questions/1031273/what-is-polymorphism-what-is-it-forand-how-is-it-used 121 Введение в объектно-ориентированное программирование Встроенная функция type возвращает тип данных объекта. Python_ex250.py 1 print(", !") 2 print(200) 3 print(200.1) >> >> >> Скажем, вам нужно написать программу, создающую три объекта, кото- рые сами себя рисуют: треугольники, квадраты и круги. Этого можно достичь, определив три различных класса Triangle, Square и Circle, и метод draw для каждого из них. Triangle.draw() будет рисовать треугольник, Square. draw() — квадрат, Circle.draw() — круг. Таким образом, каждый объект име- ет интерфейс draw, знающий, как рисовать самого себя. Вы представили один и тот же интерфейс для трех различных типов данных. Если бы Python не поддерживал полиморфизм, вам понадобился бы свой метод для рисования каждой фигуры — к примеру, draw_triangle для рисова- ния объекта Triangle, draw_square — для рисования объекта Square и draw_ circle — для рисования объекта Circle. Кроме того, если бы у вас был список этих объектов, и вы хотели бы нарисо- вать каждый из них, вам бы пришлось узнавать тип каждого объекта, затем вы- зывать подходящий для этого типа метод, увеличивая программу, делая ее более сложной для чтения и написания, а также менее надежной. И это усложнило бы процесс совершенствования программы, поскольку всякий раз при добавлении новой фигуры вам пришлось бы отслеживать каждое место в коде, где вы рису- ете фигуры, проверять их тип данных (чтобы выяснить, какой метод использо- вать) и вызывать новую функцию рисования. Ниже приведен пример рисования фигур с полиморфизмом и без него. Python_ex251.py 1 # $ . 2 # A 3 # B B 4 shapes = [tr1, sq1, cr1] 5 for a_shape in shapes: 6 if type(a_shape) == "K ": 7 a_shape.draw_triangle() 8 if type(a_shape) == " ": 9 a_shape.draw_square() 10 if type(a_shape) == " ": 11 a_shape.draw_circle() 122 Часть II 12 # A 13 # XW B 14 shapes = [tr1, 15 sw1, 16 cr1] 17 for a_shape in shapes: 18 a_shape.draw() Если бы вы пожелали добавить новую фигуру в список shapes без полимор- физма, вам пришлось бы изменить код в цикле for, чтобы проверить тип a_ shape , и вызывать его метод рисования. С помощью единообразного полимор- фического интерфейса вы може те впредь добавлять столько классов фигур в список shapes, сколько пожелаете, и фигура будет рисовать себя без какого-ли- бо дополнительного кода. Наследование Наследование в программировании напоминает наследование в биологии. При генетическом наследовании вы наследуете характеристики вроде цвета глаз от родителей. Аналогично, когда вы создаете класс, он может наследовать методы и переменные от другого класса. Класс, от которого наследуют, называется ро- дительским, а класс, который наследует, — дочерним. В этом разделе вы будете моделировать фигуры, используя наследование. Вот класс, который моделирует фигуру. Python_ex252.py 1 class Shape(): 2 def __init__( self , w, l): 3 self .width = w 4 self .len = l 5 def print_size( self ): 6 print("""{} {} 7 """.format( self .width, 8 self .len)) 9 my_shape = Shape(20, 25) 10 my_shape.print_size() >> 20 на 25 При помощи этого класса вы можете создавать объекты фигур Shape с шири- ной width и длиной len. К тому же объекты Shape имеют метод print_size, выводящий их ширину и длину. 123 Введение в объектно-ориентированное программирование Вы можете определить дочерний класс, наследующий от родительского, передав имя родительского класса в качестве параметра дочернему классу при его создании. В следующем примере создается класс Square, наследующийся от класса Shape. Python_ex253.py 1 class Shape(): 2 def __init__( self , w, l): 3 self .width = w 4 self .len = l 5 def print_size( self ): 6 print("""{} {} 7 """.format( self .width, 8 self .len)) 9 class Square(Shape): 10 pass 11 a_square = Square(20,20) 12 a_square.print_size() >> 20 20 Поскольку вы передали класс Shape в качестве параметра в класс Square, класс Square наследует переменные и методы класса Shape. Тело , которое вы определили в классе Square, состояло лишь из ключевого слова pass, сообщаю- щего Python ничего не делать. Благодаря наследованию вы можете создавать объект Square, передавать ему ширину и длину и вызывать в нем метод print_size без необходимости писать еще какой-либо код (за исключением pass) в классе Square. Такое сокра- щение кода важно, поскольку избегание повторений кода уменьшает вашу про- грамму и делает ее лучше управляемой. Дочерний класс не отличается от любого другого — вы можете определять в нем методы и переменные, не затрагивая родительский класс. Python_ex254.py 1 class Shape(): 2 def __init__( self , w, l): 3 self .width = w 4 self .len = l 5 def print_size( self ): 6 print("""{} {} 7 """.format( self .width, 8 self .len)) 124 Часть II 9 class Square(Shape): 10 def area( self ): 11 return self .width * self .len 12 a_square = Square(20, 20) 13 print(a_square.area()) >> 400 Когда дочерний класс наследует метод от родительского класса, его можно переопределить, определив новый метод с таким же именем, как у метода, кото- рый был унаследован. Способность дочернего класса изменять реализацию ме- тода, унаследованного от его родительского класса, называется переопределе- нием метода . Python_ex255.py 1 class Shape(): 2 def __init__( self , w, l): 3 self .width = w 4 self .len = l 5 def print_size( self ): 6 print("""{} by {} 7 """.format( self .width, 8 self .len)) 9 class Square(Shape): 10 def area( self ): 11 return self .width * self .len 12 def print_size( self ): 13 print("""Q {} {} 14 """.format( self .width, 15 self .len)) 16 a_square = Square(20, 20) 17 a_square.print_size() >> Q 20 20 В этом случае, поскольку вы определили метод print_size, новый метод переопределяет родительский метод с таким же именем, и выводит новое со- общение при вызове. 125 Введение в объектно-ориентированное программирование Композиция Теперь, когда вы узнали о четырех столпах объектно-ориентированного про- граммирования, я раскрою смысл еще одной важной концепции — композиции . Композиция создает отношение «имеет», сохраняя объект в другом объекте как переменную. Например, композиция может использоваться для представления отношения между собакой и ее хозяином (собака имеет хозяина). Для этого сна- чала создайте классы, представляющие собак и людей. Python_ex256.py 1 class Dog(): 2 def __init__( self , 3 name, 4 breed, 5 owner): 6 self .name = name 7 self .breed = breed 8 self .owner = owner 9 class Person(): 10 def __init__( self , name): 11 self .name = name Затем, когда вы создаете объект Dog, то передаете объект Person в качестве параметра хозяина. Python_ex257.py 1 # & 2 # X 3 mick = Person("V & ") 4 stan = Dog("% ", 5 " ", 6 mick) 7 print(stan.owner.name) >> V & Теперь объект stan с именем "% " имеет хозяина — объект Person с именем "V & ", хранящийся в переменной экземпляра класса owner. 126 Часть II Словарь терминов Абстракция: процесс «отнятия или удаления у чего-то характеристик с целью сведения его к набору основных, существенных характеристик» 10 Дочерний класс: класс, который наследуется. Инкапсуляция: относится к двум концепциям. Первая заключается в том, что в объектно-ориентированном программировании переменные (состояние) и ме- тоды (для изменения состояния либо выполнения вычислений, использующих состояние) группируются в единое целое — объект. Вторая концепция, собствен- но инкапсуляция, заключается в сокрытии внутренних данных класса для пре- дотвращения получения клиентом прямого доступа к этим данным. Клиент: код вне класса, который использует объект. Композиция: композиция моделирует отношение «имеет», сохраняя объект в другом объекте как переменную. Наследование: при генетическом наследовании вы наследуете свойства вроде цвета глаз от родителей. Аналогично, когда вы создаете класс, он может наследо- вать методы и переменные от другого класса. Переопределение метода: способность дочернего класса изменять реализацию метода, унаследованного от его родительского класса. Полиморфизм: полиморфизмом называют «способность (в программирова- нии) представлять один и тот же интерфейс для разных базовых форм (типов данных)» 11 Родительский класс: класс, от которого осуществляется наследование. Четыре столпа объектно-ориентированного программирования: четыре глав- ные концепции в объектно-ориентированном программировании: наследова- ние, полиморфизм, абстракция и инкапсуляция. Практикум 1. Создайте классы Rectangle и Square с методом calculate_perimeter, вычисляющим периметр фигур, которые эти классы представляют. Создай- те объекты Rectangle и Square вызовите в них этот метод. 2. В классе Square определите метод change_size, позволяющий передавать ему число, которое увеличивает или уменьшает (если оно отрицательное) каждую сторону объекта Square на соответствующее значение. 3. Создайте класс Shape . Определите в нем метод what_am_i, который при вы- зове выводит строку "Q — .". Измените ваши классы Rectangle и Square из предыдущих заданий для наследования от Square, создайте объек- ты Square и Rectangle и вызовите в них новый метод. 4. Создайте классы Horse и Rider. Используйте композицию, чтобы смодели- ровать лошадь с всадником на ней. Решения: chall_1.py — chall_4.py. 10 whatis.techtarget.com/de nition/abstraction 11 stackover ow.com/questions/1031273/what-is-polymorphism-what-is-it-forand-how-is-it-used |