Python. Урок 14. Классы и объекты
![]()
Данный урок посвящен объектно-ориентированному программированию в Python. Разобраны такие темы как создание объектов и классов, работа с конструктором, наследование и полиморфизм в Python.
Основные понятия объектно-ориентированного программирования
Объектно-ориентированное программирование (ООП) является методологией разработки программного обеспечения, в основе которой лежит понятие класса и объекта, при этом сама программа создается как некоторая совокупность объектов, которые взаимодействую друг с другом и с внешним миром. Каждый объект является экземпляром некоторого класса. Классы образуют иерархии. Более подробно о понятии ООП можно прочитать на википедии.
Выделяют три основных “столпа” ООП- это инкапсуляция, наследование и полиморфизм.
Инкапсуляция
Под инкапсуляцией понимается сокрытие деталей реализации, данных и т.п. от внешней стороны . Например, можно определить класс “холодильник”, который будет содержать следующие данные: производитель, объем, количество камер хранения, потребляемая мощность и т.п., и методы: открыть/закрыть холодильник, включить/выключить, но при этом реализация того, как происходит непосредственно включение и выключение пользователю вашего класса не доступна, что позволяет ее менять без опасения, что это может отразиться на использующе й класс «холодильник» программе. При этом класс становится новым типом данных в рамках разрабатываемой программы. Можно создавать переменные этого нового типа, такие переменные называются объекты.
Наследование
Под наследованием понимается возможность создания нового класса на базе существующего. Наследование предполагает наличие отношения “является” между классом наследником и классом родителем. При этом класс потомок будет содержать те же атрибуты и методы, что и базовый класс, но при этом его можно (и нужно) расширять через добавление новых методов и атрибутов.
Примером базового класса, демонстрирующего наследование, можно определить класс “автомобиль”, имеющий атрибуты: масса, мощность двигателя, объем топливного бака и методы: завести и заглушить. У такого класса может быть потомок – “грузовой автомобиль”, он будет содержать те же атрибуты и методы, что и класс “автомобиль”, и дополнительные свойства: количество осей, мощность компрессора и т.п..
Полиморфизм
Полиморфизм позволяет одинаково обращаться с объектами, имеющими однотипный интерфейс, независимо от внутренней реализации объекта. Например, с объектом класса “грузовой автомобиль” можно производить те же операции, что и с объектом класса “автомобиль”, т.к. первый является наследником второго, при этом обратное утверждение неверно (во всяком случае не всегда). Другими словами полиморфизм предполагает разную реализацию методов с одинаковыми именами. Это очень полезно при наследовании, когда в классе наследнике можно переопределить методы класса родителя.
Классы в Python
Создание классов и объектов
Создание класса в Python начинается с инструкции class. Вот так будет выглядеть минимальный класс.
class C: pass
Класс состоит из объявления (инструкция class), имени класса (нашем случае это имя C) и тела класса, которое содержит атрибуты и методы (в нашем минимальном классе есть только одна инструкция pass).
Для того чтобы создать объект класса необходимо воспользоваться следующим синтаксисом:
имя_объекта = имя_класса()
Статические и динамические атрибуты класса
Как уже было сказано выше, класс может содержать атрибуты и методы. Атрибут может быть статическим и динамическим (уровня объекта класса). Суть в том, что для работы со статическим атрибутом, вам не нужно создавать экземпляр класса, а для работы с динамическим – нужно. Пример:
class Rectangle: default_color = "green" def __init__(self, width, height): self.width = width self.height = height
В представленном выше классе, атрибут default_color – это статический атрибут, и доступ к нему, как было сказано выше, можно получить не создавая объект класса Rectangle.
>>> Rectangle.default_color 'green'
width и height – это динамические атрибуты, при их создании было использовано ключевое слово self. Пока просто примите это как должное, более подробно про self будет рассказано ниже. Для доступа к width и height предварительно нужно создать объект класса Rectangle:
>>> rect = Rectangle(10, 20) >>> rect.width 10 >>> rect.height 20
Если обратиться через класс, то получим ошибку:
>>> Rectangle.width Traceback (most recent call last): File "", line 1, in AttributeError: type object 'Rectangle' has no attribute 'width'
При этом, если вы обратитесь к статическому атрибуту через экземпляр класса, то все будет ОК, до тех пор, пока вы не попытаетесь его поменять.
Проверим ещё раз значение атрибута default_color:
>>> Rectangle.default_color 'green'
Присвоим ему новое значение:
>>> Rectangle.default_color = "red" >>> Rectangle.default_color 'red'
Создадим два объекта класса Rectangle и проверим, что default_color у них совпадает:
>>> r1 = Rectangle(1,2) >>> r2 = Rectangle(10, 20) >>> r1.default_color 'red' >>> r2.default_color 'red'
Если поменять значение default_color через имя класса Rectangle, то все будет ожидаемо: у объектов r1 и r2 это значение изменится, но если поменять его через экземпляр класса, то у экземпляра будет создан атрибут с таким же именем как статический, а доступ к последнему будет потерян:
Меняем default_color через r1:
>>> r1.default_color = "blue" >>> r1.default_color 'blue'
При этом у r2 остается значение статического атрибута:
>>> r2.default_color 'red' >>> Rectangle.default_color 'red'
Вообще напрямую работать с атрибутами – не очень хорошая идея, лучше для этого использовать свойства.
Методы класса
Добавим к нашему классу метод. Метод – это функция, находящаяся внутри класса и выполняющая определенную работу.
Методы бывают статическими, классовыми (среднее между статическими и обычными) и уровня класса (будем их называть просто словом метод). Статический метод создается с декоратором @staticmethod, классовый – с декоратором @classmethod, первым аргументом в него передается cls, обычный метод создается без специального декоратора, ему первым аргументом передается self:
class MyClass: @staticmethod def ex_static_method(): print("static method") @classmethod def ex_class_method(cls): print("class method") def ex_method(self): print("method")
Статический и классовый метод можно вызвать, не создавая экземпляр класса, для вызова ex_method() нужен объект:
>>> MyClass.ex_static_method() static method >>> MyClass.ex_class_method() class method >>> MyClass.ex_method() Traceback (most recent call last): File "", line 1, in TypeError: ex_method() missing 1 required positional argument: 'self' >>> m = MyClass() >>> m.ex_method() method
Конструктор класса и инициализация экземпляра класса
В Python разделяют конструктор класса и метод для инициализации экземпляра класса. Конструктор класса это метод __new__(cls, *args, **kwargs) для инициализации экземпляра класса используется метод __init__(self). При этом, как вы могли заметить __new__ – это классовый метод, а __init__ таким не является. Метод __new__ редко переопределяется, чаще используется реализация от базового класса object (см. раздел Наследование), __init__ же наоборот является очень удобным способом задать параметры объекта при его создании.
Создадим реализацию класса Rectangle с измененным конструктором и инициализатором, через который задается ширина и высота прямоугольника:
class Rectangle: def __new__(cls, *args, **kwargs): print("Hello from __new__") return super().__new__(cls) def __init__(self, width, height): print("Hello from __init__") self.width = width self.height = height >>> rect = Rectangle(10, 20) Hello from __new__ Hello from __init__ >>> rect.width 10 >>> rect.height 20
Что такое self?
До этого момента вы уже успели познакомиться с ключевым словом self. self – это ссылка на текущий экземпляр класса, в таких языках как Java, C# аналогом является ключевое слово this. Через self вы получаете доступ к атрибутам и методам класса внутри него:
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height
В приведенной реализации метод area получает доступ к атрибутам width и height для расчета площади. Если бы в качестве первого параметра не было указано self, то при попытке вызвать area программа была бы остановлена с ошибкой.
Уровни доступа атрибута и метода
Если вы знакомы с языками программирования Java, C#, C++ то, наверное, уже задались вопросом: “а как управлять уровнем доступа?”. В перечисленных языка вы можете явно указать для переменной, что доступ к ней снаружи класса запрещен, это делается с помощью ключевых слов (private, protected и т.д.). В Python таких возможностей нет, и любой может обратиться к атрибутам и методам вашего класса, если возникнет такая необходимость. Это существенный недостаток этого языка, т.к. нарушается один из ключевых принципов ООП – инкапсуляция. Хорошим тоном считается, что для чтения/изменения какого-то атрибута должны использоваться специальные методы, которые называются getter/setter, их можно реализовать, но ничего не помешает изменить атрибут напрямую. При этом есть соглашение, что метод или атрибут, который начинается с нижнего подчеркивания, является скрытым, и снаружи класса трогать его не нужно (хотя сделать это можно).
Внесем соответствующие изменения в класс Rectangle:
class Rectangle: def __init__(self, width, height): self._width = width self._height = height def get_width(self): return self._width def set_width(self, w): self._width = w def get_height(self): return self._height def set_height(self, h): self._height = h def area(self): return self._width * self._height
В приведенном примере для доступа к _width и _height используются специальные методы, но ничего не мешает вам обратиться к ним (атрибутам) напрямую.
>>> rect = Rectangle(10, 20) >>> rect.get_width() 10 >>> rect._width 10
Если же атрибут или метод начинается с двух подчеркиваний, то тут напрямую вы к нему уже не обратитесь (простым образом). Модифицируем наш класс Rectangle:
class Rectangle: def __init__(self, width, height): self.__width = width self.__height = height def get_width(self): return self.__width def set_width(self, w): self.__width = w def get_height(self): return self.__height def set_height(self, h): self.__height = h def area(self): return self.__width * self.__height
Попытка обратиться к __width напрямую вызовет ошибку, нужно работать только через get_width():
>>> rect = Rectangle(10, 20) >>> rect.__width Traceback (most recent call last): File "", line 1, in AttributeError: 'Rectangle' object has no attribute '__width' >>> rect.get_width() 10
Но на самом деле это сделать можно, просто этот атрибут теперь для внешнего использования носит название: _Rectangle__width:
>>> rect._Rectangle__width 10 >>> rect._Rectangle__width = 20 >>> rect.get_width() 20
Свойства
Свойством называется такой метод класса, работа с которым подобна работе с атрибутом. Для объявления метода свойством необходимо использовать декоратор @property.
Важным преимуществом работы через свойства является то, что вы можете осуществлять проверку входных значений, перед тем как присвоить их атрибутам.
Сделаем реализацию класса Rectangle с использованием свойств:
class Rectangle: def __init__(self, width, height): self.__width = width self.__height = height @property def width(self): return self.__width @width.setter def width(self, w): if w > 0: self.__width = w else: raise ValueError @property def height(self): return self.__height @height.setter def height(self, h): if h > 0: self.__height = h else: raise ValueError def area(self): return self.__width * self.__height
Теперь работать с width и height можно так, как будто они являются атрибутами:
>>> rect = Rectangle(10, 20) >>> rect.width 10 >>> rect.height 20
Можно не только читать, но и задавать новые значения свойствам:
>>> rect.width = 50 >>> rect.width 50 >>> rect.height = 70 >>> rect.height 70
Если вы обратили внимание: в setter’ах этих свойств осуществляется проверка входных значений, если значение меньше нуля, то будет выброшено исключение ValueError:
>>> rect.width = -10 Traceback (most recent call last): File "", line 1, in File "test.py", line 28, in width raise ValueError ValueError
Наследование
В организации наследования участвуют как минимум два класса: класс родитель и класс потомок. При этом возможно множественное наследование, в этом случае у класса потомка может быть несколько родителей. Не все языки программирования поддерживают множественное наследование, но в Python можно его использовать. По умолчанию все классы в Python являются наследниками от object, явно этот факт указывать не нужно.
Синтаксически создание класса с указанием его родителя выглядит так:
class имя_класса(имя_родителя1, [имя_родителя2,…, имя_родителя_n])
Переработаем наш пример так, чтобы в нем присутствовало наследование:
class Figure: def __init__(self, color): self.__color = color @property def color(self): return self.__color @color.setter def color(self, c): self.__color = c class Rectangle(Figure): def __init__(self, width, height, color): super().__init__(color) self.__width = width self.__height = height @property def width(self): return self.__width @width.setter def width(self, w): if w > 0: self.__width = w else: raise ValueError @property def height(self): return self.__height @height.setter def height(self, h): if h > 0: self.__height = h else: raise ValueError def area(self): return self.__width * self.__height
Родительским классом является Figure, который при инициализации принимает цвет фигуры и предоставляет его через свойства. Rectangle – класс наследник от Figure. Обратите внимание на его метод __init__: в нем первым делом вызывается конструктор (хотя это не совсем верно, но будем говорить так) его родительского класса:
super – это ключевое слово, которое используется для обращения к родительскому классу.
Теперь у объекта класса Rectangle помимо уже знакомых свойств width и height появилось свойство color:
>>> rect = Rectangle(10, 20, "green") >>> rect.width 10 >>> rect.height 20 >>> rect.color 'green' >>> rect.color = "red" >>> rect.color 'red'
Полиморфизм
Как уже было сказано во введении в рамках ООП полиморфизм, как правило, используется с позиции переопределения методов базового класса в классе наследнике. Проще всего это рассмотреть на примере. Добавим в наш базовый класс метод info(), который печатает сводную информацию по объекту класса Figure и переопределим этот метод в классе Rectangle, добавим в него дополнительные данные:
class Figure: def __init__(self, color): self.__color = color @property def color(self): return self.__color @color.setter def color(self, c): self.__color = c def info(self): print("Figure") print("Color: " + self.__color) class Rectangle(Figure): def __init__(self, width, height, color): super().__init__(color) self.__width = width self.__height = height @property def width(self): return self.__width @width.setter def width(self, w): if w > 0: self.__width = w else: raise ValueError @property def height(self): return self.__height @height.setter def height(self, h): if h > 0: self.__height = h else: raise ValueError def info(self): print("Rectangle") print("Color: " + self.color) print("Width: " + str(self.width)) print("Height: " + str(self.height)) print("Area: " + str(self.area())) def area(self): return self.__width * self.__height
Посмотрим, как это работает
>>> fig = Figure("orange") >>> fig.info() Figure Color: orange >>> rect = Rectangle(10, 20, "green") >>> rect.info() Rectangle Color: green Width: 10 Height: 20 Area: 200
Таким образом, класс наследник может расширять функционал класса родителя.
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Раздел: Python Уроки по Python Метки: Python, Уроки Python
Python. Урок 14. Классы и объекты : 20 комментариев
- Алексей 19.07.2018 А вот если Вы добавите вот это
.entry-title a:last-child float:right;
>
в свой css будет намного удобнее, нежели вы будите использовать 2-ную табуляцию в HTML. Спасибо.
- writer 04.12.2018 Спасибо за замечание! Добавим!
- Мария04.02.2021 круто.
- writer 13.08.2019 Да, это действительно ссылка на текущий объект. Нужно будет вообще этот урок переработать, в нем плохо раскрыты многие вопросы! Спасибо за комментарий!
- writer 02.09.2019 ОК, спасибо! Добавим!
Можно ли обратиться к безымянному экземпляру класса (например по id объекта) в Python?
Не совсем понимаю, как обратиться к безымянному экземпляру класса (например, при создании этого экземпляра в цикле). Как потом взаимодействовать с такими экземплярами (изменять атрибуты, применять к ним методы класса)? К примеру:
class Task: def __init__(self, day, month, year, title): self.day = day self.month = month self.year = year self.title = title for instance in range(6): x = 1 Task(day=x, month=3, year=2020, title='bla-bla') x += 1
Так, тут должны создаться 5 экземпляров класса Task с разными атрибутами day , но я не знаю как к ним обратиться, и как с ними взаимодействовать. Пожалуйста, подскажите, имеется ли какое-то простое решение
Отслеживать
задан 10 мая 2020 в 22:32
87 7 7 бронзовых знаков
В цикле добавляйте экземпляры класса в список.
10 мая 2020 в 22:49
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Чтоб обратиться к объекту — на него нужна ссылка.
Если ссылки нет — этот объект вскоре удалится.
Ссылку можно положить в переменную или в список или передать другой функции параметром.
Отслеживать
ответ дан 10 мая 2020 в 22:52
33.5k 3 3 золотых знака 26 26 серебряных знаков 60 60 бронзовых знаков
тогда не понимаю, как в цикле разным экземплярам присвоить разные переменные. Другими словами: как сделать для каждого экземпляра уникальную переменную со своим именем, которая хранила бы ссылку на определенный экземпляр созданного в цикле класса
10 мая 2020 в 23:05
@Kaderma, зачем заданиям уникальные имена переменных? Положите 6 заданий в список, обращайтесь к отдельным заданиям по индексу.
10 мая 2020 в 23:18
@insolor, пример с циклом я привел для упрощения, дабы не крепить огромную простыню кода, мало имеющего отношения к задаче. На самом деле у меня есть простенький графический интерфейс на ткинтер, где одни аргументы класса Task будут приниматься от параметров виджета (day, month, year), а другие вводится пользователем (title). Потом мне нужно будет отправлять полученные значения атрибутов в какую-нибудь БД и объявлять функцию для этого внутри самого класса Task . Поэтому мне и нужно обращаться к конкретному экземпляру, дабы иметь возможноть тут же применить к нему метод его класса.
10 мая 2020 в 23:50
@insolor, прошу прощения за свой поток сознания, надеюсь, что это возможно разобрать(поэтому я и решил на примере цикла вопрос сформулировать)
ООП в Python: классы и объекты

