Парадигма ООП, базовые приемы работы с классом

Сразу следует отметить, что ООП – это совершенно другая парадигма программирования, нежели структурное программирование, основу которого в основном образует функциональное программирование. По сути, мы с вами рассматривали структурное программирование на начальных занятиях по Python. Здесь же перейдем на следующий уровень написания программ в соответствии с ООП. Но что это такое и чем оно кардинально отличается от структурного подхода? Общий принцип я, обычно, объясняю на примере из реальной жизни – кофемолки. Представьте, что некий инженер решил создать электрическую кофемолку. До этого все кофемолки были ручные. Что же он сделал? Он взял механическую часть ручной кофемолки, поставил электрический мотор, добавил кнопку включения на корпусе и получил новый прибор. То есть, для создания нового прибора он использовал уже существующие наработки, а не делал все с нуля. Ровно это лежит в концепции наследования ООП. Мы можем брать ранее созданные классы, выполняющие определенные задачи и, затем, видоизменять их под текущий проект. В этом случае создается новый класс на основе существующего и дополняется необходимым функционалом. Это первый столп (наследование), на котором базируется ООП.

Далее, чтобы новая кофемолка представляла собой единый объект, а не два разрозненных (старая + электрический мотор), в новом созданном классе можно вызывать функции наследуемого класса так, словно они определены в новом. Это называется полиморфизмом в ООП. Благодаря полиморфизму программист будет работать с новым классом как с единым объектом, используя всю необходимую функциональность, несмотря на то, что он является лишь производным от другого класса. А про базовый класс он может даже и не знать.  Это второй столп ООП – полиморфизм.

Наконец, третье и последнее. Хозяин новой электрической кофемолки имеет возможность только засыпать в нее зерна, нажимать на кнопку «мотор» и высыпать уже перемолотый кофе. Вся начинка: мотор, лопасти, электрическая схема – скрыты от пользователя. Для достижения результата ему эта начинка не нужна. Достаточно лишь знать последовательность действий и больше ничего. Ровно это делает инструмент инкапсуляция в ООП. Инкапсуляция – это возможность закрывать данные и методы класса от внешнего вмешательства. И вся работа с классом возможна только через разрешенные интерфейсные методы и реже непосредственно через данные.

Вот так в целом можно представить себе три основные концепции, лежащие в основе парадигмы ООП:

  • инкапсуляция;
  • наследование;
  • полиморфизм.

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

Класс можно воспринимать как некую схему, чертеж, по которому конструируются его экземпляры. Представьте, что нам нужно построить дом. С чего начинается строительство? Да, с плана дома, его чертежей и схем. А уже потом по этому плану строится здание. И таких однотипных зданий можно построить множество. Вот также следует воспринимать класс – это схема для построения однотипных экземпляров данного класса.

Чтобы в Python определить класс, записывается ключевое слово class, и через пробел указывается его имя:

class Point:
    x = 1
    y = 1

Обратите внимание, в соответствии со стандартом PEP8 имя класса принято записывать с заглавной буквы. И, конечно же, называть так, чтобы имя отражало суть этого класса. В дальнейшем мы будем придерживаться этого правила.

Сразу же здесь отмечу, что по тому же стандарту PEP8 первая строка в классе автоматически считается его описанием, например, так:

class Point:
    "Класс для представления координат точек на плоскости"
    x = 1
    y = 1

И мы впоследствии можем вывести это описание с помощью предопределенного свойства __doc__:

print (Point.__doc__)

Вообще существует множество предопределенных свойств, например:

print (Point.__name__)

возвращает имя класса. Мы с ними будем постепенно знакомиться по мере необходимости. Полный набор данных класса или экземпляра можно увидеть с помощью функции dir:

print( dir(pt) )

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

pt = Point()

И вот здесь появляется первый важный момент. Нам нужно четко понимать: что из себя представляет переменная pt, а точнее, на что она ссылается? В действительности, мы ее можем рассматривать как пространство имен с именем pt. В этом пространстве есть две переменные x и y, которые берутся непосредственно из класса Point. И мы в этом можем легко убедиться, если изменить переменную x в самом классе:

Point.x = 100

то это приведет к изменению значения и в пространстве имен pt:

print( pt.x, pt.y )

Но при этом, pt и Point – это совершенно разные объекты, их id:

print( id(pt), id(Point), sep="\n" )

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

pt2 = Point()

и так далее, то все они будут ссылаться на независимые объекты, но переменные x, y в них, будут также браться из класса Point. Для тех кто знает ООП на С++ или Java, это похоже на поведение статических переменных, объявленных внутри класса. Кстати, в Python статические переменные именно так и объявляют, но о них мы будем говорить отдельно позже.

Далее, смотрите, если после создания экземпляра написать вот такие строчки:

pt.x = 5
pt.y = 10

то в пространстве имен pt будут созданы свои переменные x и y, ссылающиеся на свои числовые значения, никак не связанные с классом Point:

print( pt.x, Point.x )

И в каждом экземпляре имеется ссылка на словарь __dict__, который содержит список всех его локальных переменных

print( pt.__dict__ )
pt.x = 5
pt.y = 10
print( pt.__dict__ )

Из этого примера хорошо видно, что до определения x, y внутри пространства pt словарь был пуст, а после, появились две переменные. В терминологии ООП языка Python переменные x, y внутри класса Point или его экземпляров называются атрибутами (или свойствами). Именно так я в дальнейшем их буду называть.

С атрибутами экземпляров класса можно работать через такие функции:

  • getattr(obj, name [, default]) — возвращает значение атрибута объекта;
  • hasattr(obj, name) — проверяет на наличие атрибута name в obj;
  • setattr(obj, name, value) — задает значение атрибута (если атрибут не существует, то он создается);
  • delattr(obj, name) — удаляет атрибут с именем name.

Использовать их можно так:

print( getattr(pt, "x") )
print( getattr(pt, "z") )
print( getattr(pt, "z", False) )
print( hasattr(pt, "z") )
setattr(pt, "z", 7)
print( pt.__dict__ )
delattr(pt, "z")
print( pt.__dict__ )

Эти же операции можно делать и с самим классом:

setattr(Point, "z", 7)

Спрашивается, зачем нужны эти функции, если с атрибутами мы можем работать непосредственно через имена объектов:

Point.z = 100
pt.msg = "hello"

и так далее. Дело в том, что в ряде случаев они дают большую гибкость. Например, при обращении к несуществующему атрибуту:

pt.sss

получим ошибку, но через функцию:

res = getattr(pt, "sss", False)

ошибки не будет и переменная res будет равна False. Кроме того, удалить атрибут:

delattr(pt, "x")

или проверить его существование:

print( hasattr(Point, "t") )

можно только через эти функции. Правда, удалить атрибут можно также с помощью оператора del:

del pt.x
print( pt.__dict__ )

В заключение этого занятия рассмотрим весьма полезную функцию isinstance(), которая позволяет определить принадлежность экземпляра к тому или иному классу. Например, вот такая запись:

print(isinstance(pt, Point))

отобразит в консоли значение True, так как pt – экземпляр класса Point. Если для примера, объявить еще один класс:

class Point3D:
    pass

и выполнить проверку:

print(isinstance(pt, Point3D))

то получим значение False, т.к. pt – не экземпляр класса Point3D.

Итак, на этом первом занятии мы с вами разобрали принципы, лежащие в основе ООП:

  • инкапсуляция;
  • наследование;
  • полиморфизм.

Понятие класса, экземпляра класса и как они создаются. Понятие атрибутов, затронули некоторые встроенные переменные:

  • __doc__ –  содержит строку с описанием класса;
  • __name__ –  содержит строку с именем класса;
  • __dict__ –  содержит набор атрибутов экземпляра класса.

Набор функций для работы с атрибутами и функцию isinstance.

Задание для самоподготовки

Объявите класс Point3D для точек с тремя координатами x, y, z. Создайте несколько экземпляров этого класса и через них выведите в консоль значения x,y,z. Далее, сделайте следующие манипуляции:

  • поменяйте любое значение координаты в классе Point3D и посмотрите как это повлияет на отображаемые величины экземпляров класса;
  • удалите координату z в классе Point3D и убедитесь, что она будет отсутствовать во всех экземплярах;
  • поменяйте координату в каком-либо экземпляре класса и посмотрите на результат.