Решил, что эта глава - хорошее введение-практикум по работе с классами... Кроме того, автор попытался все это представить в виде методики... прочитал с удовольствием, но это только первая часть...
Вот здесь можно посмотреть первоисточник книги М.Лутца "Изучаем Питон" (2011 pdf)
В этой главе мы создадим два класса:
• Person – класс, который представляет и обрабатывает информацию о людях
• Manager – адаптированная версия класса Person, модифицирующая унаследованное поведение
Попутно мы создадим экземпляры обоих классов и протестируем их возможности. По окончании я покажу вам отличный пример использования классов – мы сохраним наши экземпляры в хранилище, в объектно-ориентированной базе данных, обеспечивающей долговременной их хранение.
Благодаря этому вы сможете использовать программный код примера как шаблон для создания своей собственной, полноценной базы данных, целиком написанной на языке Python
Шаг 1: создание экземпляров¶
In []:
# Файл person.py (начало)
class Person:
В идеале так и должно быть – как мы уже знаем, модуль только выигрывает, когда он создан ради единственной, логически связной цели.
Конструкторы¶
Обычно первые значения атрибутам экземпляра присваиваются в методе конструктора init, который вызывается автоматически всякий раз, когда создается новый экземпляр. Давайте добавим этот конструктор к нашему классу:
In []:
# Добавим инициализацию полей записи
class Person:
def __init__(self, name, job, pay): # Конструктор принимает 3 аргумента
self.name = name # Заполняет поля при создании
self.job = job # self – новый экземпляр класса
self.pay = pay
Для сохранения информации могут использоваться и другие приемы (такие, как ссылки на объемлющую область видимости), но атрибуты экземпляра являются более очевидными и простыми для понимания.
В методе __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
Аргумент self, как обычно, будет заполняться интерпретатором автоматически, и в нем будет передаваться ссылка на экземпляр созданного объекта – присваивание значений атрибутам объекта self будет присоединять их к новому экземпляру
Тестирование в процессе разработки¶
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 и
# отличаются
In [4]:
#вместо именованных можно было бы использовать
sue = Person('Sue Jones', 'nurse', 9999)# позиционные аргументы
print(sue.name, sue.job,sue.pay)
Технически bob и sue являются пространствами имен – подобно всем экземплярам класса, каждый из них обладает собственной копией информации о состоянии.
Напомним, что весь код находится в файле # Файл person.py
Если запустить этот файл как сценарий, программный код в конце файла создаст два экземпляра нашего класса и выведет значения двух атрибутов для каждого из них (name и pay):
In []:
C:\misc> person.py
Bob Smith 0
Sue Jones 100000
Двоякое использование программного кода¶
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)
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
>>>
In [5]:
print(bob.name, bob.pay)
print sue.name, sue.pay
Шаг 2: добавление методов, определяющих поведение (стр. 733)¶
In [7]:
name = "Bob Smith"# Простая строка, за пределами класса
name.split()
Out[7]:
In [8]:
name.split()[-1] # Или [1], если имя всегда состоит из 2 компонентов
Out[8]:
Точно так же мы можем увеличить зарплату, изменив значение поля pay, – то есть, изменив информацию о состоянии с помощью присваивания. Данная операция также относится к базовым операциям в языке Pythonon, действие которой не зависит от того, является объект операции самостоятельным объектом или частью структуры класса:
In [9]:
pay = 100000# Простая переменная, за пределами класса
pay *= 1.10 # Поднять на 10%. Или: pay = pay * 1.10,
print(pay) # Или: pay = pay * 1.10, если вы любите вводить с клавиатуры
# Или: pay = pay + (pay * .10), если быть более точными!
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)
...такой подход нежелательно применять на практике. Выполнение операций за пределами класса, как в данном примере, может привести к проблемам при сопровождении.
Например, представьте, что в самых разных местах программы присутствуют одинаковые фрагменты, извлекающие фамилию. Если вам потребуется изменить их (например, в случае изменения структуры поля name), вам придется отыскать и изменить все такие фрагменты.
Например, представьте, что в самых разных местах программы присутствуют одинаковые фрагменты, извлекающие фамилию. Если вам потребуется изменить их (например, в случае изменения структуры поля 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)
Шаг 3: перегрузка операторов¶
Было бы совсем неплохо, если бы вывод экземпляра целиком предоставлял нам некоторую нужную информацию...
К сожалению, формат вывода объектов экземпляров, используемый по умолчанию, выглядит не очень удобочитаемо – он предусматривает
вывод имени класса объекта и его адреса в памяти (который в языке Python не имеет практической ценности, кроме того, что идентифицирует объект уникальным образом).
In [13]:
print(sue)
Реализация отображения¶
К счастью, большего успеха можно добиться, задействовав возможность перегрузки операторов, – добавив в класс метод, который перехватывает и выполняет встроенную операцию, когда она применяется к экземплярам класса.
В частности, мы могли бы реализовать метод перегрузки операторов, занимающий, пожалуй, второе место по частоте использования после метода init:
метод str, представленный в предыдущей главе. Метод str вызывается автоматически всякий раз, когда экземпляр преобразуется в строку для вывода.
Поскольку этот метод используется для вывода объекта, фактически все, что мы получаем при выводе объекта, является возвращаемым значением метода str этого объекта,, который может быть определен в классе объекта или унаследован от суперкласса (методы, имена которых начинаются и оканчиваются двумя символами подчеркивания, наследуются точно так же, как любые другие).
С технической точки зрения метод конструктора __init__, который мы уже реализовали, также является методом перегрузки операторов – он автоматически вызывается на этапе конструирования для инициализации вновь созданного экземпляра.
Конструкторы используются настолько часто, что они практически не выглядят, как нечто особенное. Более специализированные методы, такие как __str__, позволяют нам перехватывать определенные операции и предусматривать специфическую реализацию поведения объектов, участвующих в этих операциях.
Давайте добавим реализацию этого метода в наш класс. Ниже приводится расширенная версия класса, который выводит список атрибутов при отображении экземпляров целиком и не полагается на менее полезную реализацию вывода по умолчанию:
В частности, мы могли бы реализовать метод перегрузки операторов, занимающий, пожалуй, второе место по частоте использования после метода init:
метод str, представленный в предыдущей главе. Метод str вызывается автоматически всякий раз, когда экземпляр преобразуется в строку для вывода.
Поскольку этот метод используется для вывода объекта, фактически все, что мы получаем при выводе объекта, является возвращаемым значением метода 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)
In [16]:
print sue #В версии 2.6 без скобок
Шаг 4: адаптация поведения с помощью подклассов¶
Создание подклассов¶
...мы определим подкласс с именем Manager, наследующий класс Person, в котором мы заместим унаследованный метод giveRaise более узкоспециализированной версией.
Предположим, что из некоторых соображений менеджер (экземпляр класса Manager) получает не только прибавку, которая передается в виде процентов, как обычно, но еще и дополнительную премию, по умолчанию составляющую 10%.
– поскольку переопределенный метод giveRaise в дереве наследования оказывается ближе к экземплярам класса Manager, чем оригинальная реализация в классе Person, он фактически замещает и тем самым адаптирует операцию.
Напомню, что согласно правилам, поиск в дереве наследования оканчивается, как только будет найден первый метод с подходящим именем:
In []:
class Manager(Person): # Наследует атрибута класса Person
def giveRaise(self, percent, bonus=.10): # Переопределить для адаптации
Расширение методов: неправильный способ¶
In []:
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
self.pay = int(self.pay * (1 + percent + bonus)) # Неправильно: копирование
Расширение методов: правильный способ¶
In []:
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus) # Правильно: дополняет оригинал
In []:
instance.method(args...)
In []:
class.method(instance, args...)
где класс, содержащий вызываемый метод, определяется в соответствии с теми правилами поиска в дереве наследования, которые действуют и для методов.
В своих сценариях вы можете использовать любую форму вызова, но не забывайте о различиях между ними – при обращении непосредственно к классу вы должны передавать объект экземпляра вручную. Метод всегда должен получать объект экземпляра тем или иным способом, однако интерпретатор обеспечивает автоматическую его передачу только при вызове метода через обращение к экземпляру.
При вызове метода через обращение к классу вы сами должны передавать экземпляр в аргументе self – внутри метода, такого как giveRaise, аргумент self уже содержит подразумеваемый объект вызова, то есть сам экземпляр.
Вызов через обращение к классу фактически отменяет поиск в дереве наследования, начиная с экземпляра, и запускает поиск, начиная с определенного класса и выше по дереву классов. В нашем случае мы можем использовать этот прием для вызова метода giveRaise по умолчанию, находящегося в Person
В своих сценариях вы можете использовать любую форму вызова, но не забывайте о различиях между ними – при обращении непосредственно к классу вы должны передавать объект экземпляра вручную. Метод всегда должен получать объект экземпляра тем или иным способом, однако интерпретатор обеспечивает автоматическую его передачу только при вызове метода через обращение к экземпляру.
При вызове метода через обращение к классу вы сами должны передавать экземпляр в аргументе self – внутри метода, такого как giveRaise, аргумент self уже содержит подразумеваемый объект вызова, то есть сам экземпляр.
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__
Полиморфизм в действии¶
In [20]:
if __name__ == '__main__':
print('--All three--')
for object in (bob, sue, tom): # Обработка объектов обобщенным способом
object.giveRaise(.10) # Вызовет метод giveRaise этого объекта
print(object) # Вызовет общий метод __str__
Наследование, адаптация и расширение¶
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) # Унаследованный метод перегрузки
ООП: основная идея¶
Шаг 5: адаптация конструкторов¶
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)
Комментариев нет:
Отправить комментарий