Занятие 7: объектно-ориентированное программирование в Python

5 (100%) 6 vote[s]

В этом занятии будет рассмотрено, что такое объектно-ориентированное программирование, его концепция и парадигмы, классы и объекты (инкапсуляция, наследование, полиморфизм), стандартный модуль datetime, обработка ошибок в python.

Объектно-ориентированное программирование: концепция

Буквально всё в python является объектами. Поэтому это полностью объектно-ориентированный язык программирования.

Стандартные типы данных являются классами и содержат готовые методы для обработки своих данных: изменение регистра строки, поиск подстрок, изменение порядка элементов в списке и др. Оба этих типа представляют собой последовательности и имеют общие черты поведения — это достигается за счет существования абстрактного класса «последовательность», от которого наследуются и строки, и списки. Объектно-ориентированное программирование позволяет при необходимости создавать собственные классы-типы, наследуя их от уже существующие и добавляя к ним новые функции. Такое построение программного продукта позволяет удобно структурировать и в дальнейшем расширять код. 

Классы — это по сути сложные типы данных, определенные программистом. Как и другие сложные типы, класс позволяет хранить в переменной элементы других типов. Поля класса — это переменные, объявленные внутри класса для хранения данных. Методы класса — это функции, объявленные для их обработки и взаимодействия с основной программой и другими данными.

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

Например, пусть у нас есть магазин с различными видами товаров. У каждого вида товаров есть название, цена и количество в наличии:

class ProductInStore(object):
    price = 9.99
    name = 'new product'
    qty_in_stock = 0
 
abn911 = ProductInStore()
abn911.name = 'Lego Mindstorm v1'
abn911.price = 199.99
abn911.qty_in_stock = 5
print abn911.name, abn911.price, abn911.qty_in_stock
 
abn912 = ProductInStore()
abn912.name = 'Anakin action figure 921'
abn912.price = 56.11
abn912.qty_in_stock = 11
print abn912.name, abn912.price, abn912.qty_in_stock

Классы объявляются с ключевым словом class, имя класса по стандартам python записывается следующим образом: все слова вместе, каждое с большой буквы. Для создания экземпляра класс вызывается как функция с круглыми скобками, возвращая новый объект, который вы можете сохранить в переменной.

После объявления класса, вам (или другом программисту) не обязательно понимать всё внутреннее строение для его дальнейшего использования. Главное — знать, какие данные в нем хранятся, и какие его методы можно вызывать. В этом классы подобны модулям, так как скрывают внутреннее строение, оставляя на поверхности только внешний «интерфейс» для использования.

Это сочетание данных и функций внутри одной сущности, вместе с сокрытием внутреннего строения, называется инкапсуляцией и является главным принципом ООП.

Интересный прием : если метод не должен возвращать конкретного значения, часто его пишут так, чтобы возвращать сам объект. Это позволяет при необходимости строить длинные «цепи» методов. Обратите внимание: обратный слэш в конце строк в коде программы сигнализирует интерпретатору, что инструкция не завершена и ее продолжение следует искать на следующей строке (обычно его можно использовать не только для объектов, но и для хорошего форматирования любых команд, например длинных условий в структуре ветвления).

class ProductInStore(object):
    price = 9.99
    name = 'new product'
    qty_in_stock = 0
 
    def set_price(self, price):
        self.price = price
        return self
 
    def set_name(self, name):
        self.name = name
        return self
 
    def set_stock(self, qty):
        self.qty_in_stock = qty
        return self
 
abn911 = ProductInStore()
abn911.set_name('Lego Mindstorm v1') \
    .set_price(199.99) \
    .set_stock(5)
print abn911.name, abn911.price, abn911.qty_in_stock

вместо

abn911 = ProductInStore()
abn911.set_name('Lego Mindstorm v1')
abn911.set_price(199.99)
abn911.set_stock(5)
print abn911.name, abn911.price, abn911.qty_in_stock

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

При объявлении класса в скобках могут быть записаны (одно или несколько) имена уже существующих классов — это называется подражанием. В таком случае новый класс (который называется дочерним) наследует все поля и методы классов перечисленных в скобках (которые называются родительскими).

Добавим 2 вида товаров, которые унаследуем от исходного. Это «обычный» товар, который ничем не отличается от базового класса, и «товар с опциями», у которого есть дополнительная опция, которую можно выбрать при заказе — пусть она может влиять, например, на цену товара.

