Python — это процедурно-ориентированный и одновременно объектно-ориентированный язык программирования.
«Процедурно-ориентированный» подразумевает наличие функций. Программист может создавать функции, которые затем используются в сторонних скриптах.
«Объектно-ориентированный» подразумевает наличие классов. Есть возможность создавать классы, представляющие собой прототипы для будущих объектов.
Синтаксис для написания нового класса:
class ClassName:
'Краткое описание класса (необязательно)'
# Код ...
class, его имя и двоеточие (:). Первая строчка в теле класса описывает его. (По желанию) получить доступ к этой строке можно с помощью ClassName.__doc__Атрибут — это элемент класса. Например, у прямоугольника таких 2: ширина (width) и высота (height).
self (ключевое слово, которое ссылается на сам класс).__init__.self (ключевое слово, которое ссылается на сам класс).# Прямоугольник.
class Rectangle :
'Это класс Rectangle'
# Способ создания объекта (конструктор)
def __init__(self, width, height):
self.width= width
self.height = height
def getWidth(self):
return self.width
def getHeight(self):
return self.height
# Метод расчета площади.
def getArea(self):
return self.width * self.height
Создание объекта с помощью класса Rectangle:

# Создаем 2 объекта: r1 & r2
r1 = Rectangle(10,5)
r2 = Rectangle(20,11)
print("r1.width = ", r1.width)
print("r1.height = ", r1.height)
print("r1.getWidth() = ", r1.getWidth())
print("r1.getArea() = ", r1.getArea())
print("-----------------")
print("r2.width = ", r2.width)
print("r2.height = ", r2.height)
print("r2.getWidth() = ", r2.getWidth())
print("r2.getArea() = ", r2.getArea())

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

В других языках программирования конструкторов может быть несколько. В Python — только один. Но этот язык разрешает задавать значение по умолчанию.
Все требуемые аргументы нужно указывать до аргументов со значениями по умолчанию.
class Person:
# Параметры возраста и пола имеют значение по умолчанию.
def __init__(self, name, age=1, gender="Male"):
self.name = name
self.age = age
self.gender= gender
def showInfo(self):
print("Name: ", self.name)
print("Age: ", self.age)
print("Gender: ", self.gender)
Например:
from person import Person
# Создать объект Person.
aimee = Person("Aimee", 21, "Female")
aimee.showInfo()
print(" --------------- ")
# возраст по умолчанию, пол.
alice = Person( "Alice" )
alice.showInfo()
print(" --------------- ")
# Пол по умолчанию.
tran = Person("Tran", 37)
tran.showInfo()

В Python объект, созданный с помощью конструктора, занимает реальное место в памяти. Это значит, что у него есть точный адрес.
Если объект AA — это просто ссылка на объект BB, то он не будет сущностью, занимающей отдельную ячейку памяти. Вместо этого он лишь ссылается на местоположение BB.

Оператор == нужен, чтобы узнать, ссылаются ли два объекта на одно и то же место в памяти. Он вернет True, если это так. Оператор != вернет True, если сравнить 2 объекта, которые ссылаются на разные места в памяти.
from rectangle import Rectangle
r1 = Rectangle(20, 10)
r2 = Rectangle(20 , 10)
r3 = r1
# Сравните r1 и r2
test1 = r1 == r2 # --> False
# Сравните r1 и r3
test2 = r1 == r3 # --> True
print ("r1 == r2 ? ", test1)
print ("r1 == r3 ? ", test2)
print (" -------------- ")
print ("r1 != r2 ? ", r1 != r2)
print ("r1 != r3 ? ", r1 != r3)

В Python есть два похожих понятия, которые на самом деле отличаются:
Стоит разобрать на практике:
class Player:
# Переменная класса
minAge = 18
maxAge = 50
def __init__(self, name, age):
self.name = name
self.age = age
Объекты, созданные одним и тем же классом, будут занимать разные места в памяти, а их атрибуты с «одинаковыми именами» — ссылаться на разные адреса. Например:

from player import Player
player1 = Player("Tom", 20)
player2 = Player("Jerry", 20)
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)
print(" ------------ ")
print("Assign new value to player1.age = 21 ")
# Присвойте новое значение атрибуту возраста player1.
player1.age = 21
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)

Python умеет создавать новые атрибуты для уже существующих объектов. Например, объект player1 и новый атрибут address.
from player import Player
player1 = Player("Tom", 20)
player2 = Player("Jerry", 20)
# Создайте новый атрибут с именем «address» для player1.
player1.address = "USA"
print("player1.name = ", player1.name)
print("player1.age = ", player1.age)
print("player1.address = ", player1.address)
print(" ------------------- ")
print("player2.name = ", player2.name)
print("player2.age = ", player2.age)
# player2 е имеет атрибута 'address' (Error!!)
print("player2.address = ", player2.address)
Вывод:
player1.name = Tom
player1.age = 20
player1.address = USA
-------------------
player2.name = Jerry
player2.age = 20
Traceback (most recent call last):
File "C:/Users/gvido/class.py", line 27, in <module>
print("player2.address = ", player2.address)
AttributeError: 'Player' object has no attribute 'address'
Обычно получать доступ к атрибутам объекта можно с помощью оператора «точка» (например, player1.name). Но Python умеет делать это и с помощью функции.
| Функция | Описание |
|---|---|
getattr (obj, name[,default]) |
Возвращает значение атрибута или значение по умолчанию, если первое не было указано |
hasattr (obj, name) |
Проверяет атрибут объекта — был ли он передан аргументом «name» |
setattr (obj, name, value) |
Задает значение атрибута. Если атрибута не существует, создает его |
delattr (obj, name) |
Удаляет атрибут |
from player import Player
player1 = Player("Tom", 20)
# getattr(obj, name[, default])
print("getattr(player1,'name') = " , getattr(player1,"name"))
print("setattr(player1,'age', 21): ")
# setattr(obj,name,value)
setattr(player1,"age", 21)
print("player1.age = ", player1.age)
# Проверка, что player1 имеет атрибут 'address'?
hasAddress = hasattr(player1, "address")
print("hasattr(player1, 'address') ? ", hasAddress)
# Создать атрибут 'address' для объекта 'player1'
print("Create attribute 'address' for object 'player1'")
setattr(player1, 'address', "USA")
print("player1.address = ", player1.address)
# Удалить атрибут 'address'.
delattr(player1, "address")
Вывод:
getattr(player1,'name') = Tom
setattr(player1,'age', 21):
player1.age = 21
hasattr(player1, 'address') ? False
Create attribute 'address' for object 'player1'
player1.address = USA
Объекты класса — дочерние элементы по отношению к атрибутам самого языка Python. Таким образом они заимствуют некоторые атрибуты:
| Атрибут | Описание |
|---|---|
__dict__ |
Предоставляет данные о классе коротко и доступно, в виде словаря |
__doc__ |
Возвращает строку с описанием класса, или None, если значение не определено |
__class__ |
Возвращает объект, содержащий информацию о классе с массой полезных атрибутов, включая атрибут __name__ |
__module__ |
Возвращает имя «модуля» класса или __main__, если класс определен в выполняемом модуле. |
class Customer:
'Это класс Customer'
def __init__(self, name, phone, address):
self.name = name
self.phone = phone
self.address = address
john = Customer("John",1234567, "USA")
print ("john.__dict__ = ", john.__dict__)
print ("john.__doc__ = ", john.__doc__)
print ("john.__class__ = ", john.__class__)
print ("john.__class__.__name__ = ", john.__class__.__name__)
print ("john.__module__ = ", john.__module__)
Вывод:
john.__dict__ = {'name': 'John', 'phone': 1234567, 'address': 'USA'}
john.__doc__ = Это класс Customer
john.__class__ = <class '__main__.Customer'>
john.__class__.__name__ = Customer
john.__module__ = __main__
Переменные класса в Python — это то же самое, что Field в других языках, таких как Java или С#. Получить к ним доступ можно только с помощью имени класса или объекта.
Для получения доступа к переменной класса лучше все-таки использовать имя класса, а не объект. Это поможет не путать «переменную класса» и атрибуты.
У каждой переменной класса есть свой адрес в памяти. И он доступен всем объектам класса.

from player import Player
player1 = Player("Tom", 20)
player2 = Player("Jerry", 20)
# Доступ через имя класса.
print ("Player.minAge = ", Player.minAge)
# Доступ через объект.
print("player1.minAge = ", player1.minAge)
print("player2.minAge = ", player2.minAge)
print(" ------------ ")
print("Assign new value to minAge via class name, and print..")
# Новое значение minAge через имя класса
Player.minAge = 19
print("Player.minAge = ", Player.minAge)
print("player1.minAge = ", player1.minAge)
print("player2.minAge = ", player2.minAge)
Вывод:
Player.minAge = 18
player1.minAge = 18
player2.minAge = 18
------------
Assign new value to minAge via class name, and print..
Player.minAge = 19
player1.minAge = 19
player2.minAge = 19
В Python присутствует функция dir, которая выводит список всех методов, атрибутов и переменных класса или объекта.
from player import Player
# Вывести список атрибутов, методов и переменных объекта 'Player'
print(dir(Player))
print("\n\n")
player1 = Player("Tom", 20)
player1.address ="USA"
# Вывести список атрибутов, методов и переменных объекта 'player1'
print(dir(player1))
Вывод:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'maxAge', 'minAge']
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'address', 'age', 'maxAge',
'minAge', 'name']
]]>Предыдущий урок: Массивы
Python — объектно-ориентированный язык программирования. Почти все в Python — это объект с его свойствами и методами. Класс похож на конструктор объекта или ‘‘проект’’ для создания объектов.
Для того, чтобы создать класс, используйте ключевое слово class.
Создадим класс с именем MyClass и свойством x:
class MyClass:
x = 5
Теперь мы можем использовать класс под названием myClass для создания объектов.
Создадим объект под названием p1, и выведем значение x:
p1 = MyClass()
print(p1.x)
Вывод:
5
Приведенные выше примеры — это классы и объекты в их простейшей форме и не очень полезны в приложениях.
Чтобы понять значение классов, нам нужно понять встроенную функцию __init__.
У всех классов есть функция под названием __init__(), которая всегда выполняется при создании объекта. Используйте функцию __init__() для добавления значений свойствам объекта или других операций, которые необходимо выполнить, при создании объекта.
Для создания класса под названием Person, воспользуемся функцией __init__(), что бы добавить значения для имени и возраста:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Василий", 36)
print(p1.name)
print(p1.age)
Вывод:
Василий
36
Обратите внимание: Функция __init__() автоматически вызывается каждый раз при использовании класса для создания нового объекта.
Объекты также содержат методы. Методы в объектах — это функции, принадлежащие объекту.
Давайте создадим метод в классе Person.
Добавим функцию, которая выводит приветствие, и выполним ее:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def myfunc(self):
print("Привет, меня зовут " + self.name)
p1 = Person("Василий", 36)
p1.myfunc()
Вывод:
Привет, меня зовут Василий
Параметр self является ссылкой на сам класс и используется для доступа к переменным, принадлежащим классу.
Его не обязательно называть self, вы можете называть его как хотите, но он должен быть первым параметром любой функции в классе.
Используем слова mysillyobject и abc вместо self:
class Person:
def __init__(mysillyobject, name, age):
mysillyobject.name = name
mysillyobject.age = age
def myfunc(abc):
print("Привет, меня зовут " + abc.name)
p1 = Person("Василий", 36)
p1.myfunc()
Вывод:
Привет, меня зовут Василий
Вы можете изменять свойства объектов следующим образом.
Изменим возраст от p1 на 40:
p1.age = 40
Больше примеров применения class в Python 3: Примеры работы с классами в Python
Свойства объектов можно удалять с помощью ключевого слова del
del p1.age
Вы можете удалить объекты, используя ключевое слово del.
del p1
]]>Далее: Итераторы 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__: (<class 'object'>,)
Employee.__dict__: {'__module__': '__main__', '__doc__': 'Базовый класс для всех сотрудников', 'emp_count': 0, '__init__': <function Employee.__init__ at 0x03C7D7C8>, 'display_count': <function Employee.display_count at 0x03FA6AE0>, 'display_employee': <function Employee.display_employee at 0x03FA6B28>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>}
Python автоматически удаляет ненужные объекты (встроенные типы или экземпляры классов), чтобы освободить пространство памяти. С помощью процесса ‘Garbage Collection’ Python периодически восстанавливает блоки памяти, которые больше не используются.
Сборщик мусора Python запускается во время выполнения программы и тогда, когда количество ссылок на объект достигает нуля. С изменением количества обращений к нему, меняется количество ссылок.
Когда объект присваивают новой переменной или добавляют в контейнер (список, кортеж, словарь), количество ссылок объекта увеличивается. Количество ссылок на объект уменьшается, когда он удаляется с помощью del, или его ссылка выходит за пределы видимости. Когда количество ссылок достигает нуля, Python автоматически собирает его.
a = 40 # создали объект <40>
b = a # увеличивает количество ссылок <40>
c = [b] # увеличивает количество ссылок <40>
del a # уменьшает количество ссылок <40>
b = 100 # уменьшает количество ссылок <40>
c[0] = -1 # уменьшает количество ссылок <40>
Обычно вы не заметите, когда сборщик мусора уничтожает экземпляр и очищает свое пространство. Но классом можно реализовать специальный метод __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.
Наследование — это процесс, когда один класс наследует атрибуты и методы другого. Класс, чьи свойства и методы наследуются, называют Родителем или Суперклассом. А класс, свойства которого наследуются — класс-потомок или Подкласс.
Вместо того, чтобы начинать с нуля, вы можете создать класс, на основе уже существующего. Укажите родительский класс в круглых скобках после имени нового класса.
Класс наследник наследует атрибуты своего родительского класса. Вы можете использовать эти атрибуты так, как будто они определены в классе наследнике. Он может переопределять элементы данных и методы родителя.
Классы наследники объявляются так, как и родительские классы. Только, список наследуемых классов, указан после имени класса.
class SubClassName(ParentClass1[, ParentClass2, ...]):
"""Необязательная строка документации класса"""
class_suite
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 obj |
| 3 | __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 <module>
print(counter.__secret_count)
AttributeError: 'JustCounter' object has no attribute '__secret_count'
Вы можете получить доступ к таким атрибутам, так object._className__attrName. Если вы замените свою последнюю строку следующим образом, то она будет работать.
...
print(counter._JustCounter__secret_count)
При выполнении кода, получаем результат:
1
2
2