Атрибуты созданного экземпляра класса можно добавлять, изменять или удалять в любое время, используя для доступа к ним точечную запись. Если построить инструкцию, в которой присвоить значение атрибуту, то можно изменить значение, содержащееся внутри существующего атрибута, либо создать новый с указанным именем и содержащий присвоенное значение:
имя-экземпляра.имя-атрибута = значение
del имя-экземпляра.имя-атрибута
Альтернативным способом добавления, изменения либо удаления переменной экземпляра является использование встроенных функций Python:
getattr(имя-экземпляра, 'имя-атрибута') - возвращает значение атрибута экземпляра класса;
hasattr(имя-экземпляра, 'имя-атрибута') - возвращает True, если значение атрибута существует в экземпляре, в противном случае возвращает False;
setattr(имя-экземпляра, 'имя-атрибута', значение) - модифицирует существующее значение атрибута либо создает новый атрибут для экземпляра;
delattr(имя-экземпляра, 'имя-атрибута') - удаляет атрибут из экземпляра.
Имена атрибутов, автоматически предоставляемые интерпретатором Python, всегда содержат символ подчеркивания, чтобы обозначить «частный» характер этих атрибутов — их не следует изменять либо удалять. Таким же образом вы можете добавить свои собственные атрибуты, обозначив их как «частные», но не забывайте, что они, как и все другие, могут быть изменены.
Допустим, у нас имеется следующий класс Bird:
class Bird:
count = 0
def __init__(self, say):
self.say = say
Bird.count += 1
def talk(self):
return self.say
1. Начните новую программу на Python, сделав доступными функции класса Bird, созданного в предыдущем примере.
from Bird import *
2. Затем создайте экземпляр класса, после чего добавьте, используя точечную запись, новый атрибут с присвоенным значением.
chick = Bird("Cheep, cheep!")
chick.age = "1 month"
3. Теперь выведите значения, хранящиеся в обоих атрибутах экземпляра.
print( 'nChick Says:' , chick.talk() )
print( 'Chick age:' , chick.age )
4. Измените новый атрибут с помощью точечной записи и выведите его новое значение.
chick.age = "2 month"
print( 'Chick Now:' , chick.age )
5. Опять измените новый атрибут, используя в этот раз встроенную функцию.
setattr(chick, 'age', '3 month')
6. После этого выведите список всех атрибутов экземпляра, не являющихся частными, и соответствующие им значения, используя встроенную функцию.
print( 'nChick Attributes...' )
for atttribute in dir( chick ):
if atttribute[0] != '_':
print( atttribute, ':', getattr( chick , atttribute ) )
Данный цикл пропускает все атрибуты, чьи имена начинаются с символа подчеркивания, так что «частные» атрибуты как __name__, __doc__ и т.п. не попадут в результирующий список.
7. Наконец удалите новый атрибут и проверьте его отсутствие, используя встроенные функции.
delattr(chick, 'age')
print( 'nChick age Attribute?' , hasattr( chick , 'age' ) )
Весь наш код выглядит следующим образом:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Bird:
count = 0
def __init__(self, say):
self.say = say
Bird.count += 1
def talk(self):
return self.say
chick = Bird('Cheep, cheep!')
chick.age = '1 month'
print('nChick Says:', chick.talk())
print('Chick Age:', chick.age)
chick.age = '2 month'
print('Chick Now:', chick.age)
setattr(chick, 'age', '3 month')
print('nChick Attributes...')
for attribute in dir(chick):
if attribute[0] != '_':
print(attribute, ':', getattr(chick, attribute))
delattr(chick, 'age')
print('nChick age Attribute?', hasattr(chick, 'age'))
8. Сохраните файл в вашем рабочем каталоге, затем откройте командную строку и запустите программу — вы увидите результат обращения к атрибутам экземпляра.
Интересное видео по теме для читателей:
Атрибуты классов и статические методы
Последнее обновление: 28.01.2022
Атрибуты класса
Кроме атрибутов объектов в классе можно определять атрибуты классов. Подобные атрибуты определяются в виде переменных уровня класса. Например:
class Person: type = "Person" description = "Describes a person" print(Person.type) # Person print(Person.description) # Describes a person Person.type = "Class Person" print(Person.type) # Class Person
Здесь в классе Person определено два атрибута: type, который хранит имя класса, и description, который хранит описание класса.
Для обращения к атрибутам класса мы можем использовать имя класса, например: Person.type
, и, как и атрибуты объекта, мы можем получать и изменять их значения.
Подобные атрибуты являются общими для всех объектов класса:
class Person: type = "Person" def __init__(self, name): self.name = name tom = Person("Tom") bob = Person("Bob") print(tom.type) # Person print(bob.type) # Person # изменим атрибут класса Person.type = "Class Person" print(tom.type) # Class Person print(bob.type) # Class Person
Атрибуты класса могут применяться для таких ситуаций, когда нам надо определить некоторые общие данные для всех объектов. Например:
class Person: default_name = "Undefined" def __init__(self, name): if name: self.name = name else: self.name = Person.default_name tom = Person("Tom") bob = Person("") print(tom.name) # Tom print(bob.name) # Undefined
В данном случае атрибут default_name хранит имя по умолчанию. И если в конструктор передана пустая строка для имени, то атрибуту name передается значение
атрибута класса default_name. Для обращения к атрибуту класса внутри методов можно применять имя класса
self.name = Person.default_name
Атрибут класса
Возможна ситуация, когда атрибут класса и атрибут объекта совпадает по имени. Если в коде для атрибута объекта не задано значение, то для него может применяться
значение атрибута класса:
class Person: name = "Undefined" def print_name(self): print(self.name) tom = Person() bob = Person() tom.print_name() # Undefined bob.print_name() # Undefined bob.name = "Bob" bob.print_name() # Bob tom.print_name() # Undefined
Здесь метод print_name использует атрибут объект name, однако нигде в коде этот атрибут не устанавливается. Зато на уровне класса задан атрибут name.
Поэтому при первом обращении к методу print_name, в нем будет использоваться значение атрибута класса:
tom = Person() bob = Person() tom.print_name() # Undefined bob.print_name() # Undefined
Однако далее мы можем поменять установить атрибут объекта:
bob.name = "Bob" bob.print_name() # Bob tom.print_name() # Undefined
Причем второй объект — tom продолжит использовать атрибут класса. И если мы изменим атрибут класса, соответственно значение tom.name
тоже изменится:
tom = Person() bob = Person() tom.print_name() # Undefined bob.print_name() # Undefined Person.name = "Some Person" # меняем значение атрибута класса bob.name = "Bob" # устанавливаем атрибут объекта bob.print_name() # Bob tom.print_name() # Some Person
Статические методы
Кроме обычных методов класс может определять статические методы. Такие методы предваряются аннотацией @staticmethod и относятся
в целом к классу. Статические методы обычно определяют поведение, которое не зависит от конкретного объекта:
class Person: __type = "Person" @staticmethod def print_type(): print(Person.__type) Person.print_type() # Person - обращение к статическому методу через имя класса tom = Person() tom.print_type() # Person - обращение к статическому методу через имя объекта
В данном случае в классе Person определен атрибут класса __type
, который хранит значение, общее для всего класса — название класса. Причем поскольку
название атрибута предваряется двумя подчеркиваниями, то данный атрибут будет приватным, что защитит от недопустимого изменения.
Также в классе Person определен статический метод print_type
, который выводит на консоль значение атрибута __type. Действие этого метода не зависит от
конкретного объекта и относится в целом ко всему классу — вне зависимости от объекта на консоль будет выводится одно и то же значение атрибута __type. Поэтому такой метод
можно сделать статическим.
На этом занятии
мы поговорим о работе с атрибутами класса и его экземплярами. Я напомню, что класс
можно воспринимать как некое пространство имен, в котором записаны свойства и
методы. Например, если вернуться к классу Point (представления
точки на плоскости):
class Point: MAX_COORD = 100 MIN_COORD = 0 def __init__(self, x, y): self.x = x self.y = y def set_coord(self, x, y): self.x = x self.y = y
то здесь мы
видим определение четырех атрибутов: двух свойств MAX_COORD и MIN_COORD и двух методов __init__ и set_coord. Это атрибуты
класса и при создании экземпляров:
pt1 = Point(1, 2) pt2 = Point(10, 20)
Эти атрибуты
остаются в пространстве имен класса, не копируются в экземпляры. Но из
экземпляров мы можем совершенно спокойно к ним обращаться, так как пространство
имен объектов содержит ссылку на внешнее пространство имен класса. Если
какой-либо атрибут не существует в экземпляре, то поиск переходит во внешнее
пространство, то есть, в класс и поиск продолжается там. Поэтому мы совершенно
спокойно можем через экземпляр обратиться к свойству класса MAX_COORD:
И получается,
что атрибуты и методы класса – это общие данные для всех его экземпляров.
Далее, когда мы
обращаемся к атрибутам класса внутри методов, объявленных в этом классе, то
должны не просто прописать их имена:
def set_coord(self, x, y): if MIN_COORD <= x <= MAX_COORD: self.x = x self.y = y
а явно указать
перед ними ссылку на класс, то есть, на пространство имен. Либо так:
if Point.MIN_COORD <= x <= Point.MAX_COORD:
но лучше через self:
if self.MIN_COORD <= x <= self.MAX_COORD:
Здесь self – это ссылка на
экземпляр класса, из которого метод вызывается, поэтому мы можем через этот
параметр обращаться к атрибутам класса.
Обо всем этом мы
с вами уже говорили, я лишь еще раз повторил эти важные моменты. А теперь один
нюанс, о который спотыкаются многие начинающие программисты. Давайте
предположим, что нам нужен метод, который бы изменял значение атрибута класса MIN_COORD. Пропишем его
как обычный метод:
def set_bound(self, left): self.MIN_COORD = left
Иногда ошибочно
здесь рассуждают так. Мы обращаемся к атрибуту класса MIN_COORD и присваиваем
ему новое значение left. Те из вас, кто внимательно смотрел предыдущие
занятия, понимают, в чем ошибочность такого рассуждения. Да, когда мы через self (ссылку на
объект) записываем имя атрибута и присваиваем ему какое-либо значение, то
оператор присваивания создает этот атрибут в локальной области видимости, то
есть, в самом объекте. В результате, у нас появляется новое локальное свойство
в экземпляре класса:
pt1.set_bound(-100) print(pt1.__dict__)
А в самом классе
одноименный атрибут остается без изменений:
Поэтому,
правильнее было бы здесь объявить метод уровня класса и через него менять
значения атрибутов MIN_COORD и MAX_COORD:
@classmethod def set_bound(cls, left): cls.MIN_COORD = left
Тогда в самом
объекте не будет создаваться никаких дополнительных свойств, а в классе
изменится значение переменной MIN_COORD, так, как мы
этого и хотели.
Будем полагать,
что мы теперь с вами хорошо понимаем, как обращаться к атрибутам класса и его
объектов. Во второй части занятия я хочу вам рассказать о четырех магических
методах, которые используются при работе с атрибутами:
-
__setattr__(self, key, value)__ – автоматически
вызывается при изменении свойства key класса; -
__getattribute__(self,
item) – автоматически вызывается при получении свойства класса с именем item; -
__getattr__(self, item) – автоматически
вызывается при получении несуществующего свойства item класса; -
__delattr__(self,
item) – автоматически вызывается при удалении свойства item (не важно:
существует оно или нет).
Работают они
достаточно просто. Начнем с метода __getattribute__ и с его помощью ограничим
доступ к приватным свойствам __x и __y экземпляра. Для
простоты я переопределю класс Point, следующим образом:
class Point: MAX_COORD = 100 MIN_COORD = 0 def __init__(self, x, y): self.__x = x self.__y = y def __getattribute__(self, item): print("__getattribute__") return object.__getattribute__(self, item)
Здесь добавлен новый
магический метод __getattribute__. Он автоматически вызывается, когда
идет считывание атрибута через экземпляр класса. Например, при обращении к
свойству MIN_COORD:
Или к приватному
свойству через специальное имя:
Но раз это так,
то давайте явно запретим считывать такой атрибут из экземпляра класса. Для
этого пропишем в методе __getattribute__ проверку:
def __getattribute__(self, item): if item == "_Point__x": raise ValueError("Private attribute") else: return object.__getattribute__(self, item)
То есть, мы
смотрим, если идет обращение к приватному атрибуту по внешнему имени _Point__x, то генерируем
исключение ValueError. И,
действительно, после запуска программы видим отображение этой ошибки в консоли.
Вот так, через магический метод __getattribute__ можно
реализовывать определенную логику при обращении к атрибутам через экземпляр класса.
Следующий
магический метод __setattr__ автоматически вызывается в момент
присваивания атрибуту нового значения. Пропишем формально этот метод в классе Point:
def __setattr__(self, key, value): print("__setattr__") object.__setattr__(self, key, value)
После запуска
видим несколько сообщений «__setattr__». Это связано с тем, что в
момент создания экземпляров класса в инициализаторе __init__ создавались
локальные свойства __x и __y. В этот момент
вызывался данный метод. Также в переопределенном методе __setattr__ мы должны
вызывать соответствующий метод из базового класса object, иначе,
локальные свойства в экземплярах создаваться не будут.
Давайте теперь
для примера через этот магический метод запретим создание локального свойства с
именем z. Сделаем это
следующим образом:
def __setattr__(self, key, value): if key == 'z': raise AttributeError("недопустимое имя атрибута") else: object.__setattr__(self, key, value)
Обратите
внимание, что внутри метода __setattr__ нельзя менять свойства напрямую:
def __setattr__(self, key, value): if key == 'z': raise AttributeError("недопустимое имя атрибута") else: self.__x = value
В этом случае
метод __setattr__ начнет
выполняться по рекурсии, пока не возникнет ошибка достижения максимальной
глубины рекурсии. Если нужно сделать что-то подобное, то используйте коллекцию __dict__:
self.__dict__[key] = value
или, если
требуется стандартное поведение метода, то вызывайте его из класса object, как это мы
прописывали вначале:
object.__setattr__(self, key, value)
Следующий
магический метод __getattr__ автоматически вызывается, если идет
обращение к несуществующему атрибуту. Добавим его в наш класс:
def __getattr__(self, item): print("__getattr__: " + item)
Если ниже
обратиться к несуществующему свойству, например:
то увидим сообщение
«__getattr__: a» и значение None, которое вернул данный метод. Если же
прописать существующий атрибут:
то этот
магический метод уже не вызывается. Зачем он может понадобиться? Например, нам
необходимо определить класс, в котором при обращении к несуществующим атрибутам
возвращается значение False, а не генерируется исключение. Для
этого записывается метод __getattr__ в виде:
def __getattr__(self, item): return False
Наконец, последний
магический метод __delattr__ вызывается в момент удаления
какого-либо атрибута из экземпляра класса:
def __delattr__(self, item): print("__delattr__: "+item)
Добавим новое
локальное свойство в экземпляр pt1:
затем выполним
команду его удаления:
и видим, что
действительно был вызван метод __delattr__, правда, сам атрибут удален не
был:
Это из-за того,
что внутри этого метода нужно вызвать соответствующий метод класса object, который и
выполняет непосредственное удаление:
def __delattr__(self, item): object.__delattr__(self, item)
Я думаю, что из
этого занятия вы хорошо себе уяснили, как происходит обращение к атрибутам
класса и как можно управлять доступом к ним через магические методы.
Видео по теме
Понятие класса и объекта
Понятия класса и объекта являются ключевыми в объектно-ориентированных языках программирования, к которым относится также и язык
Python. В нем всё является объектами – и строки, и списки, и словари, и функции, и даже сами классы.
Класс (от англ. class) – это шаблон кода,
который используется для описания структуры и создания объектов, т.е. экземпляров этого класса.
По сути классы в Python представляют собой типы данных, а объекты – отдельные экземпляры этих типов. Например, в инструкциях
a = ‘abcdef’ и b = ‘ABCDEF’ переменным a и b
присваиваются объекты строкового типа данных, т.е. экземпляры встроенного класса string.
Если говорить совсем уж просто, то класс можно сравнить с чертежом, по которому создаются объекты. Достаточно создать всего лишь один класс и далее можно будет порождать любое количество его
экземпляров, изменяя и дополняя их по мере необходимости.
Благодаря использованию классов Python обладает всеми преимуществами абстрактного подхода в программировании. В частности
Питону присущи:
-
Полиморфизм (от англ. polymorphism) – способность функций и методов обрабатывать
данные разных типов. Простыми примерами полиморфизма могут служить: встроенная функция len(s), которая может работать с разными типами встроенных
последовательностей, возвращая количество их элементов; метод s.count(x), который подсчитывает количество символов x в
строке s (один тип данных) или, например, количество элементов x в списке s
(другой тип данных); операторы сложения и умножения, которые для чисел (один тип данных) производят операции сложения и умножения, а для строк (другой тип данных) конкатенацию и повторение
конкатенации указанное число раз. -
Инкапсуляция (от англ. encapsulation) – механизм, который позволяет объединять
данные и методы, работающие с этими данными, в единый объект, скрывая при этом детали реализации от пользователя. Грамотно написанный класс должен ограничивать доступность своих членов и
взаимодействовать с пользователем только с помощью своего интерфейса. При этом нужно помнить, что в Python инкапсуляция работает лишь на уровне соглашения
между программистами о том, какие атрибуты следует считать общедоступными, а какие – приватными (подробнее об этом мы поговорим чуть ниже). -
Наследование (от англ. inheritance) – еще одна концепция
объектно-ориентированного программирования, которая позволяет на основе одного суперкласса создавать множественные подклассы, заимствующие его данные и функциональность
с возможностью их изменения и добавления своих данных и функциональности. При этом в Python поддерживается возможность множественного наследования,
когда подкласс наследует атрибуты сразу нескольких суперклассов одновременно. Таким образом, наследование способствует повторному использованию уже написанных компонентов
программы, помогая избежать избыточности исходного кода. -
Композиция (агрегирование) (от англ. composition) – возможность создания
классов, включающих в себя вызовы уже существующих классов. В результате при создании объектов такого класса они будут состоять или содержать объекты других классов. Здесь
важно не путать композицию с наследованием, в том числе множественным. Ведь при наследовании подкласс получает все атрибуты (т.е. возможности) своих суперклассов, а при
композиции класс-агрегатор атрибуты не наследует, он просто создает объекты этих классов для использования, например, в качестве своих атрибутов или локальных переменных своих методов.
Благодаря всем этим преимуществам ООП у программистов появляется возможность параллельной разработки отдельных независимых модулей в виде классов, способных скрывать от внешнего мира детали
своего внутреннего устройства и объединяющихся в масштабные комплексные приложения посредством предназначенных для этого интерфейсов. Данный процесс можно наглядно представить на примере из
жизни, когда отдельные модули МКС собираются независимо друг от друга разными странами, а затем на орбите собираются в одну монолитную космическую станцию за счет имеющихся интерфейсов,
обеспечивающих взаимодействие готовых модулей между собой.
Создание классов и объектов
Для создания классов в Python используется
инструкция class, которая в общем виде может быть представлена в следующем формате:
class ClassName(SuperclassName_1, ..., SuperclassName_n):
'''Документация класса ClassName.__doc__'''
# Атрибут данных класса (доступен всем экземплярам).
class_data = class_value
# Конструктор класса (инициализация экземпляра).
def __init__(self, init_arg_1, init_arg_2, ..., init_arg_n):
# Атрибут данных конкретного экземпляра.
self.init_obj_data = init_obj_value
# Атрибут-метод (доступен всем экземплярам).
def class_method(self, arg_1, arg_2, ..., arg_n):
# Атрибут данных конкретного экземпляра.
self.obj_data = obj_value
В заголовке инструкции сперва записывается служебное слово class, затем через пробел указывается имя класса, которое по принятому в
Python соглашению обычно записывается в нотации CupWords, далее в скобках перечисляются имена наследуемых классов
(их еще называют суперклассами) и завершается заголовок двоеточием. Если создаваемый класс не наследует других классов, заголовок инструкции разрешается записывать в формате
class ClassName:, опуская круглые скобки и записывая двоеточие сразу после имени класса. В любом случае после заголовка записывается вложенный блок
инструкций, в котором перечисляются атрибуты данных класса (по умолчанию они доступны для использования всеми создаваемыми экземплярами класса) и атрибуты-методы для обработки его данных,
представляющие собой обычные функции, определенные внутри класса и принимающие в качестве первого аргумента параметр self, которому при вызове методов
интерпретатор автоматически присваивает объект текущего экземпляра класса. При необходимости в начале тела класса разрешается указывать строку документирования класса, а также конструктор
класса __init__(), представляющий собой специальный метод класса, который вызывается автоматически всякий раз при создании нового экземпляра, инициализируя
его исходными данными. Изменение имени конструктора запрещено.
После того, как класс будет определен, можно приступать к созданию его отдельных экземпляров, сохраняя их в переменных или структурах данных. Для этого
нужно после имени класса просто указать круглые скобки, перечислив в них начальные данные для инициализации создаваемого экземпляра (см. пример №1).
# Определяем обычный класс (не наследует других классов).
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='nn')
# Выводим данные на экран.
print('В компании', company, 'работают:')
print(name_1, 'с', date_1)
print(name_2, 'с', date_2)
Компания «Okpython» и ее сотрудники.
В компании «Okpython» работают:
Коля с 30.05.2022
Немо с 07.03.2021
Пример №1. Создание классов и объектов в Python.
В нашем примере мы создали класс Person не наследуя никаких других классов, поэтому круглые скобки в заголовке были опущены
(class Person:). В качестве строки документации мы указали обычную строку в тройных одинарных кавычках, после чего создали атрибут данных класса,
присвоив ему название нашей компании (company = ‘«Okpython»’). Этот атрибут стал доступен как из любого экземпляра класса
(person_1.company или person_2.company), так и напрямую из объекта самого класса
(Person.company). Далее был определен конструктор класса (def __init__(self, name=’Немо’):), который затем при создании
экземпляров класса запустился в автоматическом режиме и создал атрибуты данных создаваемых экземпляров (это атрибуты созданных объектов person_1.name и
person_2.name). Тут важно помнить, что наличие в определении класса конструктора с параметрами влечет за собой необходимость в передаче вновь создаваемым
экземплярам начальных данных для их инициализации, иначе будет получена ошибка (мы подстраховались, использовав аргумент со значением по умолчанию). В конце тела класса мы добавили еще два
метода для установки и получения атрибута данных экземпляров класса (def set_date(self, date): и def get_date(self):). Как и
говорилось, в качестве первого аргумента у методов был задан параметр self, которому при каждом вызове этих методов интерпретатор автоматически присвоил
объекты текущих экземпляров класса (т.е. объекты person_1 и person_2).
После создания класса Person мы создали первый экземпляр этого класса, использовав инструкцию вызова объекта класса
person_1 = Person(‘Коля’) и указав в качестве начальных данных для нашего конструктора значение аргумента ‘Коля’. Далее,
для доступа к атрибутам созданного объекта мы использовали синтаксис с точкой, сохранив в переменной глобальной области видимости имя сотрудника
(name_1 = person_1.name), а также установив и затем также сохранив в переменной дату трудоустройства сотрудника в компанию (инструкции
person_1.set_date(‘30.05.2022’) и date_1 = person_1.get_date()). Проделав тоже самое для второго экземпляра, мы
сохранили в глобальной переменной название компании, использовав атрибут данных класса (company = Person.company), а затем вывели на экран
строку документирования класса и данные сотрудников.
Таким образом, процедура создания класса в простейшем случае сводится к объявлению имени класса ClassName в заголовке инструкции
class и перечислению в теле класса его атрибутов, состоящих из атрибутов данных и атрибутов-методов. Что касается создания экземпляров класса, то делается это
по аналогии с вызовами обычной функции, т.е. при помощи пары круглых скобок после имени класса в формате ClassName(arg_1, arg_2, … arg_n). После создания
экземпляра класса можно начинать пользоваться объявленными атрибутами: получать к ним доступ, изменять, удалять или же добавлять новые атрибуты.
Доступ к атрибутам классов и объектов
Доступ к открытому атрибуту вне класса или объекта может быть получен с помощью имени этого атрибута, указанного через точку после имени класса или
объекта в формате obj.attr. Для добавления нового атрибута или изменения значения уже существующего используется привычная нам инструкция присваивания
obj.new_attr = value или, соответственно, obj.attr = new_value, а удаление осуществляется инструкцией
del obj.attr (см. пример №2).
# Создаем простой класс.
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='nn')
# Изменяем значение атрибута в 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='nn')
# Добавляем новый атрибут 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='nn')
# Такой атрибут во втором объекте отсутствует.
# '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, если
объект позволяет это сделать. В качестве аргументов ей передаются объект и строка с именем искомого в нем атрибута.
# Создаем простой класс.
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='nn')
# Изменяем значение атрибута в 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='nn')
# Добавляем новый атрибут 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='nn')
# Такой атрибут во втором объекте отсутствует.
# '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).
# Создаем класс без методов.
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).
# Создаем класс.
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 присутствуют еще две разновидности методов: методы классов, которые при вызове в качестве первого аргумента
вместо объекта экземпляра автоматически получают объект самого класса, а также статические методы, которые представляют собой обычные функции, определенные
внутри класса и не требующие передачи экземпляра класса в качестве первого аргумента. Для получения таких методов служат встроенные функции
classmethod(method) и staticmethod(method), которые необходимо вызывать внутри классов.
Обе функции помечают переданный объект метода method как специальный, т.е. требующий передачи объекта самого класса для метода класса и, соответственно,
не требующий передачи экземпляра класса для статического метода (см. пример №6).
# Определяем класс.
class Cls:
# Обычный метод экземпляра.
def object_method(self):
print('Object')
# Метод класса.
def class_method(cls):
print('Class')
# Статический метод.
def static_method():
print('Static')
# Теперь под cls подразумевается объект
# самого класса Cls, а не его экземпляра.
class_method = classmethod(class_method)
# Теперь метод при вызове не будет
# автоматически запрашивать экземпляр.
static_method = staticmethod(static_method)
# Создаем экземпляр класса.
obj = Cls()
# Методу экземпляра в качестве первого аргумента
# автоматически передается экземпляр класса.
obj.object_method()
# Или передаем вручную при вызове из класса.
Cls.object_method(obj)
# Метод класса автоматически получает объект
# самого класса Cls вместо его экземпляра obj.
obj.class_method()
# При вызове из класса передавать его не нужно.
Cls.class_method()
# Статический метод вызывается как обычная ф-ция,
# ничего не знающая ни о классе, ни о его экз-ре.
obj.static_method()
Cls.static_method()
Object
Object
Class
Class
Static
Static
Пример №6. Виды методов в классах (часть 1).
Объявить статический метод или метод класса можно более элегантно, если учесть, что обе функции classmethod(method) и
staticmethod(method) представляют собой не что иное, как готовые встроенные
декораторы. Все что нужно, это использовать синтаксис декораторов, записав перед определениями нужных нам методов
строки @classmethod и @staticmethod (см. пример №7).
# Определяем класс.
class Cls:
# Обычный метод экземпляра.
def object_method(self):
print('Object')
@classmethod
def class_method(cls):
print('Class')
@staticmethod
def static_method():
print('Static')
# Создаем экземпляр класса.
obj = Cls()
# Вызываем методы.
obj.object_method()
Cls.object_method(obj)
obj.class_method()
Cls.class_method()
obj.static_method()
Cls.static_method()
Object
Object
Class
Class
Static
Static
Пример №7. Виды методов в классах (часть 2).
Следует помнить, что методы экземпляра предназначены для обработки данных каждого отдельного экземпляра класса, в то время как методы класса призваны обрабатывать данные самого класса,
являющиеся общими сразу для всех экземпляров этого класса. Например, методы класса могут использоваться для подсчета созданных экземпляров класса (см. пример
№8), вести список экземпляров, находящихся в данный момент в оперативной памяти, изменять существующие или устанавливать новые атрибуты-данных класса.
Кроме того, если первый аргумент методов экземпляра при определении принято обозначать через self и его нужно передавать явным образом при вызове через
класс, то для первого аргумента методов класса принято использовать имя cls, не указывая его явным образом как при вызове из экземпляров, так и вызове через
сам класс (еще раз посмотрите пример №7).
Что касается статических методов, то они как и методы класса предназначены для работы с атрибутами класса, а не экземпляра. Но при этом они не предполагают автоматической передачи в качестве
первого аргумента объекта класса. Поэтому статические методы могут быть использованы как самые обычные функции, выполняющие какие-либо полезные действия так или иначе связанные с данными
класса и определенные в его пространстве имен для более тесной связи с классом, а также уменьшения вероятности конфликта имен в глобальной области видимости. Более того, их можно даже не
объявлять статическими при помощи функции staticmethod(method). В таком случае они останутся доступны посредством объекта класса, но станут недоступны для
вызовов из экземпляров класса.
# Определяем класс.
class Cls:
# Атрибут данных класса используем
# как счетчик экземпляров, созданных
# за все время работы программы.
num_obj = 0
# Конструктор класса.
def __init__(self):
# Вызывается каждый раз при
# создании нового экземпляра.
self.increase_counter()
@classmethod
def increase_counter(cls):
# +1 экземпляр.
cls.num_obj += 1
# Просто функция для вывода.
@staticmethod
def print_num_obj():
print(Cls.num_obj)
# Создаем экземпляры класса,
# попутно выводя кол-во экземпляров.
obj1 = Cls()
Cls.print_num_obj()
obj2 = Cls()
Cls.print_num_obj()
obj3 = Cls()
# Статический метод можно вызывать
# также из любого экземпляра.
obj3.print_num_obj()
1
2
3
Пример №8. Виды методов в классах (часть 3).
Конечно, мы могли бы определить функцию print_num_objects() и вне класса, она по-прежнему выполняла бы свою работу, но, повторимся, тогда она выглядела бы
обособленной от класса (особенно для других программистов, читающих наш код), нарушая при этом принцип инкапсуляции и напрасно захломляя глобальное пространство имен модуля.
Инкапсуляция в классах
По умолчанию все атрибуты и методы класса являются открытыми или публичными (от англ. public). Это значит, что они
доступны не только внутри класса, но и за его пределами. Для этого достаточно использовать синтаксис доступа к атрибутам при помощи точки (например,
obj.attr) или предназначенные для этого встроенные функции (например, getattr(obj, ‘attr’)). Однако бывают случаи,
когда требуется ограничить прямой доступ к атрибутам классов из вызывающего кода воизбежание передачи им некорректных значений или случайного удаления. Примером может служить возраст
человека, который не может быть отрицательным, или значение атрибута, предназначенного для служебного пользования. В таких случаях вполне логично делать атрибуты закрытыми или
приватными (от англ. private), предоставляя доступ к ним посредством интерфейсов взаимодействия в виде специальных
методов, осуществляющих необходимые проверки передаваемых им значений. При этом следует помнить, что в Python инкапсуляция в классах
(т.е. сокрытие данных) поддерживается лишь на уровне соглашения между программистами. Полностью скрыть реализацию класса от целенаправленных злонамеренных действий извне не получится.
Так атрибуты, предназначенные для внутреннего использования, принято начинать с одного символа подчеркивания (например, _var). Встретив такой атрибут,
другой программист будет иметь в виду, что он не предназначен для публичного использования и в дальнейшем может быть даже удален без предварительного уведомления. В тоже время никто не
запрещает изменять такой атрибут на свой страх и риск, надеясь, что это не повлияет на работоспособность класса (см. пример №9).
# Определяем класс числа Пи.
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).
# Определяем обычный класс.
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='nn')
# Получаем возраст сотрудника.
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).
# Определяем обычный класс.
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).
# Определяем класс числа Пи.
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).
# Определяем класс числа Пи.
class NumPi:
# Атрибут для служебного пользования.
# Точность числа Пи по умолчанию.
_pi = 3.141592
# Передаем требуемую точность для экз-ра.
def __init__(self, d):
# Уст-вает экз-ру требуемую точностью Пи.
self._pi = round(NumPi._pi, d)
# Вызывается при попытке обращения к атрибуту.
def pi(self):
# Возвращаем значение Пи.
return self._pi
# Вызывается при попытке установки атрибута.
def set_pi(self, pi_val):
# Выводим предупреждение.
print('Изменение запрещено!')
# Вызывается при попытке удаления атрибута.
def del_pi(self):
# Просто выводим предупреждение.
print('Удаление невозможно!')
# Документация управляемого атрибута.
s_pi = 'Только чтение!'
# Организуем управление атрибутом.
pi = property(pi, doc=s_pi)
pi = pi.setter(set_pi)
pi = pi.deleter(del_pi)
# Выводим документацию.
print(NumPi.pi.__doc__)
# Создаем экземпляр класса.
pi_obj = NumPi(3)
# Атрибут доступен для чтения.
print('pi =', pi_obj.pi)
# Но недоступен для изменения.
pi_obj.pi = 0
# И удаления.
del pi_obj.pi
Только чтение!
pi = 3.142
Изменение запрещено!
Удаление невозможно!
Пример №13. Инкапсуляция в классах (часть 5).
Опять же, весь процесс можно осуществить при помощи декораторов, которые делают код более простым и приятным для восприятия (см. пример №14).
# Определяем класс числа Пи.
class NumPi:
# Атрибут для служебного пользования.
# Точность числа Пи по умолчанию.
_pi = 3.141592
# Передаем требуемую точность для экз-ра.
def __init__(self, d):
# Уст-вает экз-ру требуемую точностью Пи.
self._pi = round(NumPi._pi, d)
# Аналог pi = property(pi, doc='Чтение!').
@property
# Вызывается при попытке обращения к атрибуту.
def pi(self):
'''Чтение!'''
# Возвращаем значение Пи.
return self._pi
# Аналог инструкции pi = pi.setter(pi).
@pi.setter
# Вызывается при попытке установки атрибута.
def pi(self, pi_val):
# Выводим предупреждение.
print('Изменение запрещено!')
# Аналог инструкции pi = pi.deleter(pi).
@pi.deleter
# Вызывается при попытке удаления атрибута.
def pi(self):
# Просто выводим предупреждение.
print('Удаление невозможно!')
# Выводим документацию.
print('О числе:', NumPi.pi.__doc__)
# Создаем экземпляр класса.
pi_obj = NumPi(3)
# Атрибут доступен для чтения.
print('pi =', pi_obj.pi)
# Но недоступен для изменения.
pi_obj.pi = 0
# И удаления.
del pi_obj.pi
О числе: Чтение!
pi = 3.142
Изменение запрещено!
Удаление невозможно!
Пример №14. Инкапсуляция в классах (часть 6).
Обратите внимание, что при использовании декораторов все методы должны иметь одно и то же имя, иначе интерпретатор возбудит исключение.
Наследование классов
Как упоминалось в начале параграфа, наследование позволяет создавать новый класс, называемый подклассом, на основе уже
существующего класса, называемого суперклассом. Более того, в Python поддерживается механизм множественного наследования,
при котором подкласс может наследовать атрибуты и методы сразу нескольких суперклассов. Все что нужно, это перечислить наследуемые суперклассы в порядке их приоритета в скобках в заголовке
инструкции class (см. пример №15).
# Создаем 1-й суперкласс.
class Super_1:
# Атрибут данных класса.
attr_0 = 0.1
# Атрибут данных класса.
attr_1 = 1
# Конструктор 1-го суперкласса.
def __init__(self):
self.p_1 = '«Конструктор 1-го суперкласса.»'
# Атрибут-метод в 1-м суперклассе.
def method_1(self):
print('«method_1»')
# Создаем 2-й суперкласс.
class Super_2:
# Атрибут данных класса.
attr_0 = 0.2
# Атрибут данных класса.
attr_2 = 2
# Конструктор 2-го суперкласса.
def __init__(self):
self.p_2 = '«Конструктор 2-го суперкласса.»'
# Атрибут-метод во 2-м суперклассе.
def method_2(self):
print('«method_2»')
# Создаем подкласс, наследуя оба суперкласса.
class Sub_3(Super_1, Super_2):
# Атрибут данных подкласса.
attr_3 = 3
# Атрибут-метод в подклассе.
def method_3(self):
print('«method_3»')
# Создаем экземпляр подкласса Sub_3.
obj = Sub_3()
# Выведет 1 (атрибут унаследован от 1-го суперкласса).
print('obj.attr_1 ->', obj.attr_1)
# Выведет 2 (атрибут унаследован от 2-го суперкласса).
print('obj.attr_2 ->', obj.attr_2)
# Выведет 3 (значение собственного атрибута).
print('obj.attr_3 ->', obj.attr_3)
# Выведет 0.2, т.к. атрибут был найден в скобках в 1-м
# суперклассе (поиск в скобках идет слева направо).
print('obj.attr_0 ->', obj.attr_0)
# Выведет «method_1» (метод унасл. от 1-го суперкласса).
obj.method_1()
# Выведет «method_2» (метод унасл. от 2-го суперкласса).
obj.method_2()
# Выведет «method_3» (собственный метод).
obj.method_3()
# Выведет «Конструктор 1-го суперкласса.», т.к.
# конструктор был найден в скобках в 1-м суперклассе.
print('obj.p_1 ->', obj.p_1)
# 'Sub_3' object has no attribute 'p_2', т.к.
# конструктор 2-го суперкласса не вызывался.
print('obj.p_2 ->', obj.p_2)
obj.attr_1 -> 1
obj.attr_2 -> 2
obj.attr_3 -> 3
obj.attr_0 -> 0.1
«method_1»
«method_2»
«method_3»
obj.p_1 -> «Конструктор 1-го суперкласса.»
'Sub_3' object has no attribute 'p_2'
Пример №15. Наследование классов в Python (часть 1).
В нашем примере подкласс Sub_3 наследует атрибуты сразу двух суперклассов Super_1 и
Super_2. Однако поскольку оба суперкласса обладают атрибутом с именем attr_0, интерпретатор выбрал для
подкласса значение атрибута суперкласса Super_1, т.к. при наследовании поиск атрибутов ведется сперва в самом подклассе, а затем в списке
наследуемых суперклассов слева направо. Именно поэтому подкласс Sub_3 унаследовал конструктор первого суперкласса. Если бы нам понадобилось отдать
приоритет атрибутам второго суперкласса, его пришлось бы перечислить в скобках первым.
Помимо того, что подклассы наследуют данные и методы своих суперклассов, они могут определять и свои собственные атрибуты. Так в подклассе Sub_3
были определены собственный атрибут данных attr_3 и метод method_3. Но и это еще не все. При необходимости подклассы
могут изменять (специализировать) наследуемые атрибуты или даже полностью замещать их (см. пример №16).
# Создаем суперкласс.
class SuperClass:
# Атрибут данных класса.
attr_1 = 1
# Конструктор суперкласса.
def __init__(self):
self.p_1 = '«Конструктор суперкласса.»'
# 1-й метод в суперклассе.
def method_1(self):
print('«method_1 в суперклассе»')
# 2-й метод в суперклассе.
def method_2(self):
print('«method_2»')
# Создаем подкласс, наследуя суперкласс.
class SubClass(SuperClass):
# Меняем значение атрибута в подклассе.
attr_1 = 2
# Определяем собственный атрибут данных подкласса.
attr_3 = 3
# Переопределяем (расширяем) конструктор.
def __init__(self):
# Вызываем конструктор суперкласса.
SuperClass.__init__(self)
# Добавляем для экземпляра свой атрибут.
self.p_2 = '«Конструктор подкласса.»'
# Переопределяем (расширяем) метод суперкласса.
def method_2(self):
# Вызываем исходный метод суперкласса.
SuperClass.method_2(self)
# Добавляем собственную функциональность.
print('«method_2+»')
# Собственный метод в подклассе.
def method_3(self):
print('«method_3 в подклассе»')
# Создаем экземпляр подкласса SubClass.
obj = SubClass()
# Выведет 2, т.к. атрибут был переопределен в подклассе.
print('obj.attr_1 ->', obj.attr_1)
# Выведет «Конструктор суперкласса.».
print('obj.p_1 ->', obj.p_1)
# Выведет «Конструктор подкласса.».
print('obj.p_2 ->', obj.p_2)
# «method_1 суперкласса» (метод унасл. от суперкласса).
obj.method_1()
# «method_2» и «method_2+» (метод расширен в подклассе).
obj.method_2()
# «method_3 подкласса» (собственный метод).
obj.method_3()
obj.attr_1 -> 2
obj.p_1 -> «Конструктор суперкласса.»
obj.p_2 -> «Конструктор подкласса.»
«method_1 в суперклассе»
«method_2»
«method_2+»
«method_3 в подклассе»
Пример №16. Наследование классов в Python (часть 2).
Следует заметить, что в ходе специализации методов в подклассах внутри замещающего метода обычно предусматривается вызов исходной версии метода наследуемого суперкласса. Это позволяет
выполнить предусмотренный в суперклассе набор действий по умолчанию без необходимости повторного написания уже имеющегося кода. Так в подклассе SubClass
нашего примера мы расширили конструктор и метод method_2 суперкласса, вызвав исходные версии методов вместо того, чтобы заново переписывать их код.
Кстати, доступ к оригинальным методам может быть получен не только напрямую через имя суперкласса, но и через встроенную функцию
super([type[, object-or-type]]), возвращающую специальный объект-посредник, делегирующий вызовы метода суперклассу указанного типа
type. В качестве необязательных аргументов она принимает тип (т.е. объект подкласса), для которого будет производиться поиск суперкласса, а также
конкретный объект экземпляра подкласса или объект самого подкласса, для которого требуется получить доступ к методу (см. пример №17).
# Создаем суперкласс.
class A:
# Статический метод суперкласса.
def method_0():
print('«method_0 in class A»')
# 1-й метод в суперклассе.
def method_1(self):
print('«method_1 in class A»')
# 2-й метод в суперклассе.
def method_2(self):
print('«method_2 in class A»')
# Наследует суперкласс A.
class B(A):
# Расширяем 1-й метод из суперкласса A.
def method_1(self):
# Используем метод из суперкласса A.
# Можно и через super().method_1().
super(B, self).method_1()
# Своя функциональность.
print('«method_1 in class B»')
# Наследует суперкласс B.
class C(B):
# Расширяем 2-й метод из суперкласса A.
def method_2(self):
# Используем метод из суперкласса A.
super(B, self).method_2()
# Своя функциональность.
print('«method_2 in class C»')
# Создаем экземпляр подкласса C.
obj = C()
# «method_1 in class A» и «method_1 in class B».
obj.method_1()
# «method_2 in class A» и «method_2 in class C».
obj.method_2()
# Вызываем вне класса, указав подкласс для которого
# ищем суперкласс с методом, а также целевой объект.
# Выведет «method_1 in class A».
super(B, obj).method_1()
# «method_0 in class A» (указали объект класса).
super(B, C).method_0()
«method_1 in class A»
«method_1 in class B»
«method_2 in class A»
«method_2 in class C»
«method_1 in class A»
«method_0 in class A»
Пример №17. Наследование классов в Python (часть 3).
Как видим, функция super() может вызываться из подкласса без аргументов. В таком случае интерпретатор автоматически использует в качестве аргументов
текущий подкласс и объект self. Кроме того, ее можно вызывать и вне классов, явно передавая требуемый тип и целевой объект.
Теперь рассмотрим подробнее ситуацию, когда подкласс наследует сразу несколько суперклассов с одинаковыми именами атрибутов. Выше мы уже указывали, что в таком случае подклассом будет
унаследовано значение атрибута того суперкласса, который перечисляется в заголовке инструкции class первым. Но что делать, когда важны значения атрибутов
обоих классов? В этом случае, чтобы гарантировать принадлежность атрибута тому классу, который его использует, необходимо в начале имени атрибута поставить два символа подчеркивания везде,
где оно используется этим классом (см. пример №18).
# 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 и предоставляющий
возможность создания классов, включающих в себя вызовы уже существующих классов (см. пример №19).
# Создаем 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 представляет собой процесс перехватывания встроенных операций при помощи
специальных методов классов, называемых магическими методами. Все магические методы имеют специальные имена,
начинающиеся и заканчивающиеся двумя символами подчеркивания, что отличает их от других имен, которые обычно определяются в классах. Встречая такие методы, интерпретатор автоматически
вызывает их при выполнении соответствующих встроенных операций над экземплярами классов, воспринимая возвращаемые ими значения как результаты этих операций.
Перегрузка операторов позволяет классам участвовать в обычных операциях, делая экземпляры классов более похожими на представителей встроенных типов данных. Достигается такой эффект за счет
того, что магические методы позволяют перегружать все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к атрибутам и многие другие операции
(см. пример №20).
# Определяем класс.
class Num:
# Конструктор класса - один из
# самых известных методов перегрузки.
def __init__(self, start_data):
# Начальное значение экземпляра.
self.data = start_data
# Метод перегрузки операции вычитания.
def __sub__(self, n):
# Для целых чисел.
if isinstance(n, int):
# Создаем и возвр. новый экземпляр,
# изменяя хранимое в нем число.
return Num(self.data - n)
# Для других объектов этого типа.
elif isinstance(n, Num):
# Вычитаем хранимые значения.
return Num(self.data - n.data)
# Для других объектов.
else:
print('Операция не поддерживается!')
# При создании экземпляров класса интерпретатор
# автоматически вызывает конструктор класса.
a = Num(25)
b = Num(10)
# А здесь интерпретатор автоматически вызывает
# спец. метод реализации операции вычитания.
c = a - b
# Выводим результат операции на экран.
print('a - b =', c.data)
# Для целых чисел.
d = a - 5
# Выведет 20.
print('a - 5 =', d.data)
# Вещественные числа не поддерживаются.
d = a - 5.7
# Магический метод __add__ не переопределен!
# unsupported operand type(s) for +: 'Num' and 'int'.
d = a + 5
a - b = 15
a - 5 = 20
Операция не поддерживается!
unsupported operand type(s) for +: 'Num' and 'int'
Пример №20. Перегрузка операторов в Python.
В примере мы использовали два метода перегрузки, имеющихся в Python: конструктор класса __init__, а также метод
__sub__, который отвечает за операцию вычитания. Последний метод мы использовали для того, чтобы экземпляры созданного нами типа
Num могли использоваться в операции вычитания целых чисел и других экземпляров этого типа.
Все магические методы могут свободно наследоваться от суперклассов и переопределяться в подклассах, как и любые другие методы. Если же какой-то метод не реализован в классе, это лишь
означает, что соответствующая ему операция не поддерживается данным классом, а попытка применения такой операции будет возбуждать исключение (что мы и увидели при попытке выполнить
инструкцию d = a + 5, содержащую операцию сложения экземпляра a нового типа Num с
целым числом).
Перечислим для ознакомления наиболее часто используемые магические методы: __add__ (сложение), __sub__ (вычитание),
__mul__ (умножение), __truediv__ (деление), __mod__ (остаток от деления),
__pow__ (возведение в степень), __and__ (логическое И), __or__ (логическое ИЛИ),
__getattr__ (обращение к атрибуту), __setattr__ (присваивание атрибуту),
__delattr__ (удаление атрибута), __getitem__ (доступ к элементу по индексу, извлечение среза, итерации),
__setitem__ (присваивание элементу по индексу или срезу), __delitem__ (удаление среза или элемента по индексу),
__len__ (длина), __bool__ (проверка логического значения), __lt__ (меньше),
__gt__ (больше), __le__ (меньше или равно), __ge__ (больше или равно),
__eq__ (равно), __ne__ (не равно), __iter__ (получение итератора),
__iadd__ (комбинированный оператор сложения), __next__ (получение следующего элемента итератора),
__contains__ (оператор проверки на вхождение in), __new__ (создание объекта),
__init__ (конструктор), __del__ (деструктор), __call__ (вызов функции) и др.
Значительно большее количество имеющихся магических методов можно найти, например, на официальном сайте Python в разделе
Data model справочного руководства.
Конструкторы и деструкторы
Как говорилось выше, конструктор представляет собой магический метод, который автоматически вызывается при создании каждого нового
экземпляра класса, в котором он определен. Это позволяет передавать вновь создаваемым экземплярам начальные данные, тем самым инициализируя некоторое состояние объекта еще до начала его
использования. Для объявления конструкторов используется имя __init__, изменять которое запрещается.
Далее, если созданные объекты после выполнения своего предназначения становятся ненужными, имеет смысл удалять их для высвобождения памяти. В Python для
этих целей имеется автоматический сборщик мусора, удаляющий объекты, которые выходят за пределы текущей области видимости или счетчики ссылок на которые становятся равными нулю. Кроме того,
ненужные объекты можно удалять и самостоятельно, используя, например, инструкцию del. В любом случае, если ссылок на объект больше не остается, интерпретатор
вызывает деструктор – еще один магический метод с именем __del__, представляющий собой операцию
окончательного удаления объектов. Используются деструкторы реже конструкторов, но в некоторых ситуациях их применение бывает весьма оправданным. Так деструкторы обычно определяют в
классах, для которых перед стандартным удалением требуется выполнить еще какие-нибудь действия, например, закрыть файл или соединение с базой данных
(см. пример №21).
# Определяем класс.
class Бюджет:
# Конструктор класса инициализирует
# экземпляр начальными данными.
def __init__(self, сумма=100000):
# Создаем атрибут данных экземпляра.
self.сумма = сумма
# Деструктор класса запускается автоматически
# после удаления всех ссылок на объект.
def __del__(self):
# Перед удалением выводим сообщение.
print('Бюджет успешно распилен!')
# Создаем экземпляр класса.
городской_бюджет_1 = Бюджет(1000000)
# Получаем еще одну ссылку на объект.
городской_бюджет_2 = городской_бюджет_1
# Счетчик ссылок на объект уменьшается на 1.
del городской_бюджет_1
# А вот здесь уже будет вызван деструктор.
del городской_бюджет_2
Бюджет успешно распилен!
Пример №21. Конструкторы и деструкторы в 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 в классах, является перегрузка операторов посредством магических методов, которые позволяют перегружать
все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к атрибутам и т.д. Благодаря этому классы могут участвовать в обычных операциях, что делает
их более похожими на встроенные типы. -
Если созданные объекты после выполнения своего предназначения становятся ненужными, интерпретатор задействует автоматический сборщик мусора, вызывающий деструктор
__del__ для окончательного удаления объектов. Обычно это происходит, когда объект выходит за пределы текущей области видимости или счетчик ссылок на
него становится равным нулю.
Помогите проекту, подпишитесь!
Подписка на учебные материалы сайта оформляется сроком на один год и стоит около 5 у.е. После
подписки вам станут доступны следующие возможности.
- Доступ ко всем ответам на вопросы и решениям задач.
- Возможность загрузки учебных кодов и программ на свой компьютер.
- Доступ ко всем тренажерам и видеоурокам (когда появятся).
-
Возможность внести свой скромный вклад в развитие проекта и мира во всем мире,
а также выразить свою благодарить автору за его труд. Нам очень нужна ваша поддержка!
На страницу подписки
Вопросы и задания для самоконтроля
1. Каково основное назначение ООП в языке Python?
Показать решение.
Ответ. Основное назначение ООП в языке Python, да и в программировании в целом, состоит в том, чтобы
обеспечить многократное использование программного кода посредством его разложения на структурные составляющие и последующей адаптации, а не изменения или создания нового кода.
2. Перечислите основные компоненты парадигмы ООП в языке Python?
Показать решение.
Ответ. Полиморфизм, инкапсуляция, наследование и композиция (описание смотрите в начале параграфа).
3. Что представляет собой класс? Как создать класс?
Показать решение.
Ответ. Класс представляет собой шаблон кода, который используется для описания структуры и создания объектов, т.е. экземпляров этого класса.
Для создания классов в Python предназначена инструкция class. В заголовке инструкции указываются имя класса и
перечисляемые в скобках через запятую наследуемые суперклассы, а в теле инструкции – атрибуты класса, представляющие собой данные и методы для их обработки.
4. Как создать экземпляр класса?
Показать решение.
Ответ. Для создания экземпляра класса нужно после имени класса указать круглые скобки, перечислив в них начальные данные для инициализации
создаваемого экземпляра, если конструктор класса это предполагает. Внешне данная инструкция похожа на вызов обычной функции.
# Простейший в мире класс.
class Simple: pass
# Создаем экземпляры класса.
simp_1 = Simple()
simp_2 = Simple()
5. Для чего используется метод __init__?
Показать решение.
Ответ. Конструктор класса __init__, если он присутствует в классе или наследуется им, служит для
автоматической инициализации вновь создаваемых экземпляров класса начальными данными. Важно помнить, что если конструктор определяется с параметрами, которые не предполагают значений
по умолчанию, то при создании экземпляров класса аргументам должны передаваться значения, иначе интерпретатор обязательно возбудит соответствующее исключение.
# Класс с конструктором.
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), которые необходимо вызывать либо использовать в качестве декораторов внутри классов.
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.
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(), так и через встроенную функцию super([type[, object-or-type]]), возвращающую специальный
объект-посредник, делегирующий вызовы метода данного объекта экземпляра или подкласса суперклассу указанного типа type.
12. Что представляет из себя принцип композиции?
Показать решение.
Ответ. Принцип композиции подразумевает использование в определении класса вызовов других классов. В результате получаемые экземпляры класса как
бы собираются из экземпляров других классов, реализуя подход формирования целого из частей. Такой подход бывает полезным в крупных системах, в которых множественное наследование большого
числа классов может приводить к определенным трудностям, а также в случаях, когда нужно повторно использовать лишь небольшую часть возможностей базового класса.
13. Для чего используются магические методы? Как отличить их от обычных методов?
Показать решение.
Ответ. Магические методы позволяют перегружать все операторы выражений, а также такие операции, как ввод и вывод, вызов функций, обращение к
атрибутам и т.д. Благодаря этому классы могут участвовать в обычных операциях, что делает их более похожими на встроенные типы. Все магические методы имеют специальные имена, начинающиеся
и заканчивающиеся двумя символами подчеркивания, что отличает их от других имен, которые обычно определяются в классах.
14. В каких случаях деструктор автоматически вызывается интерпретатором?
Показать решение.
Ответ. Деструктор, представляемый магическим методом __del__, автоматически вызывается сборщиком мусора при
окончательном удалении объектов, которые выходят за пределы текущей области видимости или счетчики ссылок на которые становятся равными нулю.
15. Исправьте в коде все ошибки так, чтобы скрипт заработал.
Показать решение.
# Создаем суперкласс.
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)
# Создаем суперкласс.
# Скобки не нужны.
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
16. Дополнительные упражнения и задачи по теме расположены в разделе
«ООП: классы и объекты»
нашего сборника задач и упражнений по языку программирования Python.
Быстрый переход к другим страницам