class ProductInStore(object):
...
    def __init__(self, name, price):
        self.name = name
        self.price = price
...
class SimpleProductInStore(ProductInStore):
    pass
 
class ConfigurableProductInStore(ProductInStore):
    option_prices = None
 
    def __init__(self, name, basic_price, add_prices):
        self.name = name
        self.price = basic_price
        self.option_prices = add_prices
 
abn911 = ConfigurableProductInStore('Lego Mindstorm v1', 199.99, {'v1': 0, 'v1.1': 0, 'v2': 21.0, 'v3': 50.5})
print abn911.name, abn911.price, abn911.qty_in_stock, \
    abn911.option_prices
abn912 = SimpleProductInStore('Anakin action figure 921', 56.11)
print abn912.name, abn912.price, abn912.qty_in_stock

В данном примере класс SimpleProductInStore вообще не имеет ничего своего, но мы можем создавать его экземпляры — он полностью подражает поведению класса ProductInStore. Класс ConfigurableProductInStore также наследует ProductInStore, получая поля name, price, qty_in_stock, но к ним добавляет свое собственное option_prices.

Также они могут иметь собственные методы с одинаковыми именами (это называется полиморфизмом ). Можно объявить их отдельно в каждом классе:

class SimpleProductInStore(ProductInStore):
    def get_price(self):
        return self.price
 
class ConfigurableProductInStore(ProductInStore):
    # ...
    def get_price(self, option = None):
        if option != None and option in self.option_prices:
            return self.price + self.option_prices[option]
        return self.price

Но, если этот метод характерен для всех товаров, лучше объявить его сразу в родительском классе ProductInStore, чтобы он точно существовал во всех дочерних классах, а в дочерних — при необходимости его можно будет переопределить.

class ProductInStore(object):
    price = 9.99
    name = 'new product'
    qty_in_stock = 0
 
    def __init__(self, name, price):
        self.name = name
        self.price = price
 
    def get_price(self):
        return self.price
 
class SimpleProductInStore(ProductInStore):
    pass
 
class ConfigurableProductInStore(ProductInStore):
    option_prices = None
 
    def get_price(self, option = None):
        if option != None and option in self.option_prices:
            return self.price + self.option_prices[option]
        return self.price
 
    def __init__(self, name, basic_price, add_prices):
        self.name = name
        self.price = basic_price
        self.option_prices = add_prices

Теперь мы будем уверены, что при добавлении каких-либо новых типов товаров метод get_price () будет для них определен и при обработке товара не произойдет ошибки.

Метод для базового товара может быть сложнее, чем вернуть цену. Поэтому вместо повторения действия в методе get_price () класса ConfigurableProductInStore лучше вызвать родительский метод:

class SimpleProductInStore(ProductInStore):
    def get_price(self, option = None):
        return super(SimpleProductInStore, self).get_price()
 
class ConfigurableProductInStore(ProductInStore):
    option_prices = None
 
    def get_price(self, option = None):
        price = super(ConfigurableProductInStore, self).get_price()
        if option != None and option in self.option_prices:
            price += self.option_prices[option]
        return price

Это позволяет нам повторно использовать существующий код и — самое главное — в таком случае, если родительский класс будет изменен, мы будем уверены, что дочерний класс «подхватит» изменения и его код не придется лишний раз переписывать.
Конечный пример:

class ProductInStore(object):
    price = 9.99
    name = 'new product'
    qty_in_stock = 0
 
    def __init__(self, name, price):
        self.name = name
        self.price = price
 
    def set_price(self, price):
        self.price = price
        return self
 
    def set_name(self, name):
        self.name = name
        return self
 
    def set_stock(self, qty):
        self.qty_in_stock = qty
        return self
 
    def get_price(self):
        return self.price
 
    def report(self):
        return "%s of '%s' are left" % (self.qty_in_stock, self.name)
 
class SimpleProductInStore(ProductInStore):
    def get_price(self, option = None):
        return super(SimpleProductInStore, self).get_price()
 
    def report(self):
        return "-----\nsimple product report\n-----\n%s of '%s' are left \ncurrent price is %s" % (self.qty_in_stock, self.name, self.price)
 
class ConfigurableProductInStore(ProductInStore):
    option_prices = None
 
    def get_price(self, option = None):
        price = super(ConfigurableProductInStore, self).get_price()
        if option != None and option in self.option_prices:
            price += self.option_prices[option]
        return price
 
    def __init__(self, name, basic_price, add_prices):
        self.name = name
        self.price = basic_price
        self.option_prices = add_prices
 
    def report(self):
        report = "-----\nconfigurable product report\n-----\n%s of '%s' are left \ncurrent price is %s" % (self.qty_in_stock, self.name, self.price)
        if len(self.option_prices):
            report += "\nproduct options:"
            for i in self.option_prices:
                report += "\n- %s : %s" % (i, self.option_prices[i])
        return report
 
abn911 = ConfigurableProductInStore('Lego Mindstorm v1', 199.99, {'v1': 0, 'v1.1': 0, 'v2': 21.0, 'v3': 50.5})
print abn911.report()
print 'price for v3 is %s' % (abn911.get_price('v3'))
abn912 = SimpleProductInStore('Anakin action figure 921', 56.11)
print abn912.report()
print 'price is %s' % (abn912.get_price())

Стандартный модуль datetime

Содержит 5 классов:

datetime.datetime — для представления одновременно даты и времени.
datetime.date — для представления только даты. Содержит методы, аналогичные методам datetime для работы с датами.
datetime.time — для представления только времени. Содержит методы, аналогичные методам datetime для работы со временем.
datetime.timedelta — для представления разницы во времени, используется для проведения арифметических действий над датами и временем
datetime.tzinfo — для представления информации о временной зоне (часовой пояс).

Полная документация  к модулю доступна на сайте python (на английском языке).

класс datetime.datetime

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

x = datetime. datetime (year, month, day [, hour [, minute [, second [, microsecond [, tzinfo]]]]]) — конструктор класса. Первые 3 параметра (год, месяц и день) являются обязательными, другие необязательные и могут задаваться как именованные аргументы.

dt = datetime.datetime(2019, 1, 16)
dt2 = datetime.datetime(2019, 1, 16, minute=11)

x = datetime. datetime.today () — метод класса, используется для создания объекта сегодняшней даты (как альтернатива конструктору).

x = datetime. datetime.now ([tz]) — то же самое, но с возможностью задать временную зону.

dt3 = datetime.datetime.today()

x = datetime. datetime.combine (date, time) — метод класса, создает объект datetime из пары объектов date и time.

x = datetime. datetime.strptime (date_string, format) — метод класса, создает объект datetime из строки date_string, считая, что он содержит дату в формате format.

Поля объектов класса доступны только для чтения:

x. year — год (целое число).

x. month — месяц (целое число от 1 до 12).

x. day — число (целое число от 1 до количества дней в месяце).

x. hour — часы (целое число от 0 до 23).

x. minute — минуты (целое число от 0 до 59).

x. second — секунды (целое число от 0 до 59).

x. microsecond — микросекунды (целое число от 0 до 1000000).

x. tzinfo — временная зона (объекта класса datetime.tzinfo или None).

Методы объектов класса

x. date () — возвращает объект datetime.date с аналогичными данными о дате (годом, месяцем и числом).

x. time () — возвращает объект datetime.time с аналогичными данными о времени.

x. replace ([year [, month [, day [, hour [, minute [, second [, microsecond [, tzinfo]]]]]]]]) — возвращает новый объект datetime с измененными значениями указанных полей относительно объекта x. Для указания конкретных полей необходимо передать их название как именованные параметры.

print dt.replace(year=2013,hour=12) # datetime.datetime(2013, 1, 16, 12, 0)
print dt # datetime.datetime(2014, 1, 16, 0, 0)

x. weekday () — возвращает порядковый номер дня недели (0 — понедельник, 6 — воскресенье).

print dt.weekday() # 1

x. isoweekday () — возвращает порядковый номер дня недели в ISO-формате (1 — понедельник, 7 — воскресенье).

print dt.isoweekday()    # 2

x. isocalendar () — возвращает тьюпл, содержащий год, номер недели и номер дня недели в ISO-формате.

print dt.isocalendar()           # (2019, 8, 4)

x. isoformat ([sep]) — возвращает строку, содержащую дату, отформатированную в формате ISO. В качестве разделителя между датой и временем используется sep. Если sep не задан, то ‘T’.

print x.isoformat()          # 2019-08-16T00:00:00

x. strftime (format) — возвращает строку, содержащую дату в формате, заданном строкой format. Точный перечень обозначений для всех возможных элементов даты можно посмотреть в документации.

dt.strftime("%A, %d. %B %Y %I:%M%p")   #'Thursday, 01. October 2018 12:00AM'

класс datetime.timedelta

x = datetime. timedelta ([days [, seconds [, microseconds [, milliseconds [, minutes [, hours [, weeks]]]]]]]) — конструктор класса. Все аргументы являются необязательными и могут задаваться в именуемом виде.

td = datetime.timedelta(hours=48)
td2 = datetime.timedelta()

Объекты класса содержат следующие поля, доступные только для чтения:

x. days — количество дней в промежутке времени (целое число от -999 999 999 до 999 999 999 включительно).

x. seconds — количество секунд в промежутке времени (целое число от 0 до 86399 включительно).

x. microseconds — количество микросекунд в промежутке времени.

Объекты класса имеют один метод:

x. total_seconds () — возвращает общее количество секунд в промежутке времени.

print td.total_seconds()  # 172800.0

Кроме того, объекты класса datetime.timedelta можно сравнивать, добавлять и отнимать между собой, умножать и делить на целое число, добавлять то отнимать к датам (объектов datetime.datetime, datetime.date, datetime.time). Также объекты datetime.timedelta возникают при вычитании дат друг от друга.

Обработка ошибок в python

Если во время выполнения программы что-то идет не так, интерпретатор python генерирует объект исключения одного из стандартных классов, в зависимости от того, что именно произошло. Все эти стандартные классы содержатся в модуле exception, который не требует дополнительного подключения.

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

try:
    # действия, которые могут вызвать ошибку
except ExceptionClass:
    # действия, которые должны быть выполнены в случае ошибки

Вместо ExceptionClass указывается название конкретного класса исключений, которые должны перехватываться. Исключение других классах не будут перехвачены в случае их появления. Самый простой способ определить необходимый класс исключение — это воссоздать необходимую ошибку и посмотреть название класса ее исключения в стандартном сообщении интерпретатора:

x = int(raw_input('input number:'))
В нашем примере — это ValueError.

Если необходимо перехватывать одновременно несколько классов исключений, они перечисляются в тьюпле:

x = None
while not x:
    try:
        x = int(raw_input('input number:'))
    except (TypeError, ValueError):
        print 'error happened. try once more'
print 'finally!'

Различные исключения могут иметь различные поля с данными, но общим для всех является поле message, в котором хранится текстовое сообщение, описывает ошибку. Для получения подробных сведений об ошибке перехвачено исключения можно сохранить как переменную:

x = None
while not x:
    try:
        x = int(raw_input('input number:'))
    except (TypeError, ValueError) as e:
        print 'error { %s } happened. try once more' % (e.message)
print 'finally!'

Стандартные исключения наследуются друг от друга, образуя сложную иерархию. Указывая в except исключения родительских классов, вы отлавливаете также дочерние для них исключения:

x = None
while not x:
    try:
        x = int(raw_input('input number:'))
    except StandardError as e:
        print 'error { %s } happened. try once more' % (e.message)
print 'finally!'

Как это работает? При возникновении ошибки работа текущей функции прекращается и генерируется объект исключения, который передается из функции, где возникла ошибка, на уровень выше — в функцию, которая ее вызвала, также прекращая ее работу, потом еще на уровень выше, и так далее до тела главной программы. Если на этом пути исключения не было перехвачено (ни одна из этих функций не находилась внутри блока try..except), работа всей программы будет таким образом остановлена и на экран будет выведено стандартное сообщение об ошибке с указанием точки в коде, где ошибка произошла.

Полная иерархия стандартных исключений выглядит следующим образом: 

 BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StandardError
      |    +-- BufferError
      |    +-- ArithmeticError
      |    |    +-- FloatingPointError
      |    |    +-- OverflowError
      |    |    +-- ZeroDivisionError
      |    +-- AssertionError
      |    +-- AttributeError
      |    +-- EnvironmentError
      |    |    +-- IOError
      |    |    +-- OSError
      |    |         +-- WindowsError (Windows)
      |    |         +-- VMSError (VMS)
      |    +-- EOFError
      |    +-- ImportError
      |    +-- LookupError
      |    |    +-- IndexError
      |    |    +-- KeyError
      |    +-- MemoryError
      |    +-- NameError
      |    |    +-- UnboundLocalError
      |    +-- ReferenceError
      |    +-- RuntimeError
      |    |    +-- NotImplementedError
      |    +-- SyntaxError
      |    |    +-- IndentationError
      |    |         +-- TabError
      |    +-- SystemError
      |    +-- TypeError
      |    +-- ValueError
      |         +-- UnicodeError
      |              +-- UnicodeDecodeError
      |              +-- UnicodeEncodeError
      |              +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
    +-- ImportWarning
    +-- UnicodeWarning
    +-- BytesWarning 

Практические задачи

Задание 1

Разработать класс Sphere для представления сферы в трехмерном пространстве.

Обеспечить следующие методы класса:

      • конструктор, принимающий 4 действительных числа: радиус, и 3 координаты центра шара. Если конструктор вызывается без аргументов, создать объект сферы с единичным радиусом и центром в начале координат. Если конструктор вызывается с 1 аргументом, создать объект сферы с соответствующим радиусом и центром в начале координат.
      • метод get_volume (), который возвращает действительное число — объем шара, ограниченной текущей сферой.
      • метод get_square (), который возвращает действительное число — площадь внешней поверхности сферы.
      • метод get_radius (), который возвращает действительное число — радиус сферы.
      • метод get_center (), который возвращает тьюпл с 3 действительными числами — координатами центра сферы в том же порядке, в каком они задаются в конструкторе.
      • метод set_radius (r), который принимает 1 аргумент — действительное число, и меняет радиус текущей сферы, ничего не возвращая.
      • метод set_center (x, y, z), который принимает 3 аргумента — действительных числа, и меняет координаты центра сферы, ничего не возвращая. Координаты задаются в том же порядке, что и в конструкторе.
      • метод is_point_inside (x, y, z), который принимает 3 аргумента — действительных числа — координаты некоторой точки в пространстве (в том же порядке, что и в конструкторе), и возвращает логическое значение True или False в зависимости от того, находится эта точка внутри сферы.

Пример последовательности действий для тестирования класса:
s0 = Sphere (0.5) # test sphere creation with radius and default center
print s0.get_center () # (0.0, 0.0, 0.0)
print s0.get_volume () # 0.523598775598
print s0.is_point_inside (0 , -1.5, 0) # False
s0.set_radius (1.6)
print s0.is_point_inside (0, -1.5, 0) # True
print s0.get_radius () # 1.6

Смотреть ответ:

Задание 2

Разработать класс SuperStr, который наследует функциональность стандартного типа str и содержит 2 новых метода:

  • метод is_repeatance (s), который принимает 1 аргумент s и возвращает True или False в зависимости от того, может ли текущая строку быть получена целым количеством повторов строки s. Вернуть False, если s не является строкой. Считать, что пустая строка не содержит повторов.
  • метод is_palindrom (), который возвращает True или False в зависимости от того, является ли строка палиндромом. Регистрами символов пренебрегать. Пустую строку считать палиндромом.

Пример последовательности действий для тестирования класса:
s = SuperStr ( ‘123123123123’)
print s.is_repeatance ( ‘123’) # True
print s.is_repeatance ( ‘123123’) # True
print s.is_repeatance ( ‘123123123123’) # True
print s .is_repeatance ( ‘12312’) # False
print s.is_repeatance (123) # False
print s.is_palindrom () # False
print s # 123123123123 (строка)
print int (s) # 123123123123 (целое число)
print s + ‘qwe’ # 123123123123qwe
p = SuperStr ( ‘123_321’)
print p.is_palindrom () # True

Смотреть ответ:

Задание 3

Разработать функцию create_calendar_page (month, year),
которая принимает 2 аргумента — целые числа — месяц (нумерация начинается с 1) и год, и возвращает оператором return 1 строку, содержащую страницу календаря на текущий месяц.

Если месяц и год не заданы, использовать сегодняшние значения.

Возвращаемая «Страница» имеет следующий формат:

Это значение является одной строкой с переносами строки \n, пробелы после числа 28 отсутствуют. Лишние пробелы в конце подстрок или всей строки, как и лишние пустые строки, недопустимы.

Пример вызовов для тестирования функции:

print create_calendar_page(1)
print create_calendar_page()
print create_calendar_page(3)
print create_calendar_page(08, 2019)

Смотреть ответ:

Читайте больше по теме:

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
Просмотры: 570

Популярные записи