Лекция 10, введение в объектно-ориентированное программирование

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

Основные особенности ООП:

  1. ООП увеличивает модульность программы и даёт больше возможностей по повторному использованию уже написанных элементов программы.
  2. Основные понятия ООП:
    1. Объект – некоторая сущность, как имеющая пример среди физических объектов, так и абстрактная, которая может иметь некоторый набор свойств и методов.
      1. Свойства – данные объектов
      2. Методы – действия над объектам или действия объектов над другими объектами
    2. Класс объединяет некоторое множество объектов и определяет для них свойства и методы.

Синтаксис:

class Empty:
    pass
a = Empty()
a.name = 'John'  # Создаём свойство с именем name и значением 'John'
a.age = 10
print(a.name, a.age)  # Читаем значения свойств
b = a      # b ссылается на тот же объект, что и a.
b.age = 12 # изменятся и a, и b
c = Empty()  # Новый объект, не связанный с a и b. Свойств name и age нет.

Как уже сказано, класс должен определять некоторый набор свойств, так что давайте автоматизируем добавление свойств name и age в класс. Делается это с помощью метода __init__, автоматически вызываемого интерпретатором Python при создании объекта.

class Simple:
    def __init__(self, name, age):
        # __init__ всегда получает первым аргументом self только что созданный
        # объект, который этот метод должен инициализировать. После self
        # идут те аргументы, которые должны передаваться при создании объекта.
        self.name = name
        self.age = age

a = Simple('John', 10)  # 'John' и 10 станут аргументами метода __init__,
                        # который заполнит свойства name и age.
print(a.name, a.age)   # John 10
b = Simple('Vasya', 12)
print(b.name, b.age)   # Vasya 12
# Специально стоит отметить, что a и b -- это разные объекты, и после
# того, как мы создали b или изменили его свойства, свойства a не меняются
print(a.name, a.age)   # John 10

Более сложный пример, где мы кроме метода __init__ определим и другие методы:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_older(self):
        self.age += 1

    def introduce(self):
        print("Hello, my name is {}. I am {} years old".format(self.name, self.age))

    def read_book (self, title):
        print("I am reading", title)

p = Person('John', 10)
p.get_older()
p.introduce()  # Hello, my name is John. I am 11 years old.
p.read_book("War and peace")  # I am reading War and peace
  1. Метод с именем __init__ вызывается при создании объекта автоматически.
  2. Аргумент self подставляется автоматически

Инкапсуляция

Инкапсуляция – сокрытие данных о внутренней структуре объекта (в первую очередь о свойствах).

  1. Инкапсуляция повышает модульность. Если вызывающий код как можно меньше знает об алгоритмах и внутренних структурах данных некоторого объекта, у нас появляется больше возможностей дополнять и изменять этот объект без модификаций того кода, который этот объект уже использует. Примером служит файловый объект, реализация которого различна на операционных системах Linux и Windows, однако файлы в Python3 инкапсулируют все эти подробности внутри своих методов read, readline, readlines, write, close и других, и наш код будет одинаково работать и на Windows, и на Linux.
  2. Инкапсуляция достигается, если не читать и не записывать свойства извне без участия методов.
class Person:
    # Здесь те же методы, что и раньше
    def get_name(self):
        return self.name
    def set_name(self):
        self.name = name

Казалось бы, мы не получили никакого выигрыша, так как только добавили в класс два новых метода, и читать и записывать свойства стало несколько сложнее. Однако, это защищает нас от возможных изменений внутренней структуры класса. Например, в следующей версии нашей программы мы можем решить, что при изменении имени наш человек должен получать новый паспорт. Если мы уже пользовались методами доступа, то нам понадобится лишь немного изменить метод set_name:

class Person:
    # Здесь те же методы, что и раньше
    def get_name(self):
        return self.name
    def set_name(self):
        self.name = name
        self.new_passport()
    def new_passport(self):
        ... меняем паспорт

Если бы мы не пользовались методами доступа, пришлось бы править все места в файлах, где вручную изменялось свойство name, и добавлять получение нового паспорта.

Специальные методы

В языке Python есть некоторые методы со строго определёнными именами, которые имеют специальное значение и будут автоматически, то есть без явного указания их программистом, вызваны интерпретатором Python. Один такой метод мы уже знаем, и это __init__. Мы создаём объект и нигде не указываем, что нужно вызвать этот метод, а интерптератор всё равно автоматически вызывает его. Есть и другие методы, например __add__. Всякий раз, когда интерпретатор видит выражение вида

a + b

он заменяет его на

a.__add__(b)

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

Выражение Выражение после преобразования Примечание
str(x) x.__str__()  
repr(x) x.__repr__()  
bytes(x) x.__bytes__()  
bool(x) x.__bool__()  
x < y x.__lt__(y) lower than
x <= y x.__le__(y) lower or equal
x > y x.__gt__(y) greater than
x >= y x.__ge__(y) greater or equal
x == y x.__eq__(y) equal
x != y x.__ne__(y) not equal
int(x) x.__int__()  
float(x) x.__float__()  
complex(x) x.__complex__()  
round(x, n) x.__round__(n)  
x + y x.__add__(y)  
x - y x.__sub__(y)  
x * y x.__mul__(y)  
x / y x.__truediv__(y)  
x // y x.__floordiv__(y)  
x % y x.__mod__(y)  
divmod(x, y) x.__divmod__(y) должна возвращать пару (целое, остаток)
x ** y x.__pow__(y)  
x << y x.__lshift__(y)  
x >> y y.__rshift__(y)  
x & y x.__and__(y)  
x | y x.__or__(y)  
x ^ y x.__xor__(y)  
len(a) a.__len__()  
a[key] a.__getitem__(key)  
a[key] = val a.__setitem__(key, val)  
del a[key] a.__delitem__(key)  

Чтож, давайте с помощью новых знаний реализуем двумерный вектор:

class Vec2d:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vec2d(self.x + other.x, self.y + other.y)

    def __str__(self):
        return 'Vec2d({}, {})'.format(self.x, self.y)

a = Vec2d(10, 20)
b = Vec2d(1, -1.1)
c = a + b
print(a, '+', b, '==', c)

В коде выше кроме метода __add__ реализован ещё и __str__, который нужен функции print для перевода наших векторов в строки. Как видно из таблицы, функция str автоматически вызывает метод __str__ переданного ей объекта. Выведет наш код следующее:

Vec2d(10, 20) + Vec2d(1, -1.1) == Vec2d(11, 18.9)