Поиск по блогу

пятница, 10 января 2014 г.

"Более реалистичный пример" (код к главе 27 из книги М.Лутца...) Шаги 1-5 (часть первая)

Решил, что эта глава - хорошее введение-практикум по работе с классами... Кроме того, автор попытался все это представить в виде методики... прочитал с удовольствием, но это только первая часть...
Вот здесь можно посмотреть первоисточник книги М.Лутца "Изучаем Питон" (2011 pdf)
В этой главе мы создадим два класса: • Person – класс, который представляет и обрабатывает информацию о людях • Manager – адаптированная версия класса Person, модифицирующая унаследованное поведение Попутно мы создадим экземпляры обоих классов и протестируем их возможности. По окончании я покажу вам отличный пример использования классов – мы сохраним наши экземпляры в хранилище, в объектно-ориентированной базе данных, обеспечивающей долговременной их хранение. Благодаря этому вы сможете использовать программный код примера как шаблон для создания своей собственной, полноценной базы данных, целиком написанной на языке Python

Шаг 1: создание экземпляров

Наша первая задача – начать создание главного класса Person. В языке Python существует соглаше- Python существует соглаше- существует соглашение, согласно которому имена модулей начинаются со строчной буквы, а имена классов – с прописной. д. В соответствии с этими соглашениями мы назовем наш файл person.py, а классу дадим имя Person, как показано ниже:
In []:
# Файл person.py (начало)
class Person:
Мы можем запрограммировать в одном файле любое количество функций и классов, поэтому название person.py может потерять свой смысл, если позднее мы добавим в него дополнительные компоненты, никак не связанные с его начальным предназначением. Но пока мы будем полагать, что все, что находится в этом файле, так или иначе связано с классом Person.
В идеале так и должно быть – как мы уже знаем, модуль только выигрывает, когда он создан ради единственной, логически связной цели.
Конструкторы
Первое, что нам требуется сделать с классом Person, – это записать основные сведения о человеке, то есть заполнить поля записи. На языке Python они называются атрибутами объекта и обычно создаются с помощью операций присваивания значений атрибутам аргумента self в методах класса.
Обычно первые значения атрибутам экземпляра присваиваются в методе конструктора init, который вызывается автоматически всякий раз, когда создается новый экземпляр. Давайте добавим этот конструктор к нашему классу:
In []:
# Добавим инициализацию полей записи
 
class Person:
    def __init__(self, name, job, pay): # Конструктор принимает 3 аргумента
        self.name = name # Заполняет поля при создании
        self.job = job # self – новый экземпляр класса
        self.pay = pay
Это достаточно распространенный прием: мы передаем конструктору аргументы с данными, которые будут храниться экземпляром, и присваиваем их атрибутам аргумента self. В терминах ООП аргумент self представляет вновь созданный экземпляр, а аргументы name, job и pay превращаются в информацию  о состоянии – данные, сохраняемые в объекте для последующего использования. Аргумент job, например, – это локальная переменная в области видимости функции __init__, а self.job – это атрибут экземпляра, который является подразумеваемым контекстом вызова метода. Это две разные переменные, которые по совпадению имеют одно и то же имя. Присваивая значение локальной переменной job атрибуту self.job с помощью операции self.job=job, мы сохраняем его в экземпляре для последующего использования. Как обычно, место, где выполняется присваивание значения имени, определяет смысл этого имени.
Для сохранения информации могут использоваться и другие приемы (такие, как ссылки на объемлющую область видимости), но атрибуты экземпляра являются более очевидными и простыми для понимания.
В методе __init__ нет ничего необычного, кроме того, что он вызывается автоматически в момент создания экземпляра и первый его аргумент имеет специальное значение. Несмотря на непривычное название, это самая обычная функция, обладающая всеми особенностями функций, о которых мы уже знаем... Для демонстрации сделаем аргумент job необязательным – по умолчанию используем значение None, означающее, что данный человек (в настоящий момент) является безработным...
In []:
# Добавим значения по умолчанию для аргументов конструктора
 
class Person:
    def __init__(self, name, job=None, pay=0): # Конструктор принимает 3 аргумента
        self.name = name # Заполняет поля при создании
        self.job = job # self – новый экземпляр класса
        self.pay = pay
