ГЛАВА 14. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В PYTHON

14.1. Основы объектно-ориентированного подхода

Ранее мы говорили о том, что Python является полностью объектно-ориентированным языком программирования, но подробно не рассматривали, что это означает. Вернемся к этому вопросу, начнем с примера.

Предположим, что существует набор строковых переменных для описания адреса проживания некоторого человека:

addr_name = ‘Ivan Ivanov’ # имя человека
addr_line1 = ‘1122 Main Street’
addr_line2 = »
addr_city = ‘Panama City Beach’
addr_state = ‘FL’
addr_zip = ‘32407’ # индекс

Напишем функцию, которая выводит на экран всю информацию о человеке:

def printAddress(name, line1, line2, city, state, zip):
print(name)
if len(line1) > 0:
print(line1)
if len(line2) > 0:
print(line2)
print(city + «, » + state + » » + zip)

# Вызов функции, передача аргументов:

printAddress(addr_name, addr_line1, addr_line2, addr_city, addr_state, addr_zip)

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

Предположим, что изменились начальные условия и у человека в адресе появился второй индекс. Почему бы и нет? Создадим новую переменную:

# добавим переменную, содержащую индекс

addr_zip2 = «678900»

Изменим функцию printAddress с учетом новых сведений:

def printAddress(name, line1, line2, city, state, zip, zip2):

# добавили параметр zip2

print(name)
if len(line1) > 0:
print(line1)
if len(line2) > 0:
print(line2)
# добавили вывод на экран переменной zip2
print(city + «, » + state + » » + zip + zip2)

# Добавили новый аргумент addr_zip2:

printAddress(addr_name, addr_line1, addr_line2, addr_city, addr_state,
addr_zip, addr_zip2)

Пришлось несколько раз добавить новый индекс, чтобы функция printAddress корректно отработала при новых условиях. Какой недостаток у рассмотренного подхода? Огромное количество переменных! Чем больше сведений о человеке хотим обработать, тем больше переменных мы должны создать. Конечно, можно поместить всё в список (элементами списка тогда будут строки), но в Python есть более универсальный подход для работы с наборами разнородных данных, ориентированный на объекты.

Создадим структуру данных (класс) с именем Address, которая будет содержать все сведения об адресе человека:

class Address: # имя класса выбирает программист
name = «» # поля класса
line1 = «»
line2 = «»
city = «»
state = «»
zip = «»

Класс задает шаблон для хранения адреса. Превратить шаблон в конкретный адрес можно через создание объекта (экземпляра) класса Address:

homeAddress = Address()

Теперь можем заполнить поля объекта конкретными значениями:

# заполняем поле name объекта homeAddress:
homeAddress.name = «Ivan Ivanov»
homeAddress.line1 = «701 N. C Street»
homeAddress.line2 = «Carver Science Building»
homeAddress.city = «Indianola»
homeAddress.state = «IA»
homeAddress.zip = «50125»

Создадим еще один объект класса Address, который содержит информацию о загородном доме того же человека:

# переменная содержит адрес объекта класса Address:
vacationHomeAddress = Address()

Зададим поля объекта, адрес которого находится в переменной vacationHomeAddress:

vacationHomeAddress.name = «Ivan Ivanov»
vacationHomeAddress.line1 = «1122 Main Street»
vacationHomeAddress.line2 = «»
vacationHomeAddress.city = «Panama City Beach»
vacationHomeAddress.state = «FL»
vacationHomeAddress.zip = «32407»

Выведем на экран информацию о городе для основного и загородного адресов проживания (через указание имен объектов):

print(«Основной адрес проживания » + homeAddress.city)
print(«Адрес загородного дома » + vacationHomeAddress.city)

Изменим исходный текст функции printAddress() с учетом полученных знаний об объектах:

def printAddress(address): # передаем в функцию объект
print(address.name) # выводим на экран поле объекта
if len(address.line1) > 0:
print(address.line1)
if len(address.line2) > 0:
print(address.line2)
print(address.city + «, » + address.state + » » + address.zip)

Если объекты homeAddress и vacationHomeAddress ранее были созданы, то можем вывести информацию о них, передав в качестве аргумента функции printAddress:

printAddress(homeAddress)
printAddress(vacationHomeAddress)

В результате выполнения программы получим:

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

Создадим класс Dog:

class Dog:
age = 0 # возраст собаки
name = «» # имя собаки
weight = 0 # вес собаки
# Первым аргументом любого метода всегда является self, т.е. сам объект
def bark(self): # функция внутри класса называется методом
# self.name – обращение к имени текущего объекта-собаки
print(self.name, » говорит гав»)
# Создадим объект myDog класса Dog:
myDog = Dog()
# Присвоим значения полям объекта myDog:
myDog.name = «Spot» # Придумываем имя созданной собаке
myDog.weight = 20 # Указываем вес собаки
myDog.age = 1 # Возраст собаки
# Вызовем метод bark объекта myDog, т.е. попросим собаку подать голос:
myDog.bark()
# Полная форма для вызова метода myDog.bark() будет:
Dog.bark(myDog),
# т.е. полная форма требует в качестве первого аргумента сам объект – self

Результат работы программы:

Данный пример демонстрирует объектно-ориентированный подход в программировании, когда создаются объекты, приближенные к реальной жизни. Между объектами происходит взаимодействие посредством вызова методов. Поля объекта (переменные) фиксируют его состояние, а вызов метода приводит к реакции объекта и/или изменению его состояния (изменению переменных внутри объекта).

14.2. Наследование в Python

Объектно-ориентированный подход в программировании тесно связан с мышлением человека, с работой его памяти. Для того чтобы нам лучше понять свойства ООП, рассмотрим модель хранения и извлечения информации из памяти человека (модель предложена учеными Коллинзом и Квиллианом)44 . В своем эксперименте они использовали семантическую сеть, в которой были представлены знания о канарейке:

Например, «канарейка — это желтая птица, которая умеет петь», «птицы имеют перья и крылья, умеют летать» и т. п. Знания в этой сети представлены на различных уровнях: на нижнем уровне располагаются более частные знания, а на верхних — более общие. При таком подходе для понимания высказывания «Канарейка может летать» необходимо воспроизвести информацию о том, что канарейка относится к множеству птиц, и у птиц есть общее свойство «летать», которое распространяется (наследуется) и на канареек. Лабораторные эксперименты показали, что реакции людей на простые вопросы типа «Канарейка — это птица?», «Канарейка может летать?» или «Канарейка может петь?» различаются по времени. Ответ на вопрос «Может ли канарейка летать?» требует большего времени, чем на вопрос «Может ли канарейка петь». По мнению Коллинза и Квиллиана, это связано с тем, что информация запоминается человеком на наиболее абстрактном уровне. Вместо того чтобы запоминать все свойства каждой птицы, люди запоминают только отличительные особенности, например, желтый цвет и умение петь у канареек, а все остальные свойства переносятся на более абстрактные уровни: канарейка как птица умеет летать и покрыта перьями; птицы, будучи животными, дышат и питаются и т. д. Действительно, ответ на вопрос «Может ли канарейка дышать?» требует большего времени, т. к. человеку необходимо проследовать по иерархии понятий в своей памяти. С другой стороны, конкретные свойства могут перекрывать более общие, что также требует меньшего времени на обработку информации. Например, вопрос «Может ли страус летать» требует меньшего времени для ответа, чем вопросы «Имеет ли страус крылья?» или «Может ли страус дышать?».

Упомянутое выше свойство наследования нашло свое отражение в объектноориентированном программировании.

К примеру, необходимо создать программу, содержащую описание классов Работника (Employee) и Клиента (Customer). Эти классы имеют общие свойства, присущие всем людям, поэтому создадим базовый класс Человек (Person) и наследуем от него дочерние классы Employee и Customer:

Код, описывающий иерархию классов, представлен ниже:

class Person:
name = «» # имя у любого человека
class Employee(Person):
job_title = «» # наименование должности работника
class Customer(Person):
email = «» # почта клиента

Создадим объекты на основе классов и заполним их поля:

johnSmith = Person()
johnSmith.name = «John Smith»
janeEmployee = Employee()
janeEmployee.name = «Jane Employee» # поле наследуется от класса Person
janeEmployee.job_title = «Web Developer»
bobCustomer = Customer()
bobCustomer.name = «Bob Customer» # поле наследуется от класса Person
bobCustomer.email = «send_me@spam.com»

В объектах классов Employee и Customer появилось поле name, унаследованное от класса Person.

Помимо полей базового класса происходит наследование методов:

class Person:
name = «»
def init(self): # конструктор базового класса
print(«Создан человек»)
class Employee(Person):
job_title = «»
class Customer(Person):
email = «»
johnSmith = Person()
janeEmployee = Employee()
bobCustomer = Customer()

Результат работы программы:

Таким образом, при создании объектов вызывается конструктор, унаследованный от базового класса. Если дочерние классы содержат собственные методы, то выполняться будут они:

class Person:
name = «»
def init(self): # конструктор базового класса
print(«Создан человек»)
class Employee(Person):
job_title = «»
def init(self): # конструктор дочернего класса
print(«Создан работник»)
class Customer(Person):
email = «»
def init(self): # конструктор дочернего класса
print(«Создан покупатель»)
johnSmith = Person()
janeEmployee = Employee()
bobCustomer = Customer()

Результат работы программы:

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

class Person:
name = «»
def init(self):
print(«Создан человек»)
class Employee(Person):
job_title = «»
def init(self):
Person.init(self) # вызываем конструктор базового класса
print(«Создан работник»)
class Customer(Person):
email = «»
def init(self):
Person.init(self) # вызываем конструктор базового класса
print(«Создан покупатель»)
johnSmith = Person()
janeEmployee = Employee()
bobCustomer = Customer()

Результат работы программы:

14.3. Иерархия наследования в Python

В Python все создаваемые классы наследуются от класса object. Создадим класс (собственный тип данных) Point, в котором определим (переопределим методы базового класса object) специальные методы __init__, __eq__, __str__:

class Point:
def init(self, x=0, y=0): # конструктор устанавливает координаты
self.x = x
self.y = y
def eq(self, other): # метод для сравнения двух точек
return self.x == other.x and self.y == other.y
def str(self): # метод для строкового вывода информации
return «({0.x}, {0.y})».format(self)
def func(self): # понадобится в следующем примере
return abs(self.x — self.y)
a = Point() # создаем объект, по умолчанию x=0, y=0
print(str(a)) # здесь вызывается метод str класса Point
# полная форма Point.str(a)
b = Point (3, 4)
print(str(b))
b.x = -19
print(a.func())
print(str(b))
print(a == b, a != b) # вызывается метод eq
# полная форма для сравнения a == b имеет вид: Point.eq(a, b)

Результат работы программы:

Схематично иерархия классов имеет следующий вид:

Получается, что за всеми операциями над объектами стоят вызовы соответствующих методов. За каждой стандартной операцией над объектами закреплен собственный специальный метод (при сложение вызывается метод __add__ и т.д.).

Заметим, что мы не переопределяли специальный метод (__ne__) для неравенства a != b, но Python смог выполнить сравнение, т.к. принял его результат за обратный к равенству (вызов метода __eq__).

Наследуем от класса Point класс Circle:

import math
class Circle(Point):
def init(self, radius, x=0, y=0):
super().init(x, y) # вызов конструктора базового класса
self.radius = radius
def area(self): # площадь окружности
return math.pi * (self.radius ** 2)
def circumference(self): # длина окружности
return 2 * math.pi * self.radius
def eq(self, other): # сравнение двух окружностей
return self.radius == other.radius and super().eq(other)
def str(self): # вывод информации в виде строки
return «({0.radius}, {0.x}, {0.y})».format(self)
circle = Circle(2) # создаем объект, radius=2, x=0, y=0
circle.radius = 3
circle.x = 12
a = Circle(4, 5, 6)
b = Circle(4, 5, 6)
print(str(a)) # здесь вызывается специальный метод str()
print(str(b))
print(a == b) # здесь вызывается специальный метод eq()
#полная форма вызова метода для a == b: Circle. eq(a, b)
print(a == circle)
print(a != circle) # отрицание результата вызова метода eq()
# вызов метода базового класса из дочернего называется полиморфизмом:
print(circle.func())

Исходный код класса Circle:

Результат работы программы:

Таблицы специальных методов: