Источники (рекомендую)

  • Быстро и на пальцах:

  • Медленно для начинающих:

    • Think Python by Downey
  • Подробно:

    • Лутц. Изучаем Python
    • Саммерфилд. Программирование на Python 3
  • Читать дальше патерны программирования:

    • Pyhon in Practice by Sammerfield
    • Гради Буч Объектно-ориентированный анализ и проектирование с примерами приложений

Наследование

Мы разобрали принципы А состоит из В, С, D (композиция).

Разберем наследование - изменение свойств класса под изменяющиеся требования задачи.

Создадим класс, в котором будем записывать для каждого человека его имя и зарплату. Это может быть студент в институте, может быть преподаватель.

Внимание: последующие цифры зарплат и алгоритмы рассчета не имеют ничего общего с реальными зарплатами и расценками за работу. Любое совпадение с реальными данными считайте случайным.

Как делаем классы:

  1. Создаем экземпляр класса
    • конструктор
    • тестируем
  2. Методы, которые определяют поведение
  3. Перегрузка операторов
  4. Особое поведение - наследуем класс
    • создаем класс-наследник;
    • расширяем методы
  5. Изменяем конструкторы
  6. Инструменты интроспекции (как получать информацию о классе во время отладки)
  7. Сохраняем объекты в базе данных.

Шаг 1. Создаем экземпляр класса

Пишем класс в файле person.py

Нужно описать класс и его конструктор.

Подумаем что нужно хранить о каждом работнике.

Его имя (полное), кем работет, его зарплату.

Человек может еще только приниматься на работу или его уже уволили. Тогда работы нет и зарплата 0. Запишем это в конструкторе.

class Person(object):
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay

Сразу начинаем тестировать класс: создаем экземпляры класса, печатаем значение их полей

class Person(object):
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay

# Конец класса Person

# Тестируем класс:
bob = Person('Boris Ivanov')
mike = Person('Mike Kuznetsov', job='student', pay=5000)

print(bob.name, bob.pay)        # Boris Ivanov 0
print(mike.name, mike.pay)      # Mike Kuznetsov 5000

bob и mike определяют каждое свое пространство имен. Т.е. поля name и pay в объекте bob не совпадают с полями name и pay в объекте mike, так как каждый экземпляр класса имеет свой набор атрибутов (name, job, pay).

Тестирование и выполнение

Мы написали тесты, но они всегда выполняются и при import этого модуля. Это не нужно.

Можно тесты положить в отдельные файлы (и это хорошо!). Можно написать тесты с использованием библиотек docstring, unittest, pytest и так далее.

Пока будем писать тесты в том же файле, но запускать их только тогда, когда мы запускаем непосредственно файл, а не импортируем его в другие файлы.

python person.py

Для этого будем проверять, как запускается файл, используя атрибут __name__ модуля:

class Person(object):
    def __init__(self, name, job=None, pay=0):
        self.name = name
        self.job = job
        self.pay = pay

# Конец класса Person

if __name__ == '__main__':
    # Тестируем класс, только если запускаем файл
    bob = Person('Boris Alexeevich Ivanov')
    mike = Person('Mikhail Vladimirovich Kuznetsov', job='student', pay=5000)

    print(bob.name, bob.pay)       # Boris Alexeevich Ivanov 0
    print(mike.name, mike.pay)     # Mikhail Vladimirovich Kuznetsov 5000

Шаг 2. Добавляем методы, которые определяют поведение

У человека можно узнать его фамилию (а не полное имя), фамилию можно печатать с инициалами (первые буквы имени и отчества - first name, parent name).

Сотрудник может работать не на целую ставку, а меньше (например, работать половину времени и получать 0.5 зарплаты от целой ставки).

Если мы будем прямо в коде везде писать

bob.name.split()[2]         # Ivanov
mike.pay = mike.pay * 0.5   # 0.5 ставки

то потом, когда нужно будет поправить этот код, он будет в разных местах программы и использоваться разными людьми.

Например, у нас полное имя может состоять из 2 слов или 4 и более слов. Тогда нужно будет фамилию извлекать как "последнее слово полного имени", а не "слово с индексом 2". Поэтому получение фамилии нужно сделать функцией.

О зарплате: назовем лучше размер 1 ставки base_pay, добавим атрибут part_time, а метод pay

class Person(object):
    def __init__(self, name, job=None, pay=0, part_time=1):
        self.name = name
        self.job = job
        self.base_pay = pay
        self.part_time = 1

    def last_name(self):
        return self.name.split()[2]

    def pay(self):
        return int(self.base_pay * self.part_time)

# Конец класса Person

if __name__ == '__main__':
    # Тестируем класс, только если запускаем файл
    bob = Person('Boris Alexeevich Ivanov')
    mike = Person('Mikhail Vladimirovich Kuznetsov', job='student', pay=5000, part_time=0.5)

    print(bob.name, bob.base_pay)       # Boris Alexeevich Ivanov 0
    print(mike.name, mike.base_pay)     # Mikhail Vladimirovich Kuznetsov 5000

    # добавили код - добавим тесты
    print(bob.last_name())          # Ivanov
    print(mike.last_name())         # Kuznetsov

    print(mike.pay())               # 2500

Если нужно будет изменить округление при подсчете зарплаты, то мы легко делаем это в одном месте - методе класса.

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

Неудобно при тестировании печатать каждый атрибут отдельно. Хочется легко печатать всю информацию об объекте. Но print(bob) печатает что-то вроде <__main__.Person object at 0x02614430>.

Чтобы напечатать информацию об объекте типа Person, нужно этот объект представить в виде строки, то есть вызвать str(bob) (вызывается автоматически). Эта функция автоматически вызывает a.__str__().

Внешняя информация о сотруднике - сколько он получает. Внутренняя - из чего складывается эта зарплата.

Чтобы наш объект удобно печатался надо переопределить функцию __str__. И изменим тесты, вызывая print(bob) и print(mike).

class Person(object):
    def __init__(self, name, job=None, pay=0, part_time=1):
        self.name = name
        self.job = job
        self.base_pay = pay
        self.part_time = part_time

    def __str__(self):
        return '[Person: {}, {}]'.format(self.name, self.pay())

    def last_name(self):
        return self.name.split()[2]

    def pay(self):
        return int(self.base_pay * self.part_time)

# Конец класса Person

class Teacher(Person):
    pass

if __name__ == '__main__':
    # Тестируем класс, только если запускаем файл
    bob = Person('Boris Alexeevich Ivanov')
    mike = Person('Mikhail Vladimirovich Kuznetsov', job='student', pay=5000, part_time=0.5)

    print(bob)                      # [Person: Boris Alexeevich Ivanov, 0]
    print(mike)                     # [Person: Mikhail Vladimirovich Kuznetsov, 2500]

    # добавили код - добавим тесты
    print(bob.last_name())          # Ivanov
    print(mike.last_name())         # Kuznetsov

Замечание об __str__ и __repr__: оба этих метода преобразуют объект к строке. Но __str__ обычно используют для представления данных в удобном для чтения пользователем виде (и именно его вызовет метод print), а __repr__ чаще пишут так, чтобы было удобно читать отладочную информацию или выполнять полученную строку как код.

Интерпретатор вызывает __repr__.

Если функции __str__ нет, то вызывается автоматически __repr__.

Шаг 4. Дополнительное поведение в подклассах

Часть сотрудников у нас проводит занятия. Занятия оплачиваются по часам. Допустим, что каждый 1 час стоит 200 рублей. Это расширенная возможность. Значит, расширим наш класс Person так, чтобы у преподавателей была возможность получать кроме базовой части зарплаты еще и почасовую оплату.

Чтобы расширить класс Person (а не добавлять каждому студенту возможность почасовой оплаты стипендии за каждое занятие), создадим новый класс Teacher на основе Person.

class Teacher(Person):
    тут опишем что добавили к базовому классу Person, чтобы получился Teacher

Как можно написать класс Teacher? Неправильно, но просто - скопировать нужный метод и изменить его.

НЕПРАВИЛЬНО (но будет работать):

class Teacher(Person):
    def __init__(self, name, job=None, pay=0, part_time=1, hours=0):
        self.name = name
        self.job = job
        self.base_pay = pay
        self.part_time = part_time
        self.hours = hours

   def pay(self):
        return int(self.base_pay * self.part_time + self.hours * 200)

Почему это неправильно? Потому что копируя код вы делаете сложным поддержку этого кода. Теперь если нужно бдует исправлять формулу подсчета self.base_pay * self.part_time, ее нужно будет исправить в 2 местах.

Как писать правильно? Нужно использовать уже написанный код.

Правильно:

class Teacher(Person):
    def __init__(self, name, pay=0, part_time=1, hours=0):
        super().__init__(name, 'teacher', pay, part_time)
        self.hours = hours

    def pay(self):
        return super().pay() + self.hours * 200

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

Разберем как можно вызывать методы базового класса из наследника.

Вызов

instance.method(args...)

автоматически заменяется на вызов

class.method(instance, args...)

self.pay() внутри метода pay() вызывать нельзя, получится рекурсия.

Можно вызвать непосредственно метод базового класса как Person.pay(self). Это отменит поиск в дереве наследования, так как мы сразу указываем класс. Плюс: быстрее. Минус: если потом будем писать класс между Person и Teacher, то придется проверять весь код класса Teacher и заменять вызов "метода родителя" на другого родителя. Мы заботимся о возможных изменениях.

super().method(args...)     # вызвать этот метод у базового класса

Напишем именно так.

Добавим тестов:

    tanya = Teacher(name='Tatyana Vladimirovna Ovsyannikova', job='teacher', pay=10000, hours=6*4)
    print(tanya.pay())              # вызов измененной версии pay класса Teacher
    print(tanya.last_name())        # вызов унаследованного метода класса Person
    print(tanya)                    # вызов унаследованного метода класса Person

Полиморфизм

Добавим еще тестов:

    for p in (bob, mike, tanya):
        print(p.pay())
        print(p)

Этот код выведет:

0
[Person: Boris Alexeevich Ivanov, 0]
2500
[Person: Mikhail Vladimirovich Kuznetsov, 2500]
14800
[Person: Tatyana Vladimirovna Ovsyannikova, 14800]

p может быть как объектом класса Person, так и объектом класса Teacher.

p.pay() - вызывается (в зависимости от того, какой реально тип у объекта p) метод либо класса Person, либо класса Teacher.

print(p) - вызвается метод p.__str__() класса Person (так как у Teacher этого метода нет). Обратите внимание, этот метод вызывает self.pay(). И вызывается в зависимости от того, какой это реальный объект, либо Person.pay(self), либо Teacher.pay(self).

Вызов метода того класса, к которому принадлежит реальный объект, называют полиморфизмом.

Что мы можем сделать для нового поведения?

class Person(object):
    def last_name(self): ...
    def pay(self): ...
    def __str__(self): ...

class Teacher(Person):      # наследование
    def pay(self): ...      # адаптация (изменение)
    def for_books(self):... # расширение (дополнительные методы)

dima = Teacher()            
dima.last_name()            # унаследованный метод
dima.pay()                  # адаптированная (измененная) версия
dima.for_books()            # дополнительный метод
print(dima)                 # унаследованный перегруженный метод

Шаг 5. Изменим конструкторы

Заметьте, что в классе Teacher не только pay() вызывает метод базового класса.

Нам понадобилось изменить конструктор. В него добавлось полe self.hours, был вызван конструктор базового класса и мы явно указали при его вызове, что job='teacher'

    def __init__(self, name, pay=0, part_time=1, hours=0):
        super().__init__(name, 'teacher', pay, part_time)
        self.hours = hours

Мы передаем конструктору суперкласса только необходимые аргументы.

Можно вообще не вызывать конструктор базового класса, а полностью его переписать в конструкторе наследника.

Альтернатива наследованию - композиция и getattr

Существует альтернативный шаблон проектирования - делегирование. Когда мы пишем обертку вокруг вложенного объекта, эта обертка управляет вложенным объектом и перенаправляет ему вызовы методов.

Можно вместо наследования Teacher от Person, сделать Person одним из атрибутов Teacher (полный текст примера смотри в файле person2.py примеров к разделу):

class Teacher(object):      # Teacher НЕ наследует от Person
    HOUR_RATE = 200
    def __init__(self, name, pay=0, part_time=1, hours=0):
        self.person = Person(name, 'teacher', pay, part_time)   # вложенный объект
        self.hours = hours

    def pay(self):                  
        # перехватывает обращение и делегирует его к другим методам
        return self.person.pay() + self.hours * self.HOUR_RATE

    def __getattr__(self, attr):
        # делегирует обращения ко всем остальным атрибутам
        return getattr(self.person, attr)

    def __str__(self):
        # тоже требуется перегрузить
        return str(self.person)

if __name__ == '__main__':
    tanya = Teacher(name='Tatyana Vladimirovna Ovsyannikova', pay=10000, hours=6*4)
    print(tanya.pay())              # 14800
    print(tanya.last_name())        # Ovsyannikova
    print(tanya)                    # [Person: Tatyana Vladimirovna Ovsyannikova, 10000]

Заметьте, tanya.pay() посчитало правильную зарплату с часами, а str(tanya) показывает зарплату БЕЗ часов. 10000 вместо 14800.

Доступ к полям и методам разберем позже.

Без переопределения __str__ не вызывается из Person, хотя в Person определен метод __str__.

Лутц, стр 750: встроенные операции, например вывод и обращение к элементу по индексу, неявно вызывают методы перегрузки операторов, такие как __str__ и __getitem__.

В версии 3.0 встроенные операции, подобные этим, не используют менеджеры атрибутов для неявного получения ссылок на атрибуты: они не используют ни метод __getattr__ (вызывается при попытке обращения к неопределенным атрибутам), ни родственный ему метод __getattribute__ (вызывается при обращении к любым атрибутам). Именно по этой причине нам потребовалось переопределить метод __str__ в альтернативной реализации класса Teacher, чтобы обеспечить вызов метода встроенного объекта Person при запуске сценария под управлением Python 3.0.

Технически это обусловлено тем, что при работе со старыми классами интерпретатор пытается искать методы перегрузки операторов в экземплярах, а при работе с классами нового стиля - нет. Он вообще пропускает экземпляр и пытается отыскать требуемый метод в классе.

В версии 2.6 встроенные операции, при применении к экземплярам классических классов, выполняют поиск атрибутов обычным способом. Например, операция вывода пытается отыскать метод __str__ с помощью метода __getattr__. Однако в версии 3.0 классы нового стиля наследуют метод __str__ по умолчанию, что мешает работе метода __getattr__, а метод __getattribute__ вообще не перехватывает обращения к подобным именам.

Это проблема, но вполне преодолимая, – классы, опирающиеся на прием делегирования, в версии 3.0 в общем случае могут переопределять методы перегрузки операторов, чтобы делегировать вызовы вложенным объектам, либо вручную, либо с помощью других инструментов или суперклассов.

class Person:               # старый класс
class Person(object): ...   # новый класс

Шаг 6. Как показывать информацию об объектах

При печати print(tanya) плохо: [Person: Tatyana Vladimirovna Ovsyannikova, 14800]

  • Пишется 'Person', хотя фактически класс Teacher. (Хотя объект Teacher - это измененный Person, но не хотелось бы печатать имя реального класса.
  • Выводятся только те атрибуты, что мы руками указали в __str__. О поле hours ничего не печатается. Это значит, что придется делать много лишней работы, выписывая руками каждый добавленный атрибут.

Как избежать лишней работы? (Больше кода - больше шанс сделать ошибку).

Что уже есть?

  • instance.__class__ - из экземпляра instance ссылка на класс (класс - это тоже объект).
  • у класса есть атрибут __name__ - имя (как у модуля), хранит имя класса (у нас 'Person', 'Teacher')
  • у класса есть __bases__ - последовательность ссылок на базовые классы.
  • у объекта (экземпляр или сам класс) есть атрибут __dict__ - список всех полей и методов класса в виде пар ключ (название атрибута) и значение (ссылка на значение).
  • dir(obj) - включаем еще унаследованные атрибуты и методы (использование в коде: list(dir(bob)))

Посмотрим, как они работают:

>>> from person import Person
>>> bob = Person('Bob Smith')
>>> print(bob)                  # Вызов метод __str__ объекта bob
[Person: Bob Smith, 0]
>>> bob.__class__               # Выведет класс объекта bob и его имя
<class 'person.Person'>
>>> bob.__class__.__name__
'Person'
>>> list(bob.__dict__.keys())   # Атрибуты – это действительно ключи словаря
['base_pay', 'job', 'name']     # Функция list используется для получения
                                # полного списка в версии 3.0
>>> for key in bob.__dict__:
        print(key, '=>', bob.__dict__[key]) # Обращение по индексам
pay => 0
job => None
name => Bob Smith
>>> for key in bob.__dict__:
        print(key, '=>', getattr(bob, key)) # Аналогично выражению obj.attr,
                                            # где attr - переменная
pay => 0
job => None
name => Bob Smith

Для того, чтобы печатать правильное имя класса, используйте self.__class__.__name__

Чтобы распечатать все атрибуты, вызываем

def str_attrs(self):
    attr = []
    for k in sorted(self.__dict__):
        attr.append('{}={}'.format(k, getattr(self, k))
    return ' '.join(attr)

Шаг 7. Сохраним объекты в базе данных

Используйте для этого модули:

  • pickle - Преобразует произвольные объекты на языке Python в строку байтов и обратно.
  • dbm - Реализует сохранение строк в файлах, обеспечивающих возможность обращения по ключу.
  • shelve - Использует первые два модуля, позволяя сохранять объекты в файлах-хранилищах, обеспечивающих возможность обращения по ключу.

Лутц стр 757 и далее.

Права доступа и область видимости

Лутц, стр 773

results matching ""

    No results matching ""