Этот программный код означает, что при создании экземпляров класса Person нам достаточно будет передавать только значение аргумента name, а аргументы job и pay теперь являются необязательными – они по умолчанию будут получать значения None и 0.
Аргумент self, как обычно, будет заполняться интерпретатором автоматически, и в нем будет передаваться ссылка на экземпляр созданного объекта – присваивание значений атрибутам объекта self будет присоединять их к новому экземпляру

Тестирование в процессе разработки

Программисты на языке Python используют интерактивный сеанс лишь для простых тестов, а для проведения более полного тестирования предпочитают добавлять программный код в конец файла, содержащего тестируемые объекты, например:
In [2]:
# Добавляем программный код для самопроверки
 
class Person:
    def __init__(self, name, job=None, pay=0): # Конструктор принимает 3 аргумента
        self.name = name # Заполняет поля при создании
        self.job = job # self – новый экземпляр класса
        self.pay = pay

        

 # автоматически
print(bob.name, bob.pay) # Извлечет атрибуты
print(sue.name, sue.pay) # Атрибуты в объектах sue и 
 # отличаются
('Bob Smith', 0)
('Sue Jones', 100000)

Отметьте также, что при создании объекта sue мы использовали именованные аргументы, – мы могли бы передать значения в виде позиционных аргументов, однако именованные аргументы позволят нам позднее вспомнить, какие данные передавались (кроме того, они позволяют нам указывать аргументы в любом порядке).
In [4]:
#вместо именованных можно было бы использовать
sue = Person('Sue Jones', 'nurse', 9999)# позиционные аргументы
print(sue.name, sue.job,sue.pay)
('Sue Jones', 'nurse', 9999)

Технически bob и sue являются пространствами имен – подобно всем экземплярам класса, каждый из них обладает собственной копией информации о состоянии.
Напомним, что весь код находится в файле # Файл person.py Если запустить этот файл как сценарий, программный код в конце файла создаст два экземпляра нашего класса и выведет значения двух атрибутов для каждого из них (name и pay):
In []:
C:\misc> person.py
Bob Smith 0
Sue Jones 100000

Двоякое использование программного кода

Но нам не нужны будут тесты после отладки...(– инструкции print на верхнем уровне будут выполняться и при запуске файла как сценария, и при импортировании его как модуля). Мы могли бы поместить программный код тестов в отдельный файл, однако гораздо удобнее, когда тесты находятся в одном файле с тестируемыми компонентами. Гораздо лучше оформить тесты так, чтобы они выполнялись, только когда файл запускается как сценарий для тестирования, а не при его импортировании. Как вы уже знаете из предыдущей части книги, для этой цели можно использовать проверку атрибута __name__ модуля. Соответствующие изменения в файле приводятся ниже
In []:
# Предусмотреть возможность импортировать файл и запускать его, как 
# самостоятельный сценарий для самотестирования
 
class Person:
    def __init__(self, name, job=None, pay=0): # Конструктор принимает 3 аргумента
        self.name = name # Заполняет поля при создании
        self.job = job # self – новый экземпляр класса
        self.pay = pay
        
        
if __name__ == __main__: # Только когда файл запускается для тестирования
    # реализация самотестирования
    bob = Person(Bob Smith)
    sue = Person(Sue Jones, job=dev, pay=100000)
    print(bob.name, bob.pay)
    print(sue.name, sue.pay)
...тестирование выполняется, когда файл запускается как самостоятельный сценарий, потому что атрибут __name__ модуля получает значение ‘__main__’, а при импортировании этого не происходит:
In []:
C:\misc> person.py
Bob Smith 0
Sue Jones 100000
 
c:\misc> python
Python 3.0.1 (r301:69561, Feb 13 2009, 20:04:18) ...
import person
>>>
Теперь при импортировании файла интерпретатор создаст новый класс, но не будет использовать его Я запускал примеры из этой главы под управлением Python 3.0 и использовал синтаксис вызова функции print в версии 3.0. Если вы пользуетесь Python 2.6, этот программный код тоже будет работать, но вы будете замечать круглые скобки вокруг некоторых строк, потому что дополнительные круглые скобки в инструкции print превращают множество выводимых элементов в кортеж: c:\misc> c:\python26\python person.py (‘Bob Smith’, 0) (‘Sue Jones’, 100000) Если такие отличия могут лишить вас сна, тогда просто удалите круглые скобки в вызовах инструкций print при опробовании примеров в версии 2.6
In [5]:
print(bob.name, bob.pay)
print sue.name, sue.pay
('Bob Smith', 0)
Sue Jones 9999

