okpython.net
Все для начинающих

ООП в Python: классы и объекты

Понятие класса и объекта в 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

Для создания классов в Python используется инструкция class, которая в общем виде может быть представлена в следующем формате:

class ClassName(SuperclassName_1, ..., SuperclassName_n):
    
    '''Документация класса ClassName.__doc__'''
    
    # Атрибут данных класса.
    class_data = class_value
    
    # Конструктор класса (инициализация экземпляра).	
    def __init__(self, init_arg):
        # Атрибут данных конкретного экземпляра.        
        self.init_obj_data = init_arg    
    
    # Атрибут-метод экземпляров.	
    def class_method(self, arg):
        # Атрибут данных конкретного экземпляра.        
        self.obj_data = arg

В заголовке инструкции сперва записывается служебное слово class, затем через пробел указывается имя класса, которое по принятому в Python соглашению обычно записывается в нотации CapWords, далее в скобках перечисляются имена наследуемых классов (их еще называют суперклассами) и завершается заголовок двоеточием. Если создаваемый класс не наследует других классов, заголовок инструкции разрешается записывать в формате 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). После создания экземпляра класса можно начинать пользоваться объявленными атрибутами: получать к ним доступ, изменять, удалять или же добавлять новые атрибуты.

Доступ к атрибутам классов и объектов в Python

Доступ к открытому атрибуту вне класса или объекта может быть получен с помощью имени этого атрибута, указанного через точку после имени класса или объекта в формате 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).

Помимо синтаксиса с точкой операции над атрибутами объектов в Python могут проводиться и при помощи предназначенных для этого встроенных функций:

  • hasattr(object, name) – функция проверяет наличие в объекте object атрибута с именем name, возвращая True в случае успеха и False в противном случае. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута (см. пример №3).
  • getattr(object, name[, default]) – возвращает значение атрибута name переданного объекта object, если он существует. В противном случае функция возвращает значение по умолчанию default, если оно было передано, или возбуждает исключение AttributeError. В качестве обязательных аргументов функции передаются объект и строка с именем искомого в нем атрибута.
  • setattr(object, name, value) – устанавливает новое значение value атрибуту с именем name переданного объекта object, если он существует. В противном случае интерпретатор создает новый атрибут с указанным именем и присваивает ему переданное значение. В качестве первых двух аргументов функции передаются объект и строка с именем искомого в нем атрибута.
  • delattr(object, name) – удаляет из объекта 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)
Атрибуты класса: {'__module__': '__main__', 'attr_1': 1, 'set_attr': <function main...}
Атрибуты экземпляра: {}
Атрибуты экземпляра: {'attr_2': 2}
Атрибуты экземпляра: {'attr_2': 2.1, 'attr_3': 3}
Атрибуты экземпляра: {'attr_3': 3.1}
3.1
3.1





























		
			

Пример №5. Атрибуты классов и объектов в Python (часть 3).

Стоит добавить, что поскольку в Python объекты присутствуют повсюду, атрибут __dict__ имеется не только у классов, но и у других объектов, например, объектов функций.

Статические методы и методы класса в Python

Как было показано выше, обычные методы экземпляров в момент своего вызова в качестве первого аргумента автоматически получают экземпляр класса, посредством которого они вызываются. Поэтому их и называют методами экземпляров, поскольку они подразумевают воздействие на объекты экземпляров класса. Однако в Python присутствуют еще две разновидности методов: методы классов, которые при вызове в качестве первого аргумента вместо объекта экземпляра автоматически получают объект самого класса, а также статические методы, которые представляют собой обычные функции, определенные внутри класса и не требующие передачи экземпляра класса в качестве первого аргумента. Для получения таких методов служат встроенные функции classmethod(method) и staticmethod(method), которые необходимо вызывать внутри классов. Обе функции помечают переданный объект метода method как специальный, т.е. требующий передачи объекта самого класса для метода класса и, соответственно, не требующий передачи экземпляра класса для статического метода (см. пример №6).

Код Результат pythonCodes
# Определяем класс.
class TestClass: 
    # Обычный метод экземпляров.
    def object_method(self): 
        print('Object')               
    
    # Метод класса.
    def class_method(cls): 
        print('Class')            
    
    # Теперь под cls подразумевается объект самого 
    # класса TestClass, а не его экземпляра.
    class_method = classmethod(class_method)
    
    # Статический метод.
    def static_method(): 
        print('Static')
    
    # Теперь метод при вызове не будет 
    # автоматически запрашивать экземпляр.            
    static_method = staticmethod(static_method)
    
# Создаем экземпляр класса.
obj = TestClass()        

# Методу экземпляра в качестве первого аргумента
# автоматически передается экземпляр класса.    
obj.object_method()
# Или передаем вручную при вызове из класса.
TestClass.object_method(obj)

# Метод класса автоматически получает объект самого
# класса TestClass вместо его экземпляра obj.    
obj.class_method()
# При вызове из класса передавать его не нужно.
TestClass.class_method()        

# Статический метод вызывается как обычная ф-ция,
# ничего не знающая ни о классе, ни о его экз-ре.    
obj.static_method()    
TestClass.static_method()
Object
Object
Class
Class
Static
Static

































		
			

Пример №6. Виды методов в классах (часть 1).

Объявить статический метод или метод класса можно более элегантно, если учесть, что обе функции classmethod(method) и staticmethod(method) представляют собой не что иное, как готовые встроенные декораторы. Все что нужно, это использовать синтаксис декораторов, записав перед определениями нужных нам методов строки @classmethod и @staticmethod (см. пример №7).

Код Результат pythonCodes
# Определяем класс.
class TestClass: 
    # Обычный метод экземпляров.
    def object_method(self): 
        print('Object')               

    @classmethod
    def class_method(cls): 
        print('Class')            

    @staticmethod
    def static_method(): 
        print('Static')

# Создаем экземпляр класса.
obj = TestClass()        

# Вызываем методы.
obj.object_method()
TestClass.object_method(obj)

obj.class_method()
TestClass.class_method()        

obj.static_method()    
TestClass.static_method()
Object
Object
Class
Class
Static
Static


















		
			

Пример №7. Виды методов в классах (часть 2).

Следует помнить, что методы экземпляров предназначены для обработки данных каждого отдельного экземпляра класса, в то время как методы класса призваны обрабатывать данные самого класса, являющиеся общими сразу для всех экземпляров этого класса. Например, методы класса могут использоваться для подсчета созданных экземпляров класса (см. пример №8), вести список экземпляров, находящихся в данный момент в оперативной памяти, изменять существующие или устанавливать новые атрибуты-данных класса. Кроме того, если первый аргумент методов экземпляра при определении принято обозначать через self и его нужно передавать явным образом при вызове через класс, то для первого аргумента методов класса принято использовать имя cls, не указывая его явным образом как при вызове из экземпляров, так и вызове через сам класс (еще раз посмотрите пример №7).

Что касается статических методов, то они как и методы класса предназначены для работы с атрибутами класса, а не экземпляра. Но при этом они не предполагают автоматической передачи в качестве первого аргумента объекта класса. Поэтому статические методы могут быть использованы как самые обычные функции, выполняющие какие-либо полезные действия так или иначе связанные с данными класса и определенные в его пространстве имен для более тесной связи с классом, а также уменьшения вероятности конфликта имен в глобальной области видимости. Более того, их можно даже не объявлять статическими при помощи функции staticmethod(method). В таком случае они останутся доступны посредством объекта класса, но станут недоступны для вызовов из экземпляров класса.

Код Результат pythonCodes
# Определяем класс.
class TestClass: 
    # Атрибут данных класса используем
    # как счетчик экземпляров, созданных 
    # за все время работы программы.
    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(TestClass.num_obj)                    

# Создаем экземпляры класса,
# попутно выводя кол-во экземпляров.
obj_1 = TestClass()        
TestClass.print_num_obj()
obj_2 = TestClass()         
TestClass.print_num_obj()

obj_3 = TestClass()         
# Статический метод можно вызывать
# также из любого экземпляра.
obj_3.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 из любой точки программы, где имеется ссылка на экземпляр класса.

Однако способ реального контроля за доступом к важным атрибутам класса Python нам все-таки предоставляет. Делается это при помощи встроенной функции property(fget=None, fset=None, fdel=None, doc=None), которая позволяет создать свойство класса или, по-другому, управляемый атрибут. Функция принимает четыре аргумента со значениями по умолчанию None:

  • 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 get_pi(self): 
        # Возвращаем значение Пи.    
        return self._pi    
    
    # Вызывается при попытке установки атрибута.
    def set_pi(self, pi_val):                
        # Выводим предупреждение.    
        print('Изменение запрещено!')  
                                
    # Вызывается при попытке удаления атрибута.
    def del_pi(self):
        # Просто выводим предупреждение. 
        print('Удаление невозможно!')                 
    
    # Документация управляемого атрибута.
    doc_pi = 'Только чтение!'
    
    # Организуем управление атрибутом.            
    pi = property(get_pi, doc=doc_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

Как упоминалось в начале параграфа, наследование позволяет создавать новый класс, называемый подклассом, на основе уже существующего класса, называемого суперклассом. Более того, в 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.1, т.к. атрибут был найден в скобках в 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. В качестве необязательных аргументов она принимает тип type (т.е. объект подкласса), начиная с которого будет производиться поиск суперкласса, содержащего целевой метод, а также конкретный объект экземпляра подкласса или объект самого подкласса object-or-type, для которого требуется получить доступ к методу (см. пример №17).

Код Результат pythonCodes
# Создаем суперкласс.
class A: 
    # Статический метод суперкласса.
    @staticmethod
    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»')              
    
    # Расширяем 2-й метод из суперкласса A.
    def method_2(self):
        # Начинаем поиск метода с класса A.
        # Можно и через super().method_2().
        super(B, self).method_2()
        # Своя функциональность.
        print('«method_2 in class B»')

# Наследует суперкласс B.
class C(B): 
    # Расширяем статический метод класса A.
    @staticmethod        
    def method_0():
        # Начинаем поиск метода с класса A.
        super(B, C).method_0()
        # Своя функциональность.            
        print('«method_0 in class C»')
        
    # Расширяем 1-й метод из суперкласса B.
    def method_1(self):
        # Начинаем поиск метода с класса B.
        super(C, self).method_1()
        # Своя функциональность.
        print('«method_1 in class C»')        
    
    # Расширяем 2-й метод из суперкласса A.
    def method_2(self):
        # Начинаем поиск метода с класса A.
        super(B, self).method_2()
        # Своя функциональность.
        print('«method_2 in class C»')              
        
# Создаем экземпляр подкласса C. 
obj = C()
# «method_0 in class A»
# «method_0 in class C»
obj.method_0()        
print()    
# «method_1 in class A» 
# «method_1 in class B»
# «method_1 in class C»
obj.method_1()        
print()
# «method_2 in class A» 
# «method_2 in class C»
obj.method_2() 
print()    
# Вызываем вне класса, указав подкласс, начиная с
# которого ищем суперкласс с методом, а также
# целевой объект. Выведет «method_1 in class A».
super(B, obj).method_1()       
print()
# «method_0 in class A»
# «method_1 in class B»
super(C, obj).method_1()
«method_0 in class A»
«method_0 in class C»

«method_1 in class A»
«method_1 in class B»
«method_1 in class C»

«method_2 in class A»
«method_2 in class C»

«method_1 in class A»

«method_1 in class A»
«method_1 in class B»
































































		
			

Пример №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()

# Посмотрим его словарь атрибутов.
# {'x': 2, '_A__y': 1, '_B__y': 2}.
print(obj.__dict__)        
        
# Выведет 2 (x) и 1 (_A__y).
obj.get_a() 
# Выведет 2 (x) и 2 (_B__y). 
obj.get_b()
{'x': 2, '_A__y': 1, '_B__y': 2}
2
1
2
2








































		
			

Пример №18. Наследование классов в Python (часть 4).

В нашем примере благодаря наличию приставки из двух символов подчеркивания атрибуты остались доступны по короткому имени внутри классов, но стали недоступны извне, поскольку имена этих атрибутов были дополнены интерпретатором именами их классов до _A__y и _B__y, что и показал словарь атрибутов obj.__dict__ после вызова методов obj.set_a() и obj.set_b(). При этом атрибут x, который использовался без спецприставки, был методом obj.set_b() перезаписан.

Композиция классов в Python

Композиция классов – это еще один принцип ООП, использующийся в 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.

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

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

Магические методы и перегрузка операторов в Python

Перегрузка операторов в 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         
# Получим 15.         
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__ (вызов функции) и др.

Значительно большее количество имеющихся магических методов можно найти, например, в подразделе «Special method names» раздела «Data model» официального справочного руководства.

Абстрактные классы и методы в Python

Абстрактный класс – это класс, содержащий один или несколько абстрактных методов, т.е. объявленных, но не реализованных методов.

Абстрактный класс не может использоваться для создания экземпляров. Для этого он должен быть сперва унаследован, а все его абстрактные методы реализованы. Только тогда полученный подкласс можно будет использовать для получения экземпляров, в противном случае будет возбуждено исключение (см. пример №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. Переопределение абстрактных методов в подклассах.

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

Конструкторы и деструкторы в Python

Как говорилось выше, конструктор представляет собой магический метод, который автоматически вызывается при создании каждого нового экземпляра класса, в котором он определен. Это позволяет передавать вновь создаваемым экземплярам начальные данные, тем самым инициализируя некоторое состояние объекта еще до начала его использования. Для объявления конструкторов используется имя __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 обладает всеми преимуществами абстрактного подхода в программировании. В частности ему присущи:
    • полиморфизм – способность функций и методов обрабатывать данные разных типов;
    • инкапсуляция – механизм, который позволяет объединять данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя;
    • наследование – еще одна концепция объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность с возможностью их изменения и добавления своих данных и функциональности;
    • композиция (агрегирование) – возможность создания классов, включающих в себя вызовы уже существующих классов.
  • Процедура создания класса в простейшем случае сводится к объявлению имени класса в заголовке инструкции class и перечислению в теле класса его атрибутов, состоящих из атрибутов данных и атрибутов-методов. Что касается создания экземпляров класса, то делается это по аналогии с вызовами обычной функции, т.е. при помощи пары круглых скобок после имени класса в формате ClassName(arg_1, arg_2, ... arg_n).
  • Доступ к открытому атрибуту вне класса или объекта может быть получен с помощью имени этого атрибута, указанного через точку после имени класса или объекта в формате obj.attr. Для добавления нового атрибута или изменения значения уже существующего используется привычная нам инструкция присваивания obj.new_attr = value или, соответственно, obj.attr = new_value, а удаление осуществляется инструкцией del obj.attr
  • Помимо синтаксиса с точкой операции над атрибутами объектов в Python могут проводиться также при помощи предназначенных для этого встроенных функций: hasattr(object, name) – проверка наличия атрибута с именем name; getattr(object, name[, default]) – получение значения атрибута name, если он существует; setattr(object, name, value) – установка нового значения value атрибуту с именем name, если он существует; delattr(object, name) – удаление атрибута с именем name, если объект позволяет это сделать.
  • Получить доступ или посмотреть текущий набор атрибутов класса или экземпляра можно еще и при помощи словаря атрибутов object.__dict__, который динамически изменяется в процессе изменения как количества доступных атрибутов, так и их значений.
  • Что касается методов, то Python предлагает нам три их основные разновидности: методы экземпляров, которые в момент своего вызова в качестве первого аргумента автоматически получают экземпляр класса, посредством которого они вызываются; методы классов, которые при вызове в качестве первого аргумента вместо объекта экземпляра автоматически получают объект самого класса; статические методы, которые представляют собой обычные функции, определенные внутри класса и не требующие передачи экземпляра класса в качестве первого аргумента. Для создания методов классов и статических методов служат встроенные функции classmethod(method) и staticmethod(method), которые необходимо вызывать внутри классов либо использовать в качестве декораторов.
  • По умолчанию все атрибуты и методы класса являются открытыми. Однако, если необходимо пометить атрибут в качестве предназначенного для служебного пользования, его следует начинать с одного знака подчеркивания, например, _name. Атрибут все равно останется доступным вне класса по его имени, но другие программисты будут знать о его статусе. Для большей защиты от изменений извне можно использовать два символа нижнего подчеркивания, например, __name. В этом случае атрибут станет недоступен вне класса по его имени, т.к. оно будет искажено интерпретатором до формата _Class__name. При этом не стоит забывать, что данный способ не обеспечивает настоящего сокрытия данных, т.к. зная имя вмещающего класса всегда можно обратиться к таким атрибутам по их расширенному имени _Class__attr_name из любой точки программы, где имеется ссылка на экземпляр класса.
  • Кроме того, реальный контроль за доступом к важным атрибутам класса предоставляет встроенная функция property(fget=None, fset=None, fdel=None, doc=None), которая возвращает объект свойства, присваиваемый имени атрибута, которым требуется управлять и который будет находиться в области видимости класса и наследоваться всеми его экземплярами. Благодаря использованию этой функции при попытке доступа к атрибуту по его имени будет вызываться один из методов, передаваемых ей в качестве аргументов. Вызываться функция должна внутри класса. При этом разрешается использовать ее в качестве декоратора, что делает код более компактным и читабельным.
  • В Python поддерживается механизм множественного наследования, при котором подкласс может наследовать атрибуты и методы сразу нескольких суперклассов. Все что нужно, это перечислить наследуемые суперклассы в порядке их приоритета в скобках в заголовке инструкции class. При этом подклассы не только наследуют данные и методы своих суперклассов, но также могут переопределять их или же определять свои собственные атрибуты данных и методы.
  • Поиск унаследованных атрибутов выполняется сначала в объекте экземпляра, затем в классе, из которого был создан экземпляр, и далее, по умолчанию, во всех суперклассах в направлении снизу вверх и слева направо в дереве объектов. Как только будет найдено первое вхождение атрибута, поиск прекращается.
  • В ходе наследования доступ к оригинальным методам может быть получен не только напрямую через имя суперкласса SuperClass.method(), но также и через встроенную функцию super([type[, object-or-type]]), возвращающую специальный объект-посредник, делегирующий вызовы метода данного объекта экземпляра или подкласса суперклассу указанного типа type.
  • Помимо наследования возможности одних классов могут передаваться другим посредством композиции, которая подразумевает использование в определении класса вызовов других классов. В результате получаемые экземпляры класса как бы собираются из экземпляров других классов, реализуя подход формирования целого из частей. Такой подход бывает полезным в крупных системах, в которых множественное наследование большого числа классов может приводить к определенным трудностям, а также в случаях, когда нужно повторно использовать лишь небольшую часть возможностей базового класса.
  • Еще одной возможностью, предоставляемой Python в классах, является перегрузка операторов посредством магических методов, которые позволяют перегружать все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к атрибутам и т.д. Благодаря этому классы могут участвовать в обычных операциях, что делает их более похожими на встроенные типы.
  • Доступна в Python и возможность использования абстрактных классов, т.е. классов, содержащих один или несколько абстрактных методов. При этом следует помнить, что они не могут использоваться для создания экземпляров. Для этого абстрактный класс должен быть сперва унаследован, а все его абстрактные методы реализованы. Только после этого полученный подкласс можно использовать для получения экземпляров, в противном случае будет возбуждено исключение.
  • Если созданные объекты после выполнения своего предназначения становятся ненужными, интерпретатор задействует автоматический сборщик мусора, вызывающий деструктор __del__ для окончательного удаления объектов. Обычно это происходит, когда объект выходит за пределы текущей области видимости или счетчик ссылок на него становится равным нулю.

Вопросы и задания для самоконтроля

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.m
m установлен в 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
Экземпляр создан!
5































			

17. Дополнительные тесты по теме расположены в разделе «ООП: классы и объекты» нашего сборника тестов по основам Питона.

18. Дополнительные упражнения и задачи по теме расположены в разделе «ООП: классы и объекты» нашего сборника задач и упражнений по основам языка программирования Python.

Быстрый переход к другим страницам