- Понятие класса и объекта
- Создание классов и объектов
- Доступ к атрибутам классов и объектов
- Статические методы и методы класса
- Инкапсуляция в классах
- Наследование классов
- Композиция классов
- Магические методы и перегрузка операторов
- Абстрактные классы и методы
- Конструкторы и деструкторы
- Краткие итоги параграфа
- Вопросы и задания для самоконтроля
Понятие класса и объекта
Понятия класса и объекта являются ключевыми в объектно-ориентированных языках программирования, к которым относится также и язык Python . В нем всё является объектами – и строки, и списки, и словари, и функции, и даже сами классы.
(от англ. class) – это шаблон кода, который используется для описания структуры и создания , т.е. экземпляров этого класса.
По сути классы в Python представляют собой типы данных, а объекты – отдельные экземпляры этих типов. Например, в инструкциях a = ‘abcdef’ и b = ‘ABCDEF’ переменным a и b присваиваются объекты строкового типа данных, т.е. экземпляры встроенного класса string .
Если говорить совсем уж просто, то класс можно сравнить с чертежом, по которому создаются объекты. Достаточно создать всего лишь один класс и далее можно будет порождать любое количество его экземпляров, изменяя и дополняя их по мере необходимости.
Благодаря использованию классов Python обладает всеми преимуществами абстрактного подхода в программировании. В частности Питону присущи:
- (от англ. polymorphism) – способность функций и методов обрабатывать данные разных типов. Простыми примерами полиморфизма могут служить: встроенная функция len(s) , которая может работать с разными типами встроенных последовательностей, возвращая количество их элементов; метод s.count(x) , который подсчитывает количество символов x в строке s (один тип данных) или, например, количество элементов x в списке s (другой тип данных); операторы сложения и умножения, которые для чисел (один тип данных) производят операции сложения и умножения, а для строк (другой тип данных) конкатенацию и повторение конкатенации указанное число раз.
- (от англ. encapsulation) – механизм, который позволяет объединять данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя. Грамотно написанный класс должен ограничивать доступность своих членов и взаимодействовать с пользователем только с помощью своего интерфейса. При этом нужно помнить, что в Python инкапсуляция работает лишь на уровне соглашения между программистами о том, какие атрибуты следует считать общедоступными, а какие – приватными (подробнее об этом мы поговорим чуть ниже).
- (от англ. inheritance) – еще одна концепция объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность с возможностью их изменения и добавления своих данных и функциональности. При этом в Python поддерживается возможность множественного наследования, когда подкласс наследует атрибуты сразу нескольких суперклассов одновременно. Таким образом, наследование способствует повторному использованию уже написанных компонентов программы, помогая избежать избыточности исходного кода.
- (от англ. composition) – возможность создания классов, включающих в себя вызовы уже существующих классов. В результате при создании объектов такого класса они будут состоять или содержать объекты других классов. Здесь важно не путать композицию с наследованием, в том числе множественным. Ведь при наследовании подкласс получает все атрибуты (т.е. возможности) своих суперклассов, а при композиции класс-агрегатор атрибуты не наследует, он просто создает объекты этих классов для использования, например, в качестве своих атрибутов или локальных переменных своих методов.
Благодаря всем этим преимуществам ООП у программистов появляется возможность параллельной разработки отдельных независимых модулей в виде классов, способных скрывать от внешнего мира детали своего внутреннего устройства и объединяющихся в масштабные комплексные приложения посредством предназначенных для этого интерфейсов. Данный процесс можно наглядно представить на примере из жизни, когда отдельные модули МКС собираются независимо друг от друга разными странами, а затем на орбите собираются в одну монолитную космическую станцию за счет имеющихся интерфейсов, обеспечивающих взаимодействие готовых модулей между собой.
Создание классов и объектов
Для создания классов в Python используется инструкция class , которая в общем виде может быть представлена в следующем формате:
class ClassName(SuperclassName_1, . SuperclassName_n): '''Документация класса ClassName.__doc__''' # Атрибут данных класса (доступен всем экземплярам). class_data = class_value # Конструктор класса (инициализация экземпляра). def __init__(self, init_arg_1, init_arg_2, . init_arg_n): # Атрибут данных конкретного экземпляра. self.init_obj_data = init_obj_value # Атрибут-метод (доступен всем экземплярам). def class_method(self, arg_1, arg_2, . arg_n): # Атрибут данных конкретного экземпляра. self.obj_data = obj_value
В заголовке инструкции сперва записывается служебное слово class , затем через пробел указывается имя класса, которое по принятому в Python соглашению обычно записывается в нотации CupWords , далее в скобках перечисляются имена наследуемых классов (их еще называют суперклассами) и завершается заголовок двоеточием. Если создаваемый класс не наследует других классов, заголовок инструкции разрешается записывать в формате class ClassName: , опуская круглые скобки и записывая двоеточие сразу после имени класса. В любом случае после заголовка записывается вложенный блок инструкций, в котором перечисляются атрибуты данных класса (по умолчанию они доступны для использования всеми создаваемыми экземплярами класса) и атрибуты-методы для обработки его данных, представляющие собой обычные функции, определенные внутри класса и принимающие в качестве первого аргумента параметр self , которому при вызове методов интерпретатор автоматически присваивает объект текущего экземпляра класса. При необходимости в начале тела класса разрешается указывать строку документирования класса, а также конструктор класса __init__() , представляющий собой специальный метод класса, который вызывается автоматически всякий раз при создании нового экземпляра, инициализируя его исходными данными. Изменение имени конструктора запрещено.
После того, как класс будет определен, можно приступать к созданию его отдельных экземпляров, сохраняя их в переменных или структурах данных. Для этого нужно после имени класса просто указать круглые скобки, перечислив в них начальные данные для инициализации создаваемого экземпляра (см. пример №1 ).
Код Результат pythonCodes
# Определяем обычный класс (не наследует других классов). class Person: '''Компания «Okpython» и ее сотрудники.''' # Создаем атрибут данных класса (доступен всем экземплярам). company = '«Okpython»' # Конструктор класса инициализирует экземпляр начальными данными. def __init__(self, name='Немо'): # Создаем атрибут данных экземпляра. self.name = name # Определяем 1-й метод (self - текущий экземпляр). def set_date(self, date): # Создаем еще один атрибут данных экземпляра. self.date = date # Определяем 2-й метод. def get_date(self): # Используем атрибут данных экземпляра. return self.date # Создаем 1-й экземпляр класса. person_1 = Person('Коля') # Извлекаем имя сотрудника. name_1 = person_1.name # Устанавливаем дату начала работы в компании. # Можно и так: person_1.date = '30.05.2022'. person_1.set_date('30.05.2022') # Извлекаем дату для использования. # Можно и так: date_1 = person_1.date date_1 = person_1.get_date() # Для 2-го экземпляра используем имя по умолчанию. person_2 = Person() # Извлекаем имя сотрудника. name_2 = person_2.name # Устанавливаем дату начала работы в компании. # Можно и так: person_2.date = '07.03.2021'. person_2.set_date('07.03.2021') # Извлекаем дату для использования. # Можно и так: date_2 = person_2.date date_2 = person_2.get_date() # Извлекаем название компании из объекта класса. # Можно и так: person_1.company или person_2.company. company = Person.company # Выводим на экран документацию. print(Person.__doc__, end='\n\n') # Выводим данные на экран. print('В компании', company, 'работают:') print(name_1, 'с', date_1) print(name_2, 'с', date_2)
Компания «Okpython» и ее сотрудники. В компании «Okpython» работают: Коля с 30.05.2022 Немо с 07.03.2021
Пример №1. Создание классов и объектов в Python.
В нашем примере мы создали класс Person не наследуя никаких других классов, поэтому круглые скобки в заголовке были опущены ( class Person: ). В качестве строки документации мы указали обычную строку в тройных одинарных кавычках, после чего создали атрибут данных класса, присвоив ему название нашей компании ( company = ‘«Okpython»’ ). Этот атрибут стал доступен как из любого экземпляра класса ( person_1.company или person_2.company ), так и напрямую из объекта самого класса ( Person.company ). Далее был определен конструктор класса ( def __init__(self, name=’Немо’): ), который затем при создании экземпляров класса запустился в автоматическом режиме и создал атрибуты данных создаваемых экземпляров (это атрибуты созданных объектов person_1.name и person_2.name ). Тут важно помнить, что наличие в определении класса конструктора с параметрами влечет за собой необходимость в передаче вновь создаваемым экземплярам начальных данных для их инициализации, иначе будет получена ошибка (мы подстраховались, использовав аргумент со значением по умолчанию). В конце тела класса мы добавили еще два метода для установки и получения атрибута данных экземпляров класса ( def set_date(self, date): и def get_date(self): ). Как и говорилось, в качестве первого аргумента у методов был задан параметр self , которому при каждом вызове этих методов интерпретатор автоматически присвоил объекты текущих экземпляров класса (т.е. объекты person_1 и person_2 ).
После создания класса Person мы создали первый экземпляр этого класса, использовав инструкцию вызова объекта класса person_1 = Person(‘Коля’) и указав в качестве начальных данных для нашего конструктора значение аргумента ‘Коля’ . Далее, для доступа к атрибутам созданного объекта мы использовали синтаксис с точкой, сохранив в переменной глобальной области видимости имя сотрудника ( name_1 = person_1.name ), а также установив и затем также сохранив в переменной дату трудоустройства сотрудника в компанию (инструкции person_1.set_date(‘30.05.2022’) и date_1 = person_1.get_date() ). Проделав тоже самое для второго экземпляра, мы сохранили в глобальной переменной название компании, использовав атрибут данных класса ( company = Person.company ), а затем вывели на экран строку документирования класса и данные сотрудников.
Таким образом, процедура создания класса в простейшем случае сводится к объявлению имени класса ClassName в заголовке инструкции class и перечислению в теле класса его атрибутов, состоящих из атрибутов данных и атрибутов-методов. Что касается создания экземпляров класса, то делается это по аналогии с вызовами обычной функции, т.е. при помощи пары круглых скобок после имени класса в формате ClassName(arg_1, arg_2, . arg_n) . После создания экземпляра класса можно начинать пользоваться объявленными атрибутами: получать к ним доступ, изменять, удалять или же добавлять новые атрибуты.
Доступ к атрибутам классов и объектов
Доступ к открытому атрибуту вне класса или объекта может быть получен с помощью имени этого атрибута, указанного через точку после имени класса или объекта в формате obj.attr . Для добавления нового атрибута или изменения значения уже существующего используется привычная нам инструкция присваивания obj.new_attr = value или, соответственно, obj.attr = new_value , а удаление осуществляется инструкцией del obj.attr (см. пример №2 ).
Код Результат pythonCodes
# Создаем простой класс. class ExampleClass: # Устанавливаем атрибут данных экземпляра. def __init__(self): self.attr_0 = 0.0 # Создаем 1-й экземпляр класса ExampleClass. obj_1 = ExampleClass() # Создаем 2-й экземпляр класса ExampleClass. obj_2 = ExampleClass() # Выводим значение атрибута данных экземпляра. print('obj_1.attr_0 ->', obj_1.attr_0) # Также выведет 0.0. print('obj_2.attr_0 ->', obj_2.attr_0, end='\n\n') # Изменяем значение атрибута в 1-м объекте. obj_1.attr_0 = 1.1 # Выведет 1.1. print('obj_1.attr_0 ->', obj_1.attr_0) # Выведет 0.0, т.к. во 2-м объекте атрибут не изменялся. print('obj_2.attr_0 ->', obj_2.attr_0, end='\n\n') # Добавляем новый атрибут 1-му экземпляру. obj_1.attr_1 = 1.2 # Выведет True. print("hasattr(obj_1, 'attr_1') ->", hasattr(obj_1, 'attr_1')) # Выведет 1.2. print('obj_1.attr_1 ->', obj_1.attr_1, end='\n\n') # Такой атрибут во втором объекте отсутствует. # 'ExampleClass' object has no attribute 'attr_1'. # print('obj_2.attr_1 ->', obj_2.attr_1) # Удаляем атрибут 1-го экземпляра. del obj_1.attr_1 # Выведет False, т.к. атрибут был удален. print("hasattr(obj_1, 'attr_1') ->", hasattr(obj_1, 'attr_1')) # 'ExampleClass' object has no attribute 'attr_1'. # print('obj_1.attr_1 ->', obj_1.attr_1)
obj_1.attr_0 -> 0.0 obj_2.attr_0 -> 0.0 obj_1.attr_0 -> 1.1 obj_2.attr_0 -> 0.0 hasattr(obj_1, 'attr_1') -> True obj_1.attr_1 -> 1.2 hasattr(obj_1, 'attr_1') -> False
Пример №2. Атрибуты классов и объектов в Python (часть 1).
- – функция проверяет наличие в объекте object атрибута с именем name , возвращая True в случае успеха и False в противном случае. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута (см. пример №3 ).
- – возвращает значение атрибута name переданного объекта object , если он существует. В противном случае функция возвращает значение по умолчанию default , если оно было передано, или возбуждает исключение AttributeError . В качестве обязательных аргументов функции передаются объект и строка с именем искомого в нем атрибута.
- – устанавливает новое значение value атрибуту с именем name переданного объекта object , если он существует. В противном случае интерпретатор создает новый атрибут с указанным именем и присваивает ему переданное значение. В качестве первых двух аргументов функции передаются объект и строка с именем искомого в нем атрибута.
- – удаляет из объекта object атрибут с именем name , если объект позволяет это сделать. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута.
Код Результат pythonCodes
# Создаем простой класс. class ExampleClass: # Устанавливаем атрибут данных экземпляра. def __init__(self): setattr(self, 'attr_0', 0.0) # Создаем 1-й экземпляр класса ExampleClass. obj_1 = ExampleClass() # Создаем 2-й экземпляр класса ExampleClass. obj_2 = ExampleClass() # Выводим значение атрибута данных экземпляра. print("getattr(obj_1, 'attr_0') ->", getattr(obj_1, 'attr_0')) # Также выведет 0.0. print("getattr(obj_2, 'attr_0') ->", getattr(obj_2, 'attr_0'), end='\n\n') # Изменяем значение атрибута в 1-м объекте. setattr(obj_1, 'attr_0', 1.1) # Выведет 1.1. print("getattr(obj_1, 'attr_0') ->", getattr(obj_1, 'attr_0')) # Выведет 0.0, т.к. во 2-м объекте атрибут не изменялся. print("getattr(obj_2, 'attr_0') ->", getattr(obj_2, 'attr_0'), end='\n\n') # Добавляем новый атрибут 1-му экземпляру. setattr(obj_1, 'attr_1', 1.2) # Выведет True. print("hasattr(obj_1, 'attr_1') ->", hasattr(obj_1, 'attr_1')) # Выведет 1.2. print("getattr(obj_1, 'attr_1') ->", getattr(obj_1, 'attr_1'), end='\n\n') # Такой атрибут во втором объекте отсутствует. # 'ExampleClass' object has no attribute 'attr_1'. # print("getattr(obj_2, 'attr_1') ->", getattr(obj_2, 'attr_1')) # Удаляем атрибут 1-го экземпляра. delattr(obj_1, 'attr_1') # Выведет False, т.к. атрибут был удален. print("hasattr(obj_1, 'attr_1') ->", hasattr(obj_1, 'attr_1')) # 'ExampleClass' object has no attribute 'attr_1'. # print("getattr(obj_1, 'attr_1') ->", getattr(obj_1, 'attr_1'))
getattr(obj_1, 'attr_0') -> 0.0 getattr(obj_2, 'attr_0') -> 0.0 getattr(obj_1, 'attr_0') -> 1.1 getattr(obj_2, 'attr_0') -> 0.0 hasattr(obj_1, 'attr_1') -> True getattr(obj_1, 'attr_1') -> 1.2 hasattr(obj_1, 'attr_1') -> False
Пример №3. Функции для работы с атрибутами классов и объектов.
Здесь важно отметить один нюанс – при обращении к атрибуту объекта, созданного на основе классов, интерпретатор производит поиск этого атрибута сначала в самом экземпляре, а затем, при отсутствии такового, во всех классах, расположенных выше в дереве наследования (см. пример №4 ).
Код Результат pythonCodes
# Создаем класс без методов. class ExampleClass: # Атрибут данных класса (доступен всем экземплярам). attr_0 = 0 # Создаем 1-й экземпляр класса ExampleClass. obj_1 = ExampleClass() # Выведет 0, т.е. значение атрибута класса. print('obj_1.attr_0 ->', obj_1.attr_0) # Определяем (изменяем) значение атрибута в экземпляре. obj_1.attr_0 = 1 # Выведет 1, т.е. значение атрибута этого экземпляра. print('obj_1.attr_0 ->', obj_1.attr_0) # Удаляем атрибут attr_0 экземпляра (но не класса). del obj_1.attr_0 # Выведет True, т.к. интерпретатор совершит поиск атрибута # в дереве наследования и найдет его в объекте класса. print("hasattr(obj_1, 'attr_0') ->", hasattr(obj_1, 'attr_0')) # Выведет 0, т.е. значение атрибута класса. print('obj_1.attr_0 ->', obj_1.attr_0) # Получим ошибку, т.к. удалять в объекте нечего. # del obj_1.attr_0
obj_1.attr_0 -> 0 obj_1.attr_0 -> 1 hasattr(obj_1, 'attr_0') -> True obj_1.attr_0 -> 0
Пример №4. Атрибуты классов и объектов в Python (часть 2).
Как видим, даже при отсутствии атрибута непосредственно в самом экземпляре, интерпретатор не возбуждает ошибку, а сперва пытается отыскать его в самом классе. Поэтому даже после удаления атрибута attr_0 в экземпляре obj_1 при повторной попытке обращения к нему через obj_1.attr_0 интерпретатор выдал не ошибку, а вернул значение общедоступного атрибута, найденного в объекте класса ExampleClass .
Получить доступ или посмотреть текущий набор атрибутов класса или экземпляра можно при помощи словаря атрибутов object.__dict__, который динамически изменяется в процессе изменения как количества доступных атрибутов, так и их значений (см. пример №5 ).
Код Результат pythonCodes
# Создаем класс. class A: # Атрибут данных класса. attr_1 = 1 # Атрибут-метод. def set_attr(self): self.attr_2 = 2 # Словарь с атрибутами объекта класса. print('Атрибуты класса:', A.__dict__) # Создаем экземпляр класса A. obj = A() # Словарь с атрибутами экземпляра. print('Атрибуты экземпляра:', obj.__dict__) # Установим атрибут экземпляру. obj.set_attr() # Проверим словарь. print('Атрибуты экземпляра:', obj.__dict__) # Изменим значение атрибута. obj.attr_2 = 2.1 # Добавим еще один атрибут. obj.attr_3 = 3 # Проверим словарь. print('Атрибуты экземпляра:', obj.__dict__) # Изменим значение атрибута через словарь. obj.__dict__['attr_3'] = 3.1 # Удалим атрибут через словарь. del obj.__dict__['attr_2'] # Проверим словарь. print('Атрибуты экземпляра:', obj.__dict__) # Выведем измененное значение атрибута. print(obj.__dict__['attr_3']) # Но обычный способ проще. print(obj.attr_3)
Атрибуты класса: Атрибуты экземпляра: <> Атрибуты экземпляра: Атрибуты экземпляра: Атрибуты экземпляра: 3.1 3.1
Пример №5. Атрибуты классов и объектов в Python (часть 3).
Стоит добавить, что поскольку в Python объекты присутствуют повсюду, атрибут __dict__ имеется не только у классов, но и у других объектов, например, объектов функций.
Статические методы и методы класса
Как было показано выше, обычные атрибуты-методы классов в момент своего вызова в качестве первого аргумента автоматически получают экземпляр класса, посредством которого они вызываются. Такие методы формально называют , поскольку они подразумевают воздействие на объекты экземпляров класса. Однако в Python присутствуют еще две разновидности методов: , которые при вызове в качестве первого аргумента вместо объекта экземпляра автоматически получают объект самого класса, а также , которые представляют собой обычные функции, определенные внутри класса и не требующие передачи экземпляра класса в качестве первого аргумента. Для получения таких методов служат встроенные функции classmethod(method) и staticmethod(method), которые необходимо вызывать внутри классов. Обе функции помечают переданный объект метода method как специальный, т.е. требующий передачи объекта самого класса для метода класса и, соответственно, не требующий передачи экземпляра класса для статического метода (см. пример №6 ).
Код Результат pythonCodes
# Определяем класс. class Cls: # Обычный метод экземпляра. def object_method(self): print('Object') # Метод класса. def class_method(cls): print('Class') # Статический метод. def static_method(): print('Static') # Теперь под cls подразумевается объект # самого класса Cls, а не его экземпляра. class_method = classmethod(class_method) # Теперь метод при вызове не будет # автоматически запрашивать экземпляр. static_method = staticmethod(static_method) # Создаем экземпляр класса. obj = Cls() # Методу экземпляра в качестве первого аргумента # автоматически передается экземпляр класса. obj.object_method() # Или передаем вручную при вызове из класса. Cls.object_method(obj) # Метод класса автоматически получает объект # самого класса Cls вместо его экземпляра obj. obj.class_method() # При вызове из класса передавать его не нужно. Cls.class_method() # Статический метод вызывается как обычная ф-ция, # ничего не знающая ни о классе, ни о его экз-ре. obj.static_method() Cls.static_method()
Object Object Class Class Static Static
Пример №6. Виды методов в классах (часть 1).
Объявить статический метод или метод класса можно более элегантно, если учесть, что обе функции classmethod(method) и staticmethod(method) представляют собой не что иное, как готовые встроенные декораторы. Все что нужно, это использовать синтаксис декораторов, записав перед определениями нужных нам методов строки @classmethod и @staticmethod (см. пример №7 ).
Код Результат pythonCodes
# Определяем класс. class Cls: # Обычный метод экземпляра. def object_method(self): print('Object') @classmethod def class_method(cls): print('Class') @staticmethod def static_method(): print('Static') # Создаем экземпляр класса. obj = Cls() # Вызываем методы. obj.object_method() Cls.object_method(obj) obj.class_method() Cls.class_method() obj.static_method() Cls.static_method()
Object Object Class Class Static Static
Пример №7. Виды методов в классах (часть 2).
Следует помнить, что методы экземпляра предназначены для обработки данных каждого отдельного экземпляра класса, в то время как методы класса призваны обрабатывать данные самого класса, являющиеся общими сразу для всех экземпляров этого класса. Например, методы класса могут использоваться для подсчета созданных экземпляров класса (см. пример №8 ), вести список экземпляров, находящихся в данный момент в оперативной памяти, изменять существующие или устанавливать новые атрибуты-данных класса. Кроме того, если первый аргумент методов экземпляра при определении принято обозначать через self и его нужно передавать явным образом при вызове через класс, то для первого аргумента методов класса принято использовать имя cls , не указывая его явным образом как при вызове из экземпляров, так и вызове через сам класс (еще раз посмотрите пример №7 ).
Что касается статических методов, то они как и методы класса предназначены для работы с атрибутами класса, а не экземпляра. Но при этом они не предполагают автоматической передачи в качестве первого аргумента объекта класса. Поэтому статические методы могут быть использованы как самые обычные функции, выполняющие какие-либо полезные действия так или иначе связанные с данными класса и определенные в его пространстве имен для более тесной связи с классом, а также уменьшения вероятности конфликта имен в глобальной области видимости. Более того, их можно даже не объявлять статическими при помощи функции staticmethod(method) . В таком случае они останутся доступны посредством объекта класса, но станут недоступны для вызовов из экземпляров класса.
Код Результат pythonCodes
# Определяем класс. class Cls: # Атрибут данных класса используем # как счетчик экземпляров, созданных # за все время работы программы. num_obj = 0 # Конструктор класса. def __init__(self): # Вызывается каждый раз при # создании нового экземпляра. self.increase_counter() @classmethod def increase_counter(cls): # +1 экземпляр. cls.num_obj += 1 # Просто функция для вывода. @staticmethod def print_num_obj(): print(Cls.num_obj) # Создаем экземпляры класса, # попутно выводя кол-во экземпляров. obj1 = Cls() Cls.print_num_obj() obj2 = Cls() Cls.print_num_obj() obj3 = Cls() # Статический метод можно вызывать # также из любого экземпляра. obj3.print_num_obj()
1 2 3
Пример №8. Виды методов в классах (часть 3).
Конечно, мы могли бы определить функцию print_num_objects() и вне класса, она по-прежнему выполняла бы свою работу, но, повторимся, тогда она выглядела бы обособленной от класса (особенно для других программистов, читающих наш код), нарушая при этом принцип инкапсуляции и напрасно захломляя глобальное пространство имен модуля.
Инкапсуляция в классах
По умолчанию все атрибуты и методы класса являются открытыми или публичными (от англ. public). Это значит, что они доступны не только внутри класса, но и за его пределами. Для этого достаточно использовать синтаксис доступа к атрибутам при помощи точки (например, obj.attr ) или предназначенные для этого встроенные функции (например, getattr(obj, ‘attr’) ). Однако бывают случаи, когда требуется ограничить прямой доступ к атрибутам классов из вызывающего кода воизбежание передачи им некорректных значений или случайного удаления. Примером может служить возраст человека, который не может быть отрицательным, или значение атрибута, предназначенного для служебного пользования. В таких случаях вполне логично делать атрибуты закрытыми или приватными (от англ. private), предоставляя доступ к ним посредством интерфейсов взаимодействия в виде специальных методов, осуществляющих необходимые проверки передаваемых им значений. При этом следует помнить, что в Python инкапсуляция в классах (т.е. сокрытие данных) поддерживается лишь на уровне соглашения между программистами. Полностью скрыть реализацию класса от целенаправленных злонамеренных действий извне не получится.
Так атрибуты, предназначенные для внутреннего использования, принято начинать с одного символа подчеркивания (например, _var ). Встретив такой атрибут, другой программист будет иметь в виду, что он не предназначен для публичного использования и в дальнейшем может быть даже удален без предварительного уведомления. В тоже время никто не запрещает изменять такой атрибут на свой страх и риск, надеясь, что это не повлияет на работоспособность класса (см. пример №9 ).
Код Результат pythonCodes
# Определяем класс числа Пи. class NumPi: # Атрибут для служебного пользования. # Точность числа Пи по умолчанию. _pi = 3.141592 # Атрибут-метод экземпляра. def round_pi(self, d): # Уст-вает экз-ру требуемую точностью Пи. self._pi = round(NumPi._pi, d) # Создаем экземпляр класса. pi_obj = NumPi() # Атрибут доступен для чтения по имени. print('pi =', pi_obj._pi) # Установим требуемую точность. pi_obj.round_pi(3) print('pi =', pi_obj._pi) # Атрибут доступен и для изменения. pi_obj._pi = 0 # Не стоило этого делать. print('pi =', pi_obj._pi)
pi = 3.141592 pi = 3.142 pi = 0
Пример №9. Инкапсуляция в классах (часть 1).
Если нужна большая защищенность атрибута, нужно указывать в начале его имени два знака подчеркивания в формате __attr_name . В результате такой манипуляции атрибут останется доступен внутри класса, но станет недоступен по такому имени за его пределами, т.к. вне класса имена атрибутов, начинающихся с двух знаков подчеркивания, автоматически преобразуются интерпретатором из формата __attr_name в формат _Class__attr_name (см. пример №10 ).
Код Результат pythonCodes
# Определяем обычный класс. class Person: # Обычный открытый атрибут данных класса. company = '«Okpython»' # Открытый (публичный) атрибут-метод. def set_name(self, name): # Создаем атрибут данных экземпляра. self.name = name # Открытый (публичный) атрибут-метод. def get_name(self): # Используем атрибут данных экземпляра. return self.name # Закрытый (приватный) атрибут-метод. def __check_age(self, age): # Проверяем возраст сотрудника. if 18 < age < 75: # Создаем атрибут данных экземпляра. self.__age = age else: # Выводим предупреждение. print('Недопустимый возраст!') # Открытый метод. def set_age(self, age): # Внутри класса и объектов метод доступен. self.__check_age(age) # Открытый метод. def get_age(self): # Используем атрибут данных экземпляра. return self.__age # Создаем экземпляр класса. person = Person() # Добавляем имя сотрудника. person.set_name('Егор') # Добавляем возраст сотрудника. person.set_age(25) # Получаем имя сотрудника. print('Имя сотрудника:', person.get_name()) # Тоже самое, т.к. атрибут открыт. print('Имя сотрудника:', person.name, end='\n\n') # Получаем возраст сотрудника. print('Возраст сотрудника:', person.get_age()) # 'Person' object has no attribute '__age'. # print(person.__age) # 'Person' object has no attribute '__check_age'. # person.__check_age() # Но всегда можно использовать полное имя. print('Возраст сотрудника:', person._Person__age)
Имя сотрудника: Егор Имя сотрудника: Егор Возраст сотрудника: 25 Возраст сотрудника: 25
Пример №10. Инкапсуляция в классах (часть 2).
Как видим, атрибуты, в именах которых присутствует приставка с двумя символами подчеркивания, действительно недоступны напрямую по своему имени за пределами класса. Тем не менее, они также не обеспечивают настоящего сокрытия данных, т.к. зная имя вмещающего класса всегда можно обратиться к таким атрибутам по их расширенному имени в формате _Class__attr_name из любой точки программы, где имеется ссылка на экземпляр класса.
- fget – объект функции, которая будет вызываться при попытке прочитать значение атрибута и возвращать вычисленное значение атрибута;
- fset – объект функции, которая будет вызываться при попытке выполнить операцию присваивания;
- fdel – объект функции, которая будет вызываться при попытке удаления атрибута;
- doc – строка документирования с описанием атрибута, если это необходимо.
Результатом вызова функции property является объект свойства, присваиваемый имени атрибута, которым требуется управлять и который будет находиться в области видимости класса и наследоваться всеми его экземплярами (см. пример №11 ).
Код Результат pythonCodes
# Определяем обычный класс. class Age: # Вызывается при попытке обращения к атрибуту. def get_age(self): # Возвращаем значение возраста. return self._age # Вызывается при попытке установки атрибута. def set_age(self, age): # Проверяем возраст сотрудника. if 18 < age < 75: # Создаем атрибут данных экземпляра. self._age = age else: # Выводим предупреждение. print('Недопустимый возраст!') # Вызывается при попытке удаления атрибута. def del_age(self): # Просто выводим предупреждение. print('Удаление невозможно!') # Документация управляемого атрибута. s_age = 'Удаление запрещено!' # Организуем управление атрибутом. age = property(get_age, set_age, del_age, doc=s_age) # Создаем экземпляр класса. person_age = Age() # Вызовет set_age и выведет предупреждение. person_age.age = 100 # А здесь все в норме. person_age.age = 25 # Можно по-прежнему напрямую. person_age.set_age(25) # Вызовет get_age. print('Возраст:', person_age.age) # Или можно вызвать напрямую. print('Возраст:', person_age.get_age()) # Пытаемся удалить атрибут. del person_age.age
Недопустимый возраст! Возраст: 25 Возраст: 25 Удаление невозможно!
Пример №11. Инкапсуляция в классах (часть 3).
В нашем примере мы создали класс с управляемым атрибутом age . Благодаря этому, при каждой попытке обращения к нему по имени person_age.age будет вызываться один из предназначенных для его управления методов. Именно поэтому атрибут остался доступен для чтения, но стал недоступен для удаления. При этом заметьте, что по факту, воизбежание конфликта имен, мы сохранили данные в атрибуте с именем _age , т.к. само имя age было зарезервировано для хранения объекта свойства, возвращаемого функцией property .
Если необходимо запретить изменение значения атрибута или даже сделать его полностью закрытым для доступа из кода за пределами класса, нужно всего лишь изменить поведение отвечающих за это методов, переданных функции property в качестве аргументов (см. пример №12 ).
Код Результат pythonCodes
# Определяем класс числа Пи. class NumPi: # Атрибут для служебного пользования. # Точность числа Пи по умолчанию. _pi = 3.141592 # Передаем требуемую точность для экз-ра. def __init__(self, d): # Уст-вает экз-ру требуемую точностью Пи. self._pi = round(NumPi._pi, d) # Вызывается при попытке обращения к атрибуту. def get_pi(self): # Возвращаем значение Пи. return self._pi # Вызывается при попытке установки атрибута. def set_pi(self, pi_val): # Выводим предупреждение. print('Изменение запрещено!') # Вызывается при попытке удаления атрибута. def del_pi(self): # Просто выводим предупреждение. print('Удаление невозможно!') # Документация управляемого атрибута. s_pi = 'Только чтение!' # Организуем управление атрибутом. pi = property(get_pi, set_pi, del_pi, doc=s_pi) # Создаем экземпляр класса. pi_obj = NumPi(3) # Атрибут доступен для чтения. print('pi =', pi_obj.pi) # Но недоступен для изменения. pi_obj.pi = 0 # И удаления. del pi_obj.pi
pi = 3.142 Изменение запрещено! Удаление невозможно!
Пример №12. Инкапсуляция в классах (часть 4).
Следует добавить, что объект свойства, возвращаемый функцией property обладает атрибутами-методами getter(get_method), setter(set_method) и deleter(del_method), которые позволяют определить соответствующие методы для обработки обращений к управляемому атрибуту. При этом первый метод практически никогда не используется, т.к. вызов функции property() уже подразумевает его передачу в качестве первого аргумента (см. пример №13 ).
Код Результат pythonCodes
# Определяем класс числа Пи. class NumPi: # Атрибут для служебного пользования. # Точность числа Пи по умолчанию. _pi = 3.141592 # Передаем требуемую точность для экз-ра. def __init__(self, d): # Уст-вает экз-ру требуемую точностью Пи. self._pi = round(NumPi._pi, d) # Вызывается при попытке обращения к атрибуту. def pi(self): # Возвращаем значение Пи. return self._pi # Вызывается при попытке установки атрибута. def set_pi(self, pi_val): # Выводим предупреждение. print('Изменение запрещено!') # Вызывается при попытке удаления атрибута. def del_pi(self): # Просто выводим предупреждение. print('Удаление невозможно!') # Документация управляемого атрибута. s_pi = 'Только чтение!' # Организуем управление атрибутом. pi = property(pi, doc=s_pi) pi = pi.setter(set_pi) pi = pi.deleter(del_pi) # Выводим документацию. print(NumPi.pi.__doc__) # Создаем экземпляр класса. pi_obj = NumPi(3) # Атрибут доступен для чтения. print('pi =', pi_obj.pi) # Но недоступен для изменения. pi_obj.pi = 0 # И удаления. del pi_obj.pi
Только чтение! pi = 3.142 Изменение запрещено! Удаление невозможно!
Пример №13. Инкапсуляция в классах (часть 5).
Опять же, весь процесс можно осуществить при помощи декораторов, которые делают код более простым и приятным для восприятия (см. пример №14 ).
Код Результат pythonCodes
# Определяем класс числа Пи. class NumPi: # Атрибут для служебного пользования. # Точность числа Пи по умолчанию. _pi = 3.141592 # Передаем требуемую точность для экз-ра. def __init__(self, d): # Уст-вает экз-ру требуемую точностью Пи. self._pi = round(NumPi._pi, d) # Аналог pi = property(pi, doc='Чтение!'). @property # Вызывается при попытке обращения к атрибуту. def pi(self): '''Чтение!''' # Возвращаем значение Пи. return self._pi # Аналог инструкции pi = pi.setter(pi). @pi.setter # Вызывается при попытке установки атрибута. def pi(self, pi_val): # Выводим предупреждение. print('Изменение запрещено!') # Аналог инструкции pi = pi.deleter(pi). @pi.deleter # Вызывается при попытке удаления атрибута. def pi(self): # Просто выводим предупреждение. print('Удаление невозможно!') # Выводим документацию. print('О числе:', NumPi.pi.__doc__) # Создаем экземпляр класса. pi_obj = NumPi(3) # Атрибут доступен для чтения. print('pi =', pi_obj.pi) # Но недоступен для изменения. pi_obj.pi = 0 # И удаления. del pi_obj.pi
О числе: Чтение! pi = 3.142 Изменение запрещено! Удаление невозможно!
Пример №14. Инкапсуляция в классах (часть 6).
Обратите внимание, что при использовании декораторов все методы должны иметь одно и то же имя, иначе интерпретатор возбудит исключение.
Наследование классов
Как упоминалось в начале параграфа, наследование позволяет создавать новый класс, называемый , на основе уже существующего класса, называемого . Более того, в Python поддерживается механизм множественного наследования, при котором подкласс может наследовать атрибуты и методы сразу нескольких суперклассов. Все что нужно, это перечислить наследуемые суперклассы в порядке их приоритета в скобках в заголовке инструкции class (см. пример №15 ).
Код Результат pythonCodes
# Создаем 1-й суперкласс. class Super_1: # Атрибут данных класса. attr_0 = 0.1 # Атрибут данных класса. attr_1 = 1 # Конструктор 1-го суперкласса. def __init__(self): self.p_1 = '«Конструктор 1-го суперкласса.»' # Атрибут-метод в 1-м суперклассе. def method_1(self): print('«method_1»') # Создаем 2-й суперкласс. class Super_2: # Атрибут данных класса. attr_0 = 0.2 # Атрибут данных класса. attr_2 = 2 # Конструктор 2-го суперкласса. def __init__(self): self.p_2 = '«Конструктор 2-го суперкласса.»' # Атрибут-метод во 2-м суперклассе. def method_2(self): print('«method_2»') # Создаем подкласс, наследуя оба суперкласса. class Sub_3(Super_1, Super_2): # Атрибут данных подкласса. attr_3 = 3 # Атрибут-метод в подклассе. def method_3(self): print('«method_3»') # Создаем экземпляр подкласса Sub_3. obj = Sub_3() # Выведет 1 (атрибут унаследован от 1-го суперкласса). print('obj.attr_1 ->', obj.attr_1) # Выведет 2 (атрибут унаследован от 2-го суперкласса). print('obj.attr_2 ->', obj.attr_2) # Выведет 3 (значение собственного атрибута). print('obj.attr_3 ->', obj.attr_3) # Выведет 0.2, т.к. атрибут был найден в скобках в 1-м # суперклассе (поиск в скобках идет слева направо). print('obj.attr_0 ->', obj.attr_0) # Выведет «method_1» (метод унасл. от 1-го суперкласса). obj.method_1() # Выведет «method_2» (метод унасл. от 2-го суперкласса). obj.method_2() # Выведет «method_3» (собственный метод). obj.method_3() # Выведет «Конструктор 1-го суперкласса.», т.к. # конструктор был найден в скобках в 1-м суперклассе. print('obj.p_1 ->', obj.p_1) # 'Sub_3' object has no attribute 'p_2', т.к. # конструктор 2-го суперкласса не вызывался. print('obj.p_2 ->', obj.p_2)
obj.attr_1 -> 1 obj.attr_2 -> 2 obj.attr_3 -> 3 obj.attr_0 -> 0.1 «method_1» «method_2» «method_3» obj.p_1 -> «Конструктор 1-го суперкласса.» 'Sub_3' object has no attribute 'p_2'
Пример №15. Наследование классов в Python (часть 1).
В нашем примере подкласс Sub_3 наследует атрибуты сразу двух суперклассов Super_1 и Super_2 . Однако поскольку оба суперкласса обладают атрибутом с именем attr_0 , интерпретатор выбрал для подкласса значение атрибута суперкласса Super_1 , т.к. при наследовании поиск атрибутов ведется сперва в самом подклассе, а затем в списке наследуемых суперклассов слева направо. Именно поэтому подкласс Sub_3 унаследовал конструктор первого суперкласса. Если бы нам понадобилось отдать приоритет атрибутам второго суперкласса, его пришлось бы перечислить в скобках первым.
Помимо того, что подклассы наследуют данные и методы своих суперклассов, они могут определять и свои собственные атрибуты. Так в подклассе Sub_3 были определены собственный атрибут данных attr_3 и метод method_3 . Но и это еще не все. При необходимости подклассы могут изменять (специализировать) наследуемые атрибуты или даже полностью замещать их (см. пример №16 ).
Код Результат pythonCodes
# Создаем суперкласс. class SuperClass: # Атрибут данных класса. attr_1 = 1 # Конструктор суперкласса. def __init__(self): self.p_1 = '«Конструктор суперкласса.»' # 1-й метод в суперклассе. def method_1(self): print('«method_1 в суперклассе»') # 2-й метод в суперклассе. def method_2(self): print('«method_2»') # Создаем подкласс, наследуя суперкласс. class SubClass(SuperClass): # Меняем значение атрибута в подклассе. attr_1 = 2 # Определяем собственный атрибут данных подкласса. attr_3 = 3 # Переопределяем (расширяем) конструктор. def __init__(self): # Вызываем конструктор суперкласса. SuperClass.__init__(self) # Добавляем для экземпляра свой атрибут. self.p_2 = '«Конструктор подкласса.»' # Переопределяем (расширяем) метод суперкласса. def method_2(self): # Вызываем исходный метод суперкласса. SuperClass.method_2(self) # Добавляем собственную функциональность. print('«method_2+»') # Собственный метод в подклассе. def method_3(self): print('«method_3 в подклассе»') # Создаем экземпляр подкласса SubClass. obj = SubClass() # Выведет 2, т.к. атрибут был переопределен в подклассе. print('obj.attr_1 ->', obj.attr_1) # Выведет «Конструктор суперкласса.». print('obj.p_1 ->', obj.p_1) # Выведет «Конструктор подкласса.». print('obj.p_2 ->', obj.p_2) # «method_1 суперкласса» (метод унасл. от суперкласса). obj.method_1() # «method_2» и «method_2+» (метод расширен в подклассе). obj.method_2() # «method_3 подкласса» (собственный метод). obj.method_3()
obj.attr_1 -> 2 obj.p_1 -> «Конструктор суперкласса.» obj.p_2 -> «Конструктор подкласса.» «method_1 в суперклассе» «method_2» «method_2+» «method_3 в подклассе»
Пример №16. Наследование классов в Python (часть 2).
Следует заметить, что в ходе специализации методов в подклассах внутри замещающего метода обычно предусматривается вызов исходной версии метода наследуемого суперкласса. Это позволяет выполнить предусмотренный в суперклассе набор действий по умолчанию без необходимости повторного написания уже имеющегося кода. Так в подклассе SubClass нашего примера мы расширили конструктор и метод method_2 суперкласса, вызвав исходные версии методов вместо того, чтобы заново переписывать их код.
Кстати, доступ к оригинальным методам может быть получен не только напрямую через имя суперкласса, но и через встроенную функцию super([type[, object-or-type]]), возвращающую специальный объект-посредник, делегирующий вызовы метода суперклассу указанного типа type . В качестве необязательных аргументов она принимает тип (т.е. объект подкласса), для которого будет производиться поиск суперкласса, а также конкретный объект экземпляра подкласса или объект самого подкласса, для которого требуется получить доступ к методу (см. пример №17 ).
Код Результат pythonCodes
# Создаем суперкласс. class A: # Статический метод суперкласса. def method_0(): print('«method_0 in class A»') # 1-й метод в суперклассе. def method_1(self): print('«method_1 in class A»') # 2-й метод в суперклассе. def method_2(self): print('«method_2 in class A»') # Наследует суперкласс A. class B(A): # Расширяем 1-й метод из суперкласса A. def method_1(self): # Используем метод из суперкласса A. # Можно и через super().method_1(). super(B, self).method_1() # Своя функциональность. print('«method_1 in class B»') # Наследует суперкласс B. class C(B): # Расширяем 2-й метод из суперкласса A. def method_2(self): # Используем метод из суперкласса A. super(B, self).method_2() # Своя функциональность. print('«method_2 in class C»') # Создаем экземпляр подкласса C. obj = C() # «method_1 in class A» и «method_1 in class B». obj.method_1() # «method_2 in class A» и «method_2 in class C». obj.method_2() # Вызываем вне класса, указав подкласс для которого # ищем суперкласс с методом, а также целевой объект. # Выведет «method_1 in class A». super(B, obj).method_1() # «method_0 in class A» (указали объект класса). super(B, C).method_0()
«method_1 in class A» «method_1 in class B» «method_2 in class A» «method_2 in class C» «method_1 in class A» «method_0 in class A»
Пример №17. Наследование классов в Python (часть 3).
Как видим, функция super() может вызываться из подкласса без аргументов. В таком случае интерпретатор автоматически использует в качестве аргументов текущий подкласс и объект self . Кроме того, ее можно вызывать и вне классов, явно передавая требуемый тип и целевой объект.
Теперь рассмотрим подробнее ситуацию, когда подкласс наследует сразу несколько суперклассов с одинаковыми именами атрибутов. Выше мы уже указывали, что в таком случае подклассом будет унаследовано значение атрибута того суперкласса, который перечисляется в заголовке инструкции class первым. Но что делать, когда важны значения атрибутов обоих классов? В этом случае, чтобы гарантировать принадлежность атрибута тому классу, который его использует, необходимо в начале имени атрибута поставить два символа подчеркивания везде, где оно используется этим классом (см. пример №18 ).
Код Результат pythonCodes
# 1-й суперкласс. class A: # Устанавливаем экземпляру атрибуты. def set_a(self): # Обычный атрибут. self.x = 1 # y искажается в _A__y. self.__y = 1 # Получаем атрибуты. def get_a(self): print(self.x) print(self.__y) # 2-й суперкласс. class B: # Устанавливаем экземпляру атрибуты. def set_b(self): # Обычный атрибут. self.x = 2 # y искажается в _B__y. self.__y = 2 # Получаем атрибуты. def get_b(self): print(self.x) print(self.__y) # Просто наследует 2 суперкласса. class C(A, B): pass # Создаем объект подкласса. obj = C() # Теперь obj.x == 1 и _A__y == 1. obj.set_a() # obj.x переопределен на 2 и _B__y == 2. obj.set_b() # Посмотрим его словарь атрибутов. # . print(obj.__dict__) # Выведет 2 (x) и 1 (_A__y). obj.get_a() # Выведет 2 (x) и 2 (_B__y). obj.get_b()
2 1 2 2
Пример №18. Наследование классов в Python (часть 4).
В нашем примере благодаря наличию приставки из двух знаков подчеркивания атрибуты остались доступны по короткому имени внутри класса, но стали недоступны вне класса, т.к. имена этих атрибутов были дополнены интерпретатором именами их классов до _A__y и _B__y , что и показал словарь атрибутов obj.__dict__ после вызова методов obj.set_a() и obj.set_b() . При этом атрибут x , который использовался без спецприставки, был методом obj.set_b() перезаписан.
Композиция классов
Композиция классов – это еще один принцип ООП, использующийся в Python и предоставляющий возможность создания классов, включающих в себя вызовы уже существующих классов (см. пример №19 ).
Код Результат pythonCodes
# Создаем 1-й класс-донор. class Компания: # Конструктор класса. def __init__(self, название): # Определяем название компании. self.название = название # Устанавливаем юр. адрес компании. def уст_адрес(self, адрес): self.адрес = адрес # Создаем 2-й класс-донор. class Площадь: # Расчет площади прямоугольника. def рассчитать_площадь_прям(self, a, b): self.площадь_прямоугольника = a*b # Расчет площади круга. def рассчитать_площадь_круга(self, r): self.площадь_круга = 3.14*r**2 # Создаем класс-агрегатор. class Магазин: # Конструктор класса. def __init__(self, компания, номер): # Интерфейс для работы с данными компании. self.компания = Компания(компания) # Определяем номер магазина. self.номер = номер # Устанавливаем время работы магазина. def уст_время_работы(self, время='8.00 - 23.00'): self.время_работы = время # Рассчитываем площадь магазина. def уст_площадь_магазина(self, a, b): # Интерфейс для расчета площади магазина. площадь = Площадь() площадь.рассчитать_площадь_прям(a, b) self.площадь_магазина = площадь.площадь_прямоугольника # Создаем экземпляр магазина. маг_1 = Магазин('Простор', 1) # Уст. время работы магазина. маг_1.уст_время_работы('9.30 - 22.00') # Рассчитаем площадь магазина. маг_1.уст_площадь_магазина(50, 70) # Выводим информацию о магазине. print('Компания: «<>»'.format(маг_1.компания.название)) print('Магазин: №', маг_1.номер, sep='') print('Площадь магазина: <>м'.format(маг_1.площадь_магазина)) print('Время работы:', маг_1.время_работы)
Компания: «Простор» Магазин: №1 Площадь магазина: 3500м Время работы: 9.30 - 22.00
Пример №19. Композиция классов в Python.
В нашем примере мы использовали вызовы классов Компания и Площадь внутри класса Магазин , использовав реализации их интерфейсов, т.е. готовые экземпляры этих классов, для получения требуемых нам возможностей: имени компании и метода для расчета площади прямоугольника.
Таким образом, композиция представляет собой еще одну концепцию повторного использования программного кода, предполагающую формирование целого из частей, при котором класс-контейнер использует внутри себя экземпляры других классов-доноров, выступая в качестве контролера их интерфейсов. Такой подход бывает полезным в крупных системах, в которых множественное наследование большого числа классов может приводить к определенным трудностям, а также в случаях, когда нужно повторно использовать лишь небольшую часть возможностей базового класса (как в нашем примере).
Магические методы и перегрузка операторов
представляет собой процесс перехватывания встроенных операций при помощи специальных методов классов, называемых . Все магические методы имеют специальные имена, начинающиеся и заканчивающиеся двумя символами подчеркивания, что отличает их от других имен, которые обычно определяются в классах. Встречая такие методы, интерпретатор автоматически вызывает их при выполнении соответствующих встроенных операций над экземплярами классов, воспринимая возвращаемые ими значения как результаты этих операций.
Перегрузка операторов позволяет классам участвовать в обычных операциях, делая экземпляры классов более похожими на представителей встроенных типов данных. Достигается такой эффект за счет того, что магические методы позволяют перегружать все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к атрибутам и многие другие операции (см. пример №20 ).
Код Результат pythonCodes
# Определяем класс. class Num: # Конструктор класса - один из # самых известных методов перегрузки. def __init__(self, start_data): # Начальное значение экземпляра. self.data = start_data # Метод перегрузки операции вычитания. def __sub__(self, n): # Для целых чисел. if isinstance(n, int): # Создаем и возвр. новый экземпляр, # изменяя хранимое в нем число. return Num(self.data - n) # Для других объектов этого типа. elif isinstance(n, Num): # Вычитаем хранимые значения. return Num(self.data - n.data) # Для других объектов. else: print('Операция не поддерживается!') # При создании экземпляров класса интерпретатор # автоматически вызывает конструктор класса. a = Num(25) b = Num(10) # А здесь интерпретатор автоматически вызывает # спец. метод реализации операции вычитания. c = a - b # Выводим результат операции на экран. print('a - b =', c.data) # Для целых чисел. d = a - 5 # Выведет 20. print('a - 5 =', d.data) # Вещественные числа не поддерживаются. d = a - 5.7 # Магический метод __add__ не переопределен! # unsupported operand type(s) for +: 'Num' and 'int'. d = a + 5
a - b = 15 a - 5 = 20 Операция не поддерживается! unsupported operand type(s) for +: 'Num' and 'int'
Пример №20. Перегрузка операторов в Python.
В примере мы использовали два метода перегрузки, имеющихся в Python : конструктор класса __init__ , а также метод __sub__ , который отвечает за операцию вычитания. Последний метод мы использовали для того, чтобы экземпляры созданного нами типа Num могли использоваться в операции вычитания целых чисел и других экземпляров этого типа.
Все магические методы могут свободно наследоваться от суперклассов и переопределяться в подклассах, как и любые другие методы. Если же какой-то метод не реализован в классе, это лишь означает, что соответствующая ему операция не поддерживается данным классом, а попытка применения такой операции будет возбуждать исключение (что мы и увидели при попытке выполнить инструкцию d = a + 5 , содержащую операцию сложения экземпляра a нового типа Num с целым числом).
Перечислим для ознакомления наиболее часто используемые магические методы: __add__ (сложение), __sub__ (вычитание), __mul__ (умножение), __truediv__ (деление), __mod__ (остаток от деления), __pow__ (возведение в степень), __and__ (логическое И), __or__ (логическое ИЛИ), __getattr__ (обращение к атрибуту), __setattr__ (присваивание атрибуту), __delattr__ (удаление атрибута), __getitem__ (доступ к элементу по индексу, извлечение среза, итерации), __setitem__ (присваивание элементу по индексу или срезу), __delitem__ (удаление среза или элемента по индексу), __len__ (длина), __bool__ (проверка логического значения), __lt__ (меньше), __gt__ (больше), __le__ (меньше или равно), __ge__ (больше или равно), __eq__ (равно), __ne__ (не равно), __iter__ (получение итератора), __iadd__ (комбинированный оператор сложения), __next__ (получение следующего элемента итератора), __contains__ (оператор проверки на вхождение in ), __new__ (создание объекта), __init__ (конструктор), __del__ (деструктор), __call__ (вызов функции) и др.
Значительно большее количество имеющихся магических методов можно найти, например, на официальном сайте Python в разделе Data model справочного руководства.
Абстрактные классы и методы
– это класс, содержащий один или несколько , т.е. объявленных, но не реализованных методов.
Абстрактный класс не может использоваться для создания экземпляров. Для этого он должен быть сперва унаследован, а все его абстрактные методы реализованы. Только тогда полученный подкласс можно будет использовать для получения экземпляров, в противном случае будет возбуждено исключение (см. пример №21 ).
Код Результат pythonCodes
# Импортируем класс и декоратор. from abc import ABC, abstractmethod # Определяем абстрактный суперкласс. class Animal(ABC): # Определяем абстрактный метод. @abstractmethod def make_sound(self): pass # Определяем подкласс кошек. class Cat(Animal): # Реализуем абстрактный метод для кошек. def make_sound(self): return "Мяу" # Определяем подкласс собак. class Dog(Animal): # Реализуем абстрактный метод для собак. def make_sound(self): return "Гав" # Определяем подкласс коров. class Cow(Animal): # Собственный метод (абстрактный не реализован!). def set_owner(self, owner): self.owner = owner # Создаем экземпляр класса кошек. cat = Cat() # Мяукаем. print('Кошки говорят "<>"!'.format(cat.make_sound())) # Создаем экземпляр класса собак. dog = Dog() # Гавкаем. print('Собаки говорят "<>"!'.format(dog.make_sound())) try: # Пытаемся создать экземпляр абстрактного класса. animal = Animal() except Exception as err: # Can't instantiate abstract class Animal with # abstract method make_sound/ print(err) try: # Пытаемся создать экземпляр класса Cow, в # котором мы забыли реализовать абстрактный метод. cow = Cow() except Exception as err: # Can't instantiate abstract class Cow with # abstract method make_sound. print(err)
Кошки говорят "Мяу"! Собаки говорят "Гав"! Can't instantiate abstract class Animal with abstract method make_sound Can't instantiate abstract class Cow with abstract method make_sound
Пример №21. Определение абстрактных классов в Python.
Как видим, создать абстрактный класс в Пайтоне довольно просто: нужно всего лишь импортировать из модуля abc стандартной библиотеки метакласс ABC и декоратор abstractmethod , после чего унаследовать метакласс создаваемым абстрактным классом, попутно помечая декоратором методы, которые необходимо сделать абстрактными. Далее главное не забывать о необходимости реализации абстрактных методов в создаваемых подклассах, иначе интерпретатор обязательно напомнит об этом, возбуждая соответствующие исключения.
Стоит добавить, что абстрактные методы могут быть реализованы и в самом абстрактном классе. Однако в любом случае в создаваемом подклассе все они должны быть либо полностью переопределены, либо дополнены в соответствии с правилами обычного наследования (см. пример №22 ).
Код Результат pythonCodes
# Импортируем класс и декоратор. from abc import ABC, abstractmethod # Определяем абстрактный суперкласс. class Animal(ABC): # Определяем абстрактный метод. @abstractmethod def make_sound(self): self.s = 'Питомец говорит "<>"!' # Определяем подкласс кошек. class Cat(Animal): # Реализуем абстрактный метод для кошек. def make_sound(self): # Вызываем сперва родительский метод. # Animal.make_sound(self) super().make_sound() return self.s.format('Мяу') # Определяем подкласс собак. class Dog(Animal): # Реализуем абстрактный метод для собак. def make_sound(self): # Полностью переопределяем абстр. метод. s = 'Собаки говорят "<>"!'.format('Гав') return s # Создаем экземпляр класса кошек. cat = Cat() # Мяукаем. print(cat.make_sound()) # Создаем экземпляр класса собак. dog = Dog() # Гавкаем. print(dog.make_sound())
Питомец говорит "Мяу"! Собаки говорят "Гав"!
Пример №22. Переопределение абстрактных методов в подклассах.
Таким образом, как и другие концепции объектно-ориентированного программирования, абстрактные классы вносят свою лепту в улучшение качества архитектуры приложения, позволяют уменьшить объем работы и облегчают дальнейшую поддержку кода.
Конструкторы и деструкторы
Как говорилось выше, конструктор представляет собой магический метод, который автоматически вызывается при создании каждого нового экземпляра класса, в котором он определен. Это позволяет передавать вновь создаваемым экземплярам начальные данные, тем самым инициализируя некоторое состояние объекта еще до начала его использования. Для объявления конструкторов используется имя __init__ , изменять которое запрещается.
Далее, если созданные объекты после выполнения своего предназначения становятся ненужными, имеет смысл удалять их для высвобождения памяти. В Python для этих целей имеется автоматический сборщик мусора, удаляющий объекты, которые выходят за пределы текущей области видимости или счетчики ссылок на которые становятся равными нулю. Кроме того, ненужные объекты можно удалять и самостоятельно, используя, например, инструкцию del . В любом случае, если ссылок на объект больше не остается, интерпретатор вызывает деструктор – еще один магический метод с именем __del__ , представляющий собой операцию окончательного удаления объектов. Используются деструкторы реже конструкторов, но в некоторых ситуациях их применение бывает весьма оправданным. Так деструкторы обычно определяют в классах, для которых перед стандартным удалением требуется выполнить еще какие-нибудь действия, например, закрыть файл или соединение с базой данных (см. пример №23 ).
Код Результат pythonCodes
# Определяем класс. class Бюджет: # Конструктор класса инициализирует # экземпляр начальными данными. def __init__(self, сумма=100000): # Создаем атрибут данных экземпляра. self.сумма = сумма # Деструктор класса запускается автоматически # после удаления всех ссылок на объект. def __del__(self): # Перед удалением выводим сообщение. print('Бюджет успешно распилен!') # Создаем экземпляр класса. городской_бюджет_1 = Бюджет(1000000) # Получаем еще одну ссылку на объект. городской_бюджет_2 = городской_бюджет_1 # Счетчик ссылок на объект уменьшается на 1. del городской_бюджет_1 # А вот здесь уже будет вызван деструктор. del городской_бюджет_2
Бюджет успешно распилен!
Пример №23. Конструкторы и деструкторы в Python.
Обратите внимание, что инструкция del obj не вызывает напрямую метод obj.__del__() , она лишь уменьшает счетчик ссылок для соответствующего объекта на единицу. И только тогда, когда счетчик полностью обнуляется, интерпретатор использует деструктор для окончательной зачистки следов существования ненужного объекта в памяти.
Краткие итоги параграфа
- Класс – это шаблон кода, который используется для описания структуры и создания объектов, т.е. экземпляров этого класса.
- Python обладает всеми преимуществами абстрактного подхода в программировании. В частности ему присущи:
- полиморфизм – способность функций и методов обрабатывать данные разных типов;
- инкапсуляция – механизм, который позволяет объединять данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя;
- наследование – еще одна концепция объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность с возможностью их изменения и добавления своих данных и функциональности;
- композиция (агрегирование) – возможность создания классов, включающих в себя вызовы уже существующих классов.
Помогите проекту, подпишитесь!
Подписка на учебные материалы сайта оформляется сроком на один год и стоит около 15 у.е. 5 у.е. После подписки вам станут доступны следующие возможности.
- Доступ ко всем ответам тестов и решениям задач.
- Возможность загрузки учебных кодов и программ на свой компьютер. Доступ ко всем видеоурокам.
- Практически полное отключение рекламы.
- Возможность внести свой скромный вклад в развитие проекта и мира во всем мире, а также выразить свою благодарить автору за его труд. Нам очень нужна ваша поддержка!

На страницу подписки
Вопросы и задания для самоконтроля
1. Каково основное назначение ООП в языке Python ? Показать решение.
Ответ. Основное назначение ООП в языке Python , да и в программировании в целом, состоит в том, чтобы обеспечить многократное использование программного кода посредством его разложения на структурные составляющие и последующей адаптации, а не изменения или создания нового кода.
2. Перечислите основные компоненты парадигмы ООП в языке Python ? Показать решение.
Ответ. Полиморфизм, инкапсуляция, наследование и композиция (описание смотрите в начале параграфа).
3. Что представляет собой класс? Как создать класс? Показать решение.
Ответ. Класс представляет собой шаблон кода, который используется для описания структуры и создания объектов, т.е. экземпляров этого класса. Для создания классов в Python предназначена инструкция class . В заголовке инструкции указываются имя класса и перечисляемые в скобках через запятую наследуемые суперклассы, а в теле инструкции – атрибуты класса, представляющие собой данные и методы для их обработки.
4. Как создать экземпляр класса? Показать решение.
Ответ. Для создания экземпляра класса нужно после имени класса указать круглые скобки, перечислив в них начальные данные для инициализации создаваемого экземпляра, если конструктор класса это предполагает. Внешне данная инструкция похожа на вызов обычной функции.
Решение Результат pythonCodes
# Простейший в мире класс. class Simple: pass # Создаем экземпляры класса. simp_1 = Simple() simp_2 = Simple()5. Для чего используется метод __init__ ? Показать решение.
Ответ. Конструктор класса __init__ , если он присутствует в классе или наследуется им, служит для автоматической инициализации вновь создаваемых экземпляров класса начальными данными. Важно помнить, что если конструктор определяется с параметрами, которые не предполагают значений по умолчанию, то при создании экземпляров класса аргументам должны передаваться значения, иначе интерпретатор обязательно возбудит соответствующее исключение.
Решение Результат pythonCodes
# Класс с конструктором. class Simple: # Конструктор с параметром. def __init__(self, x): self.x = x # Передача аргумента обязательна. simp_1 = Simple(5) # А здесь получим ошибку. simp_2 = Simple(). __init__() missing 1 required positional argument: 'x'6. Что представляет из себя первый аргумент в методах классов? Показать решение.
Ответ. Посредством первого аргумента методы классов получают ссылку на объект экземпляра, который подразумевается при вызове данного метода. Именно поэтому про методы классов говорят, что они «объектно-ориентированные», то есть предназначенны для обработки или изменения объектов. Согласно общепринятым соглашениям в качестве имени первого аргумента используется идентификатор self .
7. Как создать статический метод? А метод класса? Показать решение.
Ответ. Для создания методов классов и статических методов служат встроенные функции classmethod(method) и staticmethod(method) , которые необходимо вызывать либо использовать в качестве декораторов внутри классов.
Решение Результат pythonCodes
class ExClass: def object_method(self): print('Метод экземпляра') @classmethod def class_method(cls): print('Метод класса') @staticmethod def static_method(): print('Статический метод') # Создаем экземпляр класса. obj = ExClass() obj.object_method() # Требуется передача объекта. ExClass.object_method(obj) obj.class_method() ExClass.class_method() obj.static_method() ExClass.static_method()Метод экземпляра Метод экземпляра Метод класса Метод класса Статический метод Статический метод8. Назовите самый простой способ сделать атрибут класса недоступным по его имени за пределами класса. Показать решение.
Ответ. Проще всего использовать два символа нижнего подчеркивания, например, __name . В этом случае атрибут станет недоступен вне класса по его имени, т.к. оно будет искажено интерпретатором до формата _Class__name . При этом не стоит забывать, что данный способ не обеспечивает настоящего сокрытия данных, т.к. зная имя вмещающего класса всегда можно обратиться к таким атрибутам по их расширенному имени _Class__attr_name из любой точки программы, где имеется ссылка на экземпляр класса.
9. Для чего предназначена встроенная функция property() ? Какие аргументы она ожидает получить? Можно ли использовать ее в качестве декоратора? Показать решение.
Ответ. Функция property(fget=None, fset=None, fdel=None, doc=None) позволяет создать свойство класса (управляемый атрибут). Функция принимает четыре аргумента со значениями по умолчанию None : fget – объект функции, которая будет вызываться при попытке прочитать значение атрибута и возвращать вычисленное значение атрибута; fset – объект функции, которая будет вызываться при попытке выполнить операцию присваивания; fdel – объект функции, которая будет вызываться при попытке удаления атрибута; doc – строка документирования с описанием атрибута, если это необходимо. Для использования ее в качестве декоратора, необходимо пометить метод, например, с именем name , отвечающий за получение значения атрибута аннотацией @property , а также при необходимости создать еще два метода с таким же именем для установки значения атрибута и удаления атрибута, использовав соответственно аннотации @name.setter и @name.deleter .
Решение Результат pythonCodes
class ExClass: @property def m(self): print('m имеет значение ', end='') return self._m @m.setter def m(self, val): self._m = val print('m установлен в', self._m) @m.deleter def m(self): del self._m print('Атрибут m удален!') obj = ExClass() obj.m = 3 print(obj.m) del obj.mm установлен в 3 m имеет значение 3 Атрибут m удален!10. Как выполняется поиск унаследованных атрибутов? Показать решение.
Ответ. Поиск унаследованных атрибутов выполняется сначала в объекте экземпляра, затем в классе, из которого был создан экземпляр, и далее, по умолчанию, во всех суперклассах в направлении снизу вверх и слева направо в дереве объектов. Как только будет найдено первое вхождение атрибута, поиск прекращается.
11. Как получить доступ из подкласса к методу суперкласса? Показать решение.
Ответ. В ходе наследования доступ к оригинальным методам может быть получен как напрямую через имя суперкласса в формате SuperClass.method(self) , так и через встроенную функцию super([type[, object-or-type]]) , возвращающую специальный объект-посредник, делегирующий вызовы метода данного объекта экземпляра или подкласса суперклассу указанного типа type .
12. Что представляет из себя принцип композиции? Показать решение.
Ответ. Принцип композиции подразумевает использование в определении класса вызовов других классов. В результате получаемые экземпляры класса как бы собираются из экземпляров других классов, реализуя подход формирования целого из частей. Такой подход бывает полезным в крупных системах, в которых множественное наследование большого числа классов может приводить к определенным трудностям, а также в случаях, когда нужно повторно использовать лишь небольшую часть возможностей базового класса.
13. Для чего используются магические методы? Как отличить их от обычных методов? Показать решение.
Ответ. Магические методы позволяют перегружать все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к атрибутам и т.д. Благодаря этому классы могут участвовать в обычных операциях, что делает их более похожими на встроенные типы. Все магические методы имеют специальные имена, начинающиеся и заканчивающиеся двумя символами подчеркивания, что отличает их от других имен, которые обычно определяются в классах.
14. О чем нужно помнить, создавая экземпляры абстрактного класса? Показать решение.
Ответ. Все абстрактные методы в подклассе абстрактного класса должны быть обязательно реализованы или переопределены. Если этого не сделать, интерпретатор бросит исключение.
15. В каких случаях деструктор автоматически вызывается интерпретатором? Показать решение.
Ответ. Деструктор, представляемый магическим методом __del__ , автоматически вызывается сборщиком мусора при окончательном удалении объектов, которые выходят за пределы текущей области видимости или счетчики ссылок на которые становятся равными нулю.
16. Исправьте в коде все ошибки так, чтобы скрипт заработал. Показать решение.
Условие pythonCodes
# Создаем суперкласс. class SuperClass(): # Конструктор суперкласса. def __init__(self): self.num = num # Метод суперкласса. def get_num(): print(self.num) # Создаем подкласс. class SubClass(self, SuperClass): # Конструктор подкласса. def __init__(self, num): # Вызываем конструктор суперкласса. super()._init_(self, num) print('Экземпляр создан!') # Создаем 1-й экземпляр подкласса. obj_1 = SubClass() # Выводим значение атрибута. print(obj_1.num) # Создаем 2-й экземпляр подкласса. obj_2 = SubClass(5) # Выводим значение атрибута. obj_1.get_num(self)Решение Результат pythonCodes
# Создаем суперкласс. # Скобки не нужны. class SuperClass: # Конструктор суперкласса. # Был пропущен арг-т num. def __init__(self, num): self.num = num # Метод суперкласса. # Не был указан арг-т self. def get_num(self): print(self.num) # Создаем подкласс. # А здесь self передавать не нужно. class SubClass(SuperClass): # Конструктор подкласса. def __init__(self, num): # Вызываем конструктор суперкласса. # Правильно __init__ и без self. super().__init__(num) print('Экземпляр создан!') # Создаем 1-й экземпляр подкласса. # Конструктор требует начального значения. obj_1 = SubClass(7) # Выводим значение атрибута. print(obj_1.num) # Создаем 2-й экземпляр подкласса. obj_2 = SubClass(5) # Выводим значение атрибута. # Здесь self тоже не нужен. obj_2.get_num()Экземпляр создан! 7 Экземпляр создан! 517. Дополнительные упражнения и задачи по теме расположены в разделе «ООП: классы и объекты» нашего сборника задач и упражнений по языку программирования Python .
Быстрый переход к другим страницам
- Функции в Python
- ООП в Python: классы и объекты
- Модули и пакеты в Python
- Вернуться к оглавлению учебника

okpython.net Copyright © 2022-2023.
Примеры работы с классами в Python
Python — объектно-ориентированный язык с начала его существования. Поэтому, создание и использование классов и объектов в Python просто и легко. Эта статья поможет разобраться на примерах в области поддержки объектно-ориентированного программирования Python. Если у вас нет опыта работы с объектно-ориентированным программированием (OOП), ознакомьтесь с вводным курсом или учебным пособием, чтобы понять основные понятия.
Создание классов
Оператор class создает новое определение класса. Имя класса сразу следует за ключевым словом class , после которого ставиться двоеточие:
class ClassName: """Необязательная строка документации класса""" class_suite- У класса есть строка документации, к которой можно получить доступ через ClassName.__doc__ .
- class_suite состоит из частей класса, атрибутов данных и функции.
Пример создания класса на Python:
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.emp_count += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.empCount) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary))- Переменная emp_count — переменная класса, значение которой разделяется между экземплярами этого класса. Получить доступ к этой переменной можно через Employee.emp_count из класса или за его пределами.
- Первый метод __init__() — специальный метод, который называют конструктором класса или методом инициализации. Его вызывает Python при создании нового экземпляра этого класса.
- Объявляйте другие методы класса, как обычные функции, за исключением того, что первый аргумент для каждого метода self . Python добавляет аргумент self в список для вас; и тогда вам не нужно включать его при вызове этих методов.
Создание экземпляров класса
Чтобы создать экземпляры классов, нужно вызвать класс с использованием его имени и передать аргументы, которые принимает метод __init__ .
# Это создаст первый объект класса Employee emp1 = Employee("Андрей", 2000) # Это создаст второй объект класса Employee emp2 = Employee("Мария", 5000)Доступ к атрибутам
Получите доступ к атрибутам класса, используя оператор . после объекта класса. Доступ к классу можно получить используя имя переменой класса:
emp1.display_employee() emp2.display_employee() print("Всего сотрудников: %d" % Employee.emp_count)Теперь, систематизируем все.
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.emp_count += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.emp_count) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary)) # Это создаст первый объект класса Employee emp1 = Employee("Андрей", 2000) # Это создаст второй объект класса Employee emp2 = Employee("Мария", 5000) emp1.display_employee() emp2.display_employee() print("Всего сотрудников: %d" % Employee.emp_count)При выполнении этого кода, мы получаем следующий результат:
Имя: Андрей. Зарплата: 2000 Имя: Мария. Зарплата: 5000 Всего сотрудников: 2Вы можете добавлять, удалять или изменять атрибуты классов и объектов в любой момент.
emp1.age = 7 # Добавит атрибут 'age' emp1.age = 8 # Изменит атрибут 'age' del emp1.age # Удалит атрибут 'age'
Вместо использования привычных операторов для доступа к атрибутам вы можете использовать эти функции:
- getattr(obj, name [, default]) — для доступа к атрибуту объекта.
- hasattr(obj, name) — проверить, есть ли в obj атрибут name .
- setattr(obj, name, value) — задать атрибут. Если атрибут не существует, он будет создан.
- delattr(obj, name) — удалить атрибут.
hasattr(emp1, 'age') # возвращает true если атрибут 'age' существует getattr(emp1, 'age') # возвращает значение атрибута 'age' setattr(emp1, 'age', 8) #устанавливает атрибут 'age' на 8 delattr(empl, 'age') # удаляет атрибут 'age'
Встроенные атрибуты класса
Каждый класс Python хранит встроенные атрибуты, и предоставляет к ним доступ через оператор . , как и любой другой атрибут:
- __dict__ — словарь, содержащий пространство имен класса.
- __doc__ — строка документации класса. None если, документация отсутствует.
- __name__ — имя класса.
- __module__ — имя модуля, в котором определяется класс. Этот атрибут __main__ в интерактивном режиме.
- __bases__ — могут быть пустые tuple, содержащие базовые классы, в порядке их появления в списке базового класса.
Для вышеуказанного класса давайте попробуем получить доступ ко всем этим атрибутам:
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.empCount += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.empCount) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary)) print("Employee.__doc__:", Employee.__doc__) print("Employee.__name__:", Employee.__name__) print("Employee.__module__:", Employee.__module__) print("Employee.__bases__:", Employee.__bases__) print("Employee.__dict__:", Employee.__dict__)Когда этот код выполняется, он возвращает такой результат:
Employee.__doc__: Базовый класс для всех сотрудников Employee.__name__: Employee Employee.__module__: __main__ Employee.__bases__: (,) Employee.__dict__: , 'display_count': , 'display_employee': , '__dict__': , '__weakref__': >Удаление объектов (сбор мусора)
Python автоматически удаляет ненужные объекты (встроенные типы или экземпляры классов), чтобы освободить пространство памяти. С помощью процесса ‘Garbage Collection’ Python периодически восстанавливает блоки памяти, которые больше не используются.
Сборщик мусора Python запускается во время выполнения программы и тогда, когда количество ссылок на объект достигает нуля. С изменением количества обращений к нему, меняется количество ссылок.
Когда объект присваивают новой переменной или добавляют в контейнер (список, кортеж, словарь), количество ссылок объекта увеличивается. Количество ссылок на объект уменьшается, когда он удаляется с помощью del , или его ссылка выходит за пределы видимости. Когда количество ссылок достигает нуля, Python автоматически собирает его.
a = 40 # создали объект b = a # увеличивает количество ссылок c = [b] # увеличивает количество ссылок del a # уменьшает количество ссылок b = 100 # уменьшает количество ссылок c[0] = -1 # уменьшает количество ссылок
Обычно вы не заметите, когда сборщик мусора уничтожает экземпляр и очищает свое пространство. Но классом можно реализовать специальный метод __del__() , называемый деструктором. Он вызывается, перед уничтожением экземпляра. Этот метод может использоваться для очистки любых ресурсов памяти.
Пример работы __del__()
Деструктор __del__() выводит имя класса того экземпляра, который должен быть уничтожен:class Point: def __init__(self, x=0, y=0): self.x = x self.y = y def __del__(self): class_name = self.__class__.__name__ print('<> уничтожен'.format(class_name)) pt1 = Point() pt2 = pt1 pt3 = pt1 print(id(pt1), id(pt2), id(pt3)) # выведите id объектов del pt1 del pt2 del pt3Когда вышеуказанный код выполняется и выводит следующее:
17692784 17692784 17692784 Point уничтоженВ идеале вы должны создавать свои классы в отдельном модуле. Затем импортировать их в основной модуль программы с помощью import SomeClass .
Наследование класса в python
Наследование — это процесс, когда один класс наследует атрибуты и методы другого. Класс, чьи свойства и методы наследуются, называют Родителем или Суперклассом. А класс, свойства которого наследуются — класс-потомок или Подкласс.
Вместо того, чтобы начинать с нуля, вы можете создать класс, на основе уже существующего. Укажите родительский класс в круглых скобках после имени нового класса.
Класс наследник наследует атрибуты своего родительского класса. Вы можете использовать эти атрибуты так, как будто они определены в классе наследнике. Он может переопределять элементы данных и методы родителя.
Синтаксис наследования класса
Классы наследники объявляются так, как и родительские классы. Только, список наследуемых классов, указан после имени класса.
class SubClassName(ParentClass1[, ParentClass2, . ]): """Необязательная строка документации класса""" class_suiteПример наследования класса в Python
class Parent: # объявляем родительский класс parent_attr = 100 def __init__(self): print('Вызов родительского конструктора') def parent_method(self): print('Вызов родительского метода') def set_attr(self, attr): Parent.parent_attr = attr def get_attr(self): print('Атрибут родителя: <>'.format(Parent.parent_attr)) class Child(Parent): # объявляем класс наследник def __init__(self): print('Вызов конструктора класса наследника') def child_method(self): print('Вызов метода класса наследника') c = Child() # экземпляр класса Child c.child_method() # вызов метода child_method c.parent_method() # вызов родительского метода parent_method c.set_attr(200) # еще раз вызов родительского метода c.get_attr() # снова вызов родительского методаКогда этот код выполняется, он выводит следующий результат:
Вызов конструктора класса наследника Вызов метода класса наследника Вызов родительского метода Атрибут родителя: 200Аналогичным образом вы можете управлять классом с помощью нескольких родительских классов:
class A: # объявите класс A . class B: # объявите класс B . class C(A, B): # C наследуется от A и B .Вы можете использовать функции issubclass() или isinstance() для проверки отношений двух классов и экземпляров.
- Логическая функция issubclass(sub, sup) возвращает значение True , если данный подкласс sub действительно является подклассом sup .
- Логическая функция isinstance(obj, Class) возвращает True , если obj является экземпляром класса Class или является экземпляром подкласса класса.
Переопределение методов
Вы всегда можете переопределить методы родительского класса. В вашем подклассе могут понадобиться специальные функции. Это одна из причин переопределения родительских методов.
Пример переопределения методов:
class Parent: # объявите родительский класс def my_method(self): print('Вызов родительского метода') class Child(Parent): # объявите класс наследник def my_method(self): print('Вызов метода наследника') c = Child() # экземпляр класса Child c.my_method() # метод переопределен классом наследникомКогда этот код выполняется, он производит следующий результат:
Вызов метода наследникаПопулярные базовые методы
В данной таблице перечислены некоторые общие функции. Вы можете переопределить их в своих собственных классах.
№ Метод, описание и пример вызова 1 __init__(self [, args. ]) — конструктор (с любыми необязательными аргументами)
obj = className(args)2 __del__(self) — деструктор, удаляет объект
del obj3 __repr__(self) — программное представление объекта
repr(obj)4 __str__(self) — строковое представление объекта
str(obj)Пример использования __add__
Предположим, вы создали класс Vector для представления двумерных векторов. Что происходит, когда вы используете дополнительный оператор для их добавления? Скорее всего, Python будет против.
Однако вы можете определить метод __add__ в своем классе для добавления векторов и оператор + будет вести себя так как нужно.
class Vector: def __init__(self, a, b): self.a = a self.b = b def __str__(self): return 'Vector (<>, <>)'.format(self.a, self.b) def __add__(self, other): return Vector(self.a + other.a, self.b + other.b) v1 = Vector(2, 10) v2 = Vector(5, -2) print(v1 + v2)
При выполнении этого кода, мы получим:
Vector(7, 8)Приватные методы и атрибуты
Атрибуты класса могут быть не видимыми вне определения класса. Вам нужно указать атрибуты с __ вначале, и эти атрибуты не будут вызваны вне класса.
Пример приватного атрибута:
class JustCounter: __secret_count = 0 def count(self): self.__secret_count += 1 print(self.__secret_count) counter = JustCounter() counter.count() counter.count() print(counter.__secret_count)
При выполнении данного кода, имеем следующий результат:
1 2 Traceback (most recent call last): File "test.py", line 12, in print(counter.__secret_count) AttributeError: 'JustCounter' object has no attribute '__secret_count'Вы можете получить доступ к таким атрибутам, так object._className__attrName . Если вы замените свою последнюю строку следующим образом, то она будет работать.