Шаг 2: добавление методов, определяющих поведение (стр. 733)

Хотя классы и добавляют дополнительный структурный уровень, тем не менее большую часть своей работы они выполняют за счет внедрения и обработки данных базовых типов, таких как списки и строки. Например, поле name в наших объектах является обычной строкой, поэтому мы в состоянии извлекать фамилии людей из наших объектов, разбивая значение атрибута по пробелам и используя операцию индексирования. Все это – операции над базовыми типами данных, которые работают независимо от того, входит ли объект операции в состав экземпляра класса или нет:
In [7]:
name = "Bob Smith"# Простая строка, за пределами класса
name.split() 
Out[7]:
['Bob', 'Smith']
In [8]:
name.split()[-1] # Или [1], если имя всегда состоит из 2 компонентов
Out[8]:
'Smith'
Точно так же мы можем увеличить зарплату, изменив значение поля pay, – то есть, изменив информацию о состоянии с помощью присваивания. Данная операция также относится к базовым операциям в языке Pythonon, действие которой не зависит от того, является объект операции самостоятельным объектом или частью структуры класса:
In [9]:
pay = 100000# Простая переменная, за пределами класса
pay *= 1.10 # Поднять на 10%. Или: pay = pay * 1.10,
print(pay)  # Или: pay = pay * 1.10, если вы любите вводить с клавиатуры
            # Или: pay = pay + (pay * .10), если быть более точными!
110000.0

Чтобы применить те же самые операции к объектам класса Person, созданным нашим сценарием, просто подставьте имена bob.name и sue.pay на место name и pay. Операции останутся теми же самыми, но в качестве объектов операций будут использоваться атрибуты нашего класса:
In [11]:
# Обработка встроенных типов: строки, изменяемость
 
class Person:
  def __init__(self, name, job=None, pay=0):
    self.name = name
    self.job = job
    self.pay = pay
 
if __name__ == '__main__':
 bob = Person('Bob Smith')
 sue = Person('Sue Jones', job='dev', pay=100000)
 print(bob.name, bob.pay)
 print(sue.name, sue.pay)
 print(bob.name.split()[-1]) # Извлечь фамилию
 sue.pay *= 1.10 # Повысить зарплату
 print(sue.pay)
('Bob Smith', 0)
('Sue Jones', 100000)
Smith
110000.0

Здесь мы добавили в конец три новые строки – они извлекают фамилию из объекта bob, используя простые операции над строками и списками, и поднимают зарплату sue, изменяя значение атрибута pay с помощью простой числовой операции.
...такой подход нежелательно применять на практике. Выполнение операций за пределами класса, как в данном примере, может привести к проблемам при сопровождении.
Например, представьте, что в самых разных местах программы присутствуют одинаковые фрагменты, извлекающие фамилию. Если вам потребуется изменить их (например, в случае изменения структуры поля name), вам придется отыскать и изменить все такие фрагменты.

Методы реализации

Что нам действительно сейчас необходимо, так это реализовать концепцию проектирования, которая называется инкапсуляцией. Идея инкапсуляции заключается в том, чтобы спрятать логику операций за интерфейсами и тем самым добиться, чтобы каждая операция имела единственную реализацию в нашей программе.
В терминах языка Python это означает, что мы должны реализовать операции над объектами в виде методов класса, а не разбрасывать их по всей программе. В следующем листинге мы переместили реализацию двух операций из программы в методы класса, добившись инкапсуляции. Давайте попутно изменим программный код самопроверки внизу файла и заменим в нем жестко запрограммированные операции вызовами методов:
In [12]:
# Добавлены методы, инкапсулирующие операции, для удобства в сопровождении 
class Person:
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay
    def lastName(self): # Методы, реализующие поведение экземпляров
        return self.name.split()[-1] # self – подразумеваемый экземпляр
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent)) # Изменения придется вносить 
 # только в одном месте
if __name__ == '__main__':
    bob = Person('Bob Smith')
    sue = Person('Sue Jones', job='dev', pay=100000)
    print(bob.name, bob.pay)
    print(sue.name, sue.pay)
    print(bob.lastName(), sue.lastName()) # Вместо жестко определенных 
    sue.giveRaise(.10) # операций используются методы
    print(sue.pay)           
('Bob Smith', 0)
('Sue Jones', 100000)
('Smith', 'Jones')
110000

Как мы уже знаем, методы – это самые обычные функции, которые присоединяются к классам и предназначены для обработки экземпляров этих классов. Экземпляр – это подразумеваемый контекст вызова метода, который автоматически передается методу в виде аргумента self.

Шаг 3: перегрузка операторов

Однако тестирование все еще выполняется не так удобно, как могло бы, – для проверки объектов нам приходится вручную извлекать и выводить значения отдельных атрибутов (например, bob.name, sue.pay).
Было бы совсем неплохо, если бы вывод экземпляра целиком предоставлял нам некоторую нужную информацию...
К сожалению, формат вывода объектов экземпляров, используемый по умолчанию, выглядит не очень удобочитаемо – он предусматривает вывод имени класса объекта и его адреса в памяти (который в языке Python не имеет практической ценности, кроме того, что идентифицирует объект уникальным образом).
In [13]:
print(sue)
<__main__.Person instance at 0x000000000620CE48>

Реализация отображения

К счастью, большего успеха можно добиться, задействовав возможность перегрузки операторов, – добавив в класс метод, который перехватывает и выполняет встроенную операцию, когда она применяется к экземплярам класса.
В частности, мы могли бы реализовать метод перегрузки операторов, занимающий, пожалуй, второе место по частоте использования после метода init:
метод str, представленный в предыдущей главе. Метод str вызывается автоматически всякий раз, когда экземпляр преобразуется в строку для вывода.
Поскольку этот метод используется для вывода объекта, фактически все, что мы получаем при выводе объекта, является возвращаемым значением метода str этого объекта,, который может быть определен в классе объекта или унаследован от суперкласса (методы, имена которых начинаются и оканчиваются двумя символами подчеркивания, наследуются точно так же, как любые другие).
С технической точки зрения метод конструктора __init__, который мы уже реализовали, также является методом перегрузки операторов – он автоматически вызывается на этапе конструирования для инициализации вновь созданного экземпляра. Конструкторы используются настолько часто, что они практически не выглядят, как нечто особенное. Более специализированные методы, такие как __str__, позволяют нам перехватывать определенные операции и предусматривать специфическую реализацию поведения объектов, участвующих в этих операциях. Давайте добавим реализацию этого метода в наш класс. Ниже приводится расширенная версия класса, который выводит список атрибутов при отображении экземпляров целиком и не полагается на менее полезную реализацию вывода по умолчанию:
In [15]:
# Добавлены методы, инкапсулирующие операции, для удобства в сопровождении 
class Person:
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay
    def lastName(self): # Методы, реализующие поведение экземпляров
        return self.name.split()[-1] # self – подразумеваемый экземпляр
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent)) # Изменения придется вносить 
    def __str__(self): # Добавленный метод
        return '[Person: %s, %s]' % (self.name, self.pay) # Строка для вывода    
 # только в одном месте
if __name__ == '__main__':
    bob = Person('Bob Smith')
    sue = Person('Sue Jones', job='dev', pay=100000)
    print(bob)
    print(sue)
    print(bob.lastName(), sue.lastName())
    sue.giveRaise(.10)
    print(sue)
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
('Smith', 'Jones')
[Person: Sue Jones, 110000]

In [16]:
   print sue #В версии 2.6 без скобок
[Person: Sue Jones, 110000]

Обратите внимание, что здесь в методе __str__ для создания строки вывода мы применили оператор форматирования %, – для реализации необходимых действий классы могут использовать встроенные типы объектов и операции, как в данном случае. Мы также изменили программный код самопроверки – теперь он выводит не отдельные атрибуты объектов, а объекты целиком. Если теперь запустить этот сценарий (см. выше, уже получили) мы получим более понятные и осмысленные результаты – функции print автоматически будут вызывать наш новый метод __str__, возвращающий строки вида «[...]»: Несколько важных замечаний: как мы узнаем в следующей главе, родственный метод __repr__ перегрузки операторов возвращает представление объекта в виде программного кода. Иногда классы переопределяют оба метода: __str__ – для отображения объектов в удобочитаемом формате, для пользователя, и __repr__ – для вывода дополнительных сведений об объектах, которые могут представлять интерес для разработчика. Поскольку операция вывода автоматически вызывает метод __str__, а интерактивная оболочка выводит результаты с помощью метода __repr__, подходящие варианты вывода могут предоставляться обеим категориям потенциальных клиентов. Так как нас не интересует представление объектов в виде программного кода, нам вполне будет достаточно одного метода __str__.

Шаг 4: адаптация поведения с помощью подклассов

В настоящий момент в нашем классе задействовано большинство механизмов ООП, имеющихся в языке Python: класс создает экземпляры, обеспечивает особенности поведения с помощью методов и даже использует перегрузку операторов, перехватывая операции вывода с помощью метода __str__. Он фактически объединяет логику и данные в единый самостоятельный программный  компонент, упрощая поиск и модификацию программного кода, что может потребоваться в будущем. Возможность инкапсуляции также позволяет нам организовать программный код так, чтобы избежать избыточности и связан ных с ней проблем при сопровождении. Единственное основное понятие ООП, которое еще не было задействовано, – это адаптация программного кода за счет наследования. В некотором смысле мы уже использовали наследование – экземпляры наследуют методы своего класса. Однако для демонстрации истинной мощи ООП нам следует определить отношение типа суперкласс/подкласс, которое позволит нам расширить возможности нашего программного обеспечения и немного изменить унаследованное поведение.

Создание подклассов

...мы определим подкласс с именем Manager, наследующий класс Person, в котором мы заместим унаследованный метод giveRaise более узкоспециализированной версией.
Предположим, что из некоторых соображений менеджер (экземпляр класса Manager) получает не только прибавку, которая передается в виде процентов, как обычно, но еще и дополнительную премию, по умолчанию составляющую 10%. – поскольку переопределенный метод giveRaise в дереве наследования оказывается ближе к экземплярам класса Manager, чем оригинальная реализация в классе Person, он фактически замещает и тем самым адаптирует операцию. Напомню, что согласно правилам, поиск в дереве наследования оканчивается, как только будет найден первый метод с подходящим именем:
In []:
class Manager(Person): # Наследует атрибута класса Person
 def giveRaise(self, percent, bonus=.10): # Переопределить для адаптации

Расширение методов: неправильный способ

Далее, у нас имеется два способа адаптации программного кода в class Manager правильный и неправильный. Начнем с неправильного способа, потому что он проще для понимания/ Неправильный способ заключается в простом копировании реализации метода giveRaise из класса Person и его изменении в классе Manager, как показано ниже:
In []:
class Manager(Person):
 def giveRaise(self, percent, bonus=.10):
   self.pay = int(self.pay * (1 + percent + bonus)) # Неправильно:  копирование
Проблема здесь самая обычная: всякий раз, когда вы копируете программный код, вы фактически усложняете его сопровождение в будущем. Представьте себе: из-за того, что мы скопировали оригинальную версию, нам придется изменять программный код уже не в одном, а в двух местах... – всякий раз, когда у вас появляется соблазн скопировать программный код, вам наверняка стоит поискать более правильный подход.

Расширение методов: правильный способ

В действительности нам требуется лишь дополнить оригинальный метод giveRaise, а не заменить его полностью. Правильный способ состоит в том, чтобы вызвать оригинальную версию с измененными аргументами, как показано ниже:
In []:
class Manager(Person):
 def giveRaise(self, percent, bonus=.10):
 Person.giveRaise(self, percent + bonus) # Правильно: дополняет оригинал
Данная реализация учитывает то обстоятельство, что методы класса могут вызываться либо обращением к экземпляру (обычный способ, когда интерпретатор автоматически передает экземпляр в аргументе self), либо обращением к классу (менее распространенный способ, когда экземпляр передается вручную). Вспомним, что вызов метода:
In []:
instance.method(args...)
автоматически транслируется интерпретатором в эквивалентную форму:
In []:
class.method(instance, args...)
где класс, содержащий вызываемый метод, определяется в соответствии с теми правилами поиска в дереве наследования, которые действуют и для методов.
В своих сценариях вы можете использовать любую форму вызова, но не забывайте о различиях между ними – при обращении непосредственно к классу вы должны передавать объект экземпляра вручную. Метод всегда должен получать объект экземпляра тем или иным способом, однако интерпретатор обеспечивает автоматическую его передачу только при вызове метода через обращение к экземпляру.
При вызове метода через обращение к классу вы сами должны передавать экземпляр в аргументе self – внутри метода, такого как giveRaise, аргумент self уже содержит подразумеваемый объект вызова, то есть сам экземпляр.
Вызов через обращение к классу фактически отменяет поиск в дереве наследования, начиная с экземпляра, и запускает поиск, начиная с определенного класса и выше по дереву классов. В нашем случае мы можем использовать этот прием для вызова метода giveRaise по умолчанию, находящегося в Person
In [18]:
# Добавлен подкласс, адаптирующий поведение суперкласса
 
class Person:
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay
    def lastName(self):
        return self.name.split()[-1]
    def giveRaise(self, percent):
        self.pay = int(self.pay * (1 + percent))
    def __str__(self):
        return '[Person: %s, %s]' % (self.name, self.pay)
 
class Manager(Person):
    def giveRaise(self, percent, bonus=.10): # Переопределение метода
        Person.giveRaise(self, percent + bonus) # Вызов версии из 
 # класса Person
if __name__ == '__main__':
    bob = Person('Bob Smith')
    sue = Person('Sue Jones', job='dev', pay=100000)
    print(bob)
    print(sue)
    print(bob.lastName(), sue.lastName())
    sue.giveRaise(.10)
    print(sue)
    tom = Manager('Tom Jones', 'mgr', 50000) # Экземпляр Manager: __init__
    tom.giveRaise(.10) # Вызов адаптированной версии
    print(tom.lastName()) # Вызов унаследованного метода
    print(tom) # Вызов унаследованного __str__
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
('Smith', 'Jones')
[Person: Sue Jones, 110000]
Jones
[Person: Tom Jones, 60000]

Все выглядит совсем неплохо: результаты тестирования с участием объектов bob и sue выглядят, как и прежде, а когда для экземпляра tom класса Manager производится повышение зарплаты на 10%, действительное повышение составляет 20% (его зарплата увеличилась с $50K до $60K) Обратите также внимание, что при выводе информации об объекте tom используется форматирование, определенное в методе __str__ Person: экземпляры класса Manager наследуют его, а также методы lastName и __init__ от класса Person.

Полиморфизм в действии

Чтобы еще более полно задействовать механизм наследования, мы можем добавить в конец файла следующий программный код:
In [20]:
if __name__ == '__main__':
    print('--All three--')
    for object in (bob, sue, tom): # Обработка объектов обобщенным способом
        object.giveRaise(.10) # Вызовет метод giveRaise этого объекта
        print(object) # Вызовет общий метод __str__
--All three--
[Person: Bob Smith, 0]
[Person: Sue Jones, 121000]
[Person: Tom Jones, 72000]

В добавленном программном коде переменная object может ссылаться либо на экземпляр класса Person, либо на экземпляр класса Manager, а интерпретатор автоматически вызовет соответствующий метод giveRaise – для объектов bob и sue будет вызвана оригинальная версия метода из класса Person, а для объекта tom – адаптированная версия из класса Manager. Проследите сами, как интерпретатор выбирает нужную версию метода giveRaise для каждого объекта Проявление полиморфизма особенно очевидно, когда его можно наблюдать на примере выбора метода из классов, написанных нами. Так как выбор версии метода giveRaise основывается на типе объекта, в результате sue получает прибавку в 10%, а tom – прибавку в 20%. Класс Manager может не только адаптировать, но и использовать оригинальную реализацию в классе Person С другой стороны, операция вывода вызывает одну и ту же  версию метода __str__ для всех трех объектов, потому что в программном коде присутствует только одна его версия – в классе Person.

Наследование, адаптация и расширение

In []:
class Person:
    def lastName(self): ...
    def giveRaise(self): ...
    def __str__(self): ... 
class Manager(Person): # Наследование
    def giveRaise(self, ...): ... # Адаптация
    def someThingElse(self, ...): ... # Расширение 
tom = Manager()
tom.lastName() # Унаследованный метод
tom.giveRaise() # Адаптированная версия
tom.someThingElse() # Дополнительный метод
print(tom) # Унаследованный метод перегрузки
Дополнительные методы, такие как метод someThingElse в этом примере, расширяют возможности существующего программного обеспечения и доступны только для объектов класса Manager.

ООП: основная идея

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

Шаг 5: адаптация конструкторов

Наш программный код действует так, как он действует, но если вы внимательнее изучите текущую версию, вы можете заметить кое-что непонятное – кажется бессмысленным указывать значение ‘mgr’ (менеджер) в аргументе job (должность) при создании объекта класса Manager: эта должность уже подразумевается названием класса. Было бы лучше заполнять этот атрибут автоматически, при создании экземпляра класса Manager. Для этого мы можем проделать тот же трюк, что и в предыдущем разделе: мы можем адаптировать логику работы конструктора в классе Manager так, чтобы он автоматически подставлял название должности. С точки зрения реализации, нам необходимо переопределить метод __init__ в классе Manager, чтобы он подставлял строку ‘mgr’ автоматически. Как и при адаптации метода giveRaise, нам также необходимо вызывать оригинальный метод __init__ из класса Person за счет обращения к имени класса, чтобы инициализировать остальные атрибуты объекта. В новой версии сценария, которая приводится ниже, мы создали новый конструктор для класса Manager и изменили вызов, создающий объект tom, – теперь мы не передаем ему название должности ‘mgr’:
In [3]:
# Добавлен адаптированный конструктор в подкласс 
class Person:
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay
    def lastName(self):
        return self.name.split()[-1]
    def giveRaise(self, percent):self.pay = int(self.pay * (1 + percent))
    def __str__(self):
        return '[Person: %s, %s]' % (self.name, self.pay)
 
class Manager(Person):
     def __init__(self, name, pay):              # Переопределенный конструктор
         Person.__init__(self, name, 'mgr', pay) # Вызов оригинального 
                                                 # конструктора со значением ‘mgr’ в аргументе job
     def giveRaise(self, percent, bonus=.10):
         Person.giveRaise(self, percent + bonus)
 
if __name__ == '__main__':
     bob = Person('Bob Smith')
     sue = Person('Sue Jones', job='dev', pay=100000)
     print(bob)
     print(sue)
     print(bob.lastName(), sue.lastName())
     sue.giveRaise(.10)
     print(sue)
     tom = Manager('Tom Jones', 50000) # Указывать должность не требуется:
     tom.giveRaise(.10) # Подразумевается/устанавливается 
     print(tom.lastName()) # классом
     print(tom)
[Person: Bob Smith, 0]
[Person: Sue Jones, 100000]
('Smith', 'Jones')
[Person: Sue Jones, 110000]
Jones
[Person: Tom Jones, 60000]

Здесь мы снова использовали тот же прием расширения конструктора __init__, который выше использовался для расширения метода giveRaise, – вызвали версию метода из суперкласса обращением к имени класса и явно передали экземпляр self. Несмотря на странный вид имени конструктора, конечный эффект получается тот же самый. Так как нам необходимо задействовать логику конструктора класса Person (чтобы инициализировать атрибуты экземпляра), мы должны вызвать его именно так, как показано в примере, в противном случае экземпляры класса Manager окажутся без атрибутов Такая форма вызова конструктора суперкласса из конструктора подкласса ши-thon. Механизм наследования, реализованный в интерпретаторе, позволяет отыскать только один метод __init__ на этапе конструирования – самый нижний в дереве классов. Если во время конструирования объекта требуется вызвать метод __init__, расположенный выше (что обычно и делается), его необходимо вызывать вручную, обращением через имя суперкласса. Положительная сторона такого подхода заключается в том, что вы можете явно передать конструктору суперкласса только необходимые аргументы или вообще не вызывать его: возможность отказа от вызова конструктора суперкласса позволяет полностью заместить логику его работы, а не дополнять ее.

ООП проще, чем может показаться

В своем законченном виде, несмотря на незначительные размеры, наши классы задействовали практически все наиболее важные концепции механизма ООП в языке Python: • Создание экземпляров – заполнение атрибутов экземпляров. • Методы, реализующие поведение, – инкапсуляция логики в методах класса. • Перегрузка операторов – реализация поддержки встроенных операций, таких как вывод. • Адаптация поведения – переопределение специализированных версий методов в подклассах. • Адаптация конструкторов – добавление логики инициализации, в дополнение к логике суперкласса. Большая часть этих концепций основана на трех простых механизмах: поиске атрибутов в дереве наследования, специальном аргументе self методов и автоматическом выборе нужного метода перегрузки операторов.

Комментариев нет:

Отправить комментарий