Книга: Как устроен Python. Гид для разработчиков, программистов и интересующихся
Назад: 20. Юникод
Дальше: 22. Субклассирование

21. Классы

Строки, словари, файлы и целые числа — все это объекты. Даже функции представляют собой объекты. В Python почти все является объектом. Есть и исключения: ключевые слова (например, in) объектами не являются. Кроме того, имена переменных объектами не являются, но указывают на них. В этой главе мы углубимся в тему и разберемся, что же собой представляет объект.

Термин «объект» многозначен. Одно из определений объектно-ориентированного программирования подразумевает использование структур для группировки данных (состояния) и методов (функций для изменения состояния). Многие объектно-ориентированные языки, такие как C++, Java и Python, используют классы для определения состояния, которое может храниться в объекте, и методов для изменения этого состояния. Если классы являются определениями состояния и методов, экземпляры являются воплощениями этих классов. Как правило, когда разработчик говорит об объектах, он имеет в виду экземпляры классов.

В языке Python str — имя класса, используемого для хранения строк. Класс str определяет методы строк.

Чтобы создать экземпляр класса str с именем b, используйте синтаксис строковых литералов Python:

>>> b = "I'm a string"

>>> b

"I'm a string"

Если программисты начнут обсуждать b, вы можете услышать самые разные термины. «b — это строка», «b — это объект», «b — это экземпляр строки»… Пожалуй, последняя формулировка самая конкретная. Однако b при этом не является классом строки.

515073.png 

Рис. 21.1. Объект строки. У каждого объекта имеется тип, которым в действительности является класс объекта. В данном случае это класс str

ПРИМЕЧАНИЕ

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

>>> c = str("I'm a string"),

так как Python автоматически создает строку при заключении символов в кавычки. Термин «литерал» в данном случае означает специальный синтаксис создания строк, встроенный в Python.

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

>>> num = 42

>>> answer = str(num)

>>> answer

'42'

 

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

515090.png 

Рис. 21.2. Обновленная версия объекта строки. Тип изменился на __class__, потому что при анализе объекта атрибут __class__ указывает на класс объекта. Этот класс содержит разные методы (на схеме показан только метод capitalize, но есть много других). Методы тоже являются объектами, как видно из диаграммы

21.1. Планирование класса

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

Если вы решили, что хотите моделировать что-либо с помощью класса, задайте себе следующие вопросы:

• У него есть имя?

• Какими свойствами он обладает?

• Присущи ли эти свойства всем экземплярам класса? А именно:

• Какие свойства являются общими для класса в целом?

• Какие из этих свойств уникальны для каждого экземпляра?

• Какие операции он выполняет?

Рассмотрим конкретный пример. Предположим, вы работаете на горнолыжном курорте и хотите моделировать использование подъемников. Один из вариантов основан на создании класса, определяющего кресло на подъемник. Подъемник (если вдруг вы никогда им не пользовались) оснащается множеством кресел. Лыжники встают в очередь у основания горы, чтобы сесть на кресла, а затем сходят с них на вершине.

Разумеется, модель необходимо до определенного уровня абстрагировать. Не нужно моделировать каждое низкоуровневое свойство кресла. Например, для расчета нагрузки неважно, из какого материала сделано кресло — из алюминия, стали или дерева. С другой стороны, это может быть важно для некоторых лыжников.

Есть ли имя у моделируемой сущности? Да, «кресло». Среди свойств кресла можно выделить номер, вместимость, наличие планки безопасности и мягких сидений. Если погрузиться немного глубже, вместимость можно разбить на максимальную вместимость и текущую занятость. Максимальная емкость должна оставаться постоянной, тогда как занятость может изменяться в любой момент времени.

С креслом связан ряд операций — например, добавление людей у подножия горы и выход на вершине. Другим действием может стать позиция планки безопасности. Мы будем обрабатывать загрузку и выгрузку посетителей, но в нашей модели позиция планки безопасности будет игнорироваться.

21.2. Определение класса

Ниже приведен класс Python, представляющий кресло на подъемнике. Сначала рассмотрим простое определение класса; комментарии пронумерованы для последующего обсуждения:

>>> class Chair: # 1

... ''' A Chair on a chairlift ''' # 2

... max_occupants = 4 # 3

...

... def __init__(self, id): # 4

... self.id = id # 5

... self.count = 0

...

... def load(self, number): # 6

... self.count += number

...

... def unload(self, number): # 7

... self.count -= number

Ключевое слово class в Python определяет класс. Классу должно быть присвоено имя (1), за которым следует двоеточие. Вспомните, что в Python за двоеточием следует блок с отступом (кроме использования в срезах). Также обратите внимание на последовательные отступы под определением класса и на то, что имя класса начинается с прописной буквы.

ПРИМЕЧАНИЕ

Имена классов записываются в «верблюжьем регистре». Так как Chair — одно слово, вы могли этого и не заметить. В отличие от функций, в именах которых слова соединяются символами подчеркивания, в «верблюжьем регистре» каждое слово начинается с прописной буквы, а сами слова просто соединяются без разделителей. Обычно в качестве имен классов используются имена существительные. В Python имена классов не могут начинаться с цифр. Ниже приведены примеры имен классов — как хороших, так и плохих.

• Kitten # хорошо

• jaguar # плохо - начинается со строчной

• SnowLeopard # хорошо - "верблюжий регистр"

• White_Tiger # плохо - содержит подчеркивания

• 9Lives # плохо - начинается с цифры

За дополнительной информацией о выборе имен классов обращайтесь к PEP 8.

Следует заметить, что многие встроенные типы не соблюдают это правило: str, int, float и т.д.

Непосредственно после объявления класса можно вставить строку документации (2). Это самая обычная строка. Обратите внимание: если эта строка заключена в тройные кавычки, она может состоять из нескольких абзацев. Строки документации не обязательны, но они могут пригодиться читателям вашего кода; кроме того, они выводятся функцией help при анализе кода в REPL. Используйте строки документации осмотрительно, и она принесет огромную пользу.

Внутри тела класса, снабженного отступами, можно создать атрибуты класса (3). Атрибут класса используется для хранения состояния, общего для всех экземпляров класса. В нашем примере любое кресло, которое мы будем создавать, может вмещать до четырех посетителей. У атрибутов классов есть свои преимущества. Так как значение задается на уровне класса, вам не придется повторяться и задавать его при создании каждого нового кресла. С другой стороны, ваши кресла будут жестко запрограммированы так, чтобы они поддерживали только четыре места. Позднее вы узнаете, как переопределить атрибут класса.

Затем следует команда def (4). Все выглядит так, словно мы определяем функцию внутри тела класса. Все верно, если не считать того, что функция, определяемая прямо в теле класса, называется методом. Так как этот метод имеет специальное имя __init__, он называется конструктором. Метод получает два параметра, self и id. У большинства методов первым параметром является self. Его можно интерпретировать как экземпляр класса.

Конструктор вызывается при создании экземпляра класса. Если рассматривать класс как «фабрику», которая предоставляет шаблон или чертеж для создания экземпляров, то конструктор инициализирует состояние этих экземпляров. Конструктор получает экземпляр во входных данных (параметр self) и обновляет его внутри метода. Python помогает вам и передает экземпляр автоматически. Впрочем, это может породить путаницу, но об этом позднее.

Внутри тела конструктора (5) (оно снабжено отступом, потому что следует за двоеточием) присоединяются два атрибута, которые будут уникальными для экземпляра: id и count. На большинстве подъемников каждое кресло помечается уникальным номером. Атрибут id представляет число. Кроме того, на одном кресле могут ехать несколько лыжников — их количество хранится в атрибуте count и инициализируется нулем. Обратите внимание: конструктор ничего не возвращает, но обновляет значения, уникальные для экземпляра.

ПРИМЕЧАНИЕ

В Python есть встроенная функция id, но вы также можете использовать это имя как имя атрибута класса. Функция id при этом остается доступной. Каждый раз, когда вы хотите обратиться к атрибуту id, проводится поиск по экземпляру. Если экземпляру было присвоено имя chair, то для получения значения id следует использовать запись chair.id. Таким образом, встроенная функция не замещается.

О завершении логики конструктора можно судить по исчезновению уровня отступа. Мы видим определение другого метода (6), load. Этот метод представляет операцию, которая может выполняться экземпляром класса. В данном случае кресло может загружать пассажиров, а этот метод сообщает экземпляру, что нужно делать в подобных ситуациях. И снова self (экземпляр) является первым параметром метода. Второй параметр, number, содержит количество людей, садящихся на кресло. Помните, что кресло на подъемнике обычно вмещает несколько (до четырех в нашем примере) человек. Когда лыжник садится на кресло, нужно вызвать метод load для кресла, а внутри тела этого метода обновить атрибут count экземпляра.

Также существует парный метод unload (7), который должен вызываться при сходе лыжника с подъемника на вершине горы.

515120.png 

Рис. 21.3. Создание экземпляра класса. Python создает за вас новый тип. Все атрибуты класса или методы будут храниться в атрибутах нового класса. Атрибуты экземпляров (id и count) в классе отсутствуют, потому что они определяются для экземпляров

ПРИМЕЧАНИЕ

Не бойтесь методов. Вы уже видели многие методы — например, метод .capitalize, определенный для строки. Методы представляют собой функции, присоединенные к классу. Метод вызывается не сам по себе, а для конкретного экземпляра класса:

>>> 'matt'.capitalize()

'Matt'

 

Таким образом, создать класс несложно. Сначала вы определяете нужные атрибуты. Те атрибуты, которые существуют на уровне класса, размещаются в определении класса. Атрибуты, уникальные для каждого экземпляра, размещаются в конструкторе. Вы также можете определить методы с операциями, изменяющими экземпляр класса. После того как класс будет определен, Python создает переменную с именем класса, которая указывает на класс:

>>> Chair

<class '__main__.Chair'>

Для получения расширенной информации о классе можно воспользоваться функцией dir. Обратите внимание: атрибуты класса определяются в классе, и к ним можно обращаться с указанием имени класса:

>>> dir(Chair)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',

'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',

'__hash__', '__init__', '__le__', '__lt__', '__module__',

'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',

'__setattr__', '__sizeof__', '__str__', '__subclasshook__',

'__weakref__', 'load', 'max_occupants', 'unload']

 

>>> Chair.max_occupants

4

На диаграмме в этом разделе показано, как хранятся атрибуты и методы класса. Также список атрибутов и методов можно просмотреть из REPL. Так как в Python нет ничего, кроме объектов, все они обладают атрибутом __class__:

>>> Chair.__class__

<class 'type'>

>>> Chair.max_occupants.__class__

<class 'int'>

>>> Chair.__init__.__class__

<class 'function'>

>>> Chair.load.__class__

<class 'function'>

>>> Chair.unload.__class__

<class 'function'>

Методы также определяются на уровне класса, но атрибуты экземпляров — нет. Так как атрибуты экземпляров уникальны для объекта, они хранятся в экземпляре.

Если в вашем классе или его методах определены строки документации, вы можете просмотреть их при помощи help:

>>> help(Chair)

Help on class Chair in module __main__:

 

class Chair(builtins.object)

| A Chair on a chairlift

|

| Methods defined here:

|

| __init__(self, id)

|

| load(self, number)

|

| unload(self, number)

|

| ------------------------------------------------------

| Data descriptors defined here:

|

| __dict__

| dictionary for instance variables (if defined)

|

| __weakref__

| list of weak references to the object (if defined)

|

| ------------------------------------------------------

| Data and other attributes defined here:

|

| max_occupants = 4

21.3. Создание экземпляра класса

Итак, вы определили класс, моделирующий кресло на подъемнике, и можете создавать экземпляры этого класса. Класс Chair можно сравнить с фаб­рикой: он берет заготовки объектов и превращает их в объекты кресел.

Если говорить конкретно, то эта задача решается методом конструктора __init__. В первом параметре передается минимальный объект self, то есть «заготовка». Python назначает объекту атрибут __class__ (указывающий на класс Chair) перед тем, как передавать его конструктору.

Возможно, другая аналогия поможет вам лучше понять, как работают классы: вспомните, как в мультфильмах изображают рождение детей. Нерожденные дети обитают где-то в облаках. В один прекрасный момент прилетает аист, забирает ребенка с облака и доставляет его в колыбель в родительском доме. При вызове конструктора Python забирает ребенка с облака (получает объект). Он доставляет ребенка в дом, делает его членом семьи (присваивая атрибуту __class__ значение Chair или другое значение, соответствующее вашему классу). Когда ребенок окажется в доме, его можно будет одеть, помыть и т.д. Помните, что объекты хранят состояние, которое может изменяться при помощи операций.

Ниже приведен код создания кресла с номером 21 на языке Python. При вызове класса (то есть указании имени класса с круглыми скобками) вы сообщаете Python о необходимости вызова конструктора. В отличие от некоторых языков, в Python не нужно использовать ключевое слово new или указывать тип; достаточно поставить круглые скобки с параметрами конструктора после имени класса:

>>> chair = Chair(21)

Еще раз уточним терминологию: переменная chair указывает на объект, то есть экземпляр. Она не указывает на класс. Объект относится к классу Chair. Экземпляр содержит ряд атрибутов, включая count и id.

Чтобы обратиться к атрибуту экземпляра, следует указать его экземпляр (chair):

>>> chair.count

0

 

>>> chair.id

21

В Python используется определенная иерархия поиска атрибутов. Сначала Python ищет атрибут в экземпляре. Если поиск оказывается безуспешным, Python переходит к поиску атрибута в классе (так как экземпляры знают, к какому классу они принадлежат). Если и на этот раз поиск завершится неудачей, Python выдает ошибку AttributeError (атрибут отсутствует). Атрибут max_occupants обычно хранится в классе, но к нему также можно обратиться через экземпляр:

>>> chair.max_occupants

4

Во внутренней реализации Python заменяет это обращение следующим:

>>> chair.__class__.max_occupants

4

517339.png 

Рис. 21.4. Процесс построения объекта. При вызове конструктора «аист» — Python приносит конструктору «ребенка» — объект (self). У этого объекта установлен атрибут __class__, но конструктор может свободно изменять экземпляр, добавляя новые атрибуты. Объект превращается в chair

Поиск атрибутов отличается от поиска переменных. Вспомните, что Python начинает поиск переменных с локальной области видимости, затем переходит к глобальной области видимости, затем к встроенной — и в итоге выдает ошибку NameError, если поиск не дал результатов. Поиск атрибутов начинается с экземпляра, затем переходит к классу, а если атрибут не найден, выдается ошибка AttributeError.

21.4. Вызов метода

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

>>> chair.load(3)

Кратко разберем синтаксис вызова метода. Сначала указывается экземпляр (chair), за которым следует точка. Точка в Python обозначает поиск атрибута (если только она не следует за числовым литералом). Когда вы видите, что за экземпляром следует точка, помните, что Python будет искать то, что идет после точки.

Сначала Python ищет load в экземпляре. В экземпляре этот атрибут найти не удается (вспомните, что в конструкторе для экземпляра были назначены только атрибуты count и id). Однако экземпляр также содержит ссылку на свой класс. Так как поиск в экземпляре завершился неудачей, Python переходит к поиску этих атрибутов в классе. Метод .load определен для класса Chair, поэтому Python возвращает его. Круглые скобки обозначают вызов метода, а число 3 передается в параметре метода.

Вспомните, как выглядело объявление load:

... def load(self, number): # 6

... self.count += number

В объявлении указаны два параметра, self и number, а при вызове передается только один параметр 3. Почему количество параметров не совпадает? Параметр self представляет экземпляр (chair в данном случае). Python вызывает метод .load, передавая chair в параметре self и 3 в параметре number. Фактически Python берет на себя все хлопоты, связанные с параметром self, и передает его автоматически.

ПРИМЕЧАНИЕ

Когда вы используете вызов вида

chair.load(3),

во внутренней реализации используется вызов следующего вида:

Chair.load(chair, 3).

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

21.5. Анализ экземпляра

Если у вас имеется экземпляр и вы хотите узнать его атрибуты, есть несколько вариантов. Информацию можно посмотреть в документации (если она существует). Можно прочитать код, в котором определяется класс. Наконец, можно воспользоваться функцией dir, которая проанализирует его за вас:

>>> dir(chair)

['__class__', '__delattr__', '__dict__', '__dir__',

'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',

'__gt__', '__hash__', '__init__', '__le__', '__lt__',

'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',

'__repr__', '__setattr__', '__sizeof__', '__str__',

'__subclasshook__', '__weakref__', 'count', 'id', 'load',

'max_occupants', 'unload']

Напомним, что функция dir перечисляет атрибуты объекта. Обратившись к документации dir, вы увидите, что приведенное выше определение dir не совсем правильно. В документации говорится:

«…возвращает упорядоченный по алфавиту список имен, включающий (некоторые) атрибуты заданного объекта, а также атрибуты, доступные из него».

help(dir) (наше выделение)

Функция выводит атрибуты, доступные из объекта. Фактическое состояние экземпляра хранится в атрибуте __dict__ — словаре, связывающем имена атрибутов со значениями:

>>> chair.__dict__

{'count': 3, 'id': 21}

Итак, в экземпляре на самом деле хранятся только атрибуты count и id, а другие атрибуты доступны через класс. Где же хранится класс?

В атрибуте __class__:

>>> chair.__class__

<class '__main__.Chair'>

Важно, чтобы экземпляр знал свой класс, потому что в классе хранятся методы и атрибуты класса.

21.6. Приватный и защищенный доступ

В некоторых языках существует концепция приватных атрибутов и методов. Предполагается, что эти методы являются подробностями реализации и не должны вызываться конечными пользователями. Более того, язык программирования может блокировать доступ к ним.

Python не пытается в чем-то препятствовать пользователям. Предполагается, что вы взрослый человек и способны отвечать за свои действия. Если вы хотите получить к чему-либо доступ, вы сможете это сделать. Тем не менее будьте готовы принять последствия.

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

>>> class CorrectChair:

... ''' A Chair on a chairlift '''

... max_occupants = 4

...

... def __init__(self, id):

... self.id = id

... self.count = 0

...

... def load(self, number):

... new_val = self._check(self.count + number)

... self.count = new_val

...

... def unload(self, number):

... new_val = self._check(self.count - number)

... self.count = new_val

...

... def _check(self, number):

... if number < 0 or number > self.max_occupants:

... raise ValueError('Invalid count:{}'.format(

... number))

... return number

Метод ._check считается приватным — к нему должны обращаться только экземпляры. Приватные методы вызываются методами .load и .unload класса. При желании вы сможете вызвать их за пределами класса. Тем не менее делать этого не следует — все компоненты с символом подчеркивания считаются подробностями реализации, которые могут отсутствовать в будущих версиях класса.

21.7. Простая программа, моделирующая поток посетителей

Воспользуемся классом для моделирования потока лыжников на горнолыжном курорте. Мы сделаем ряд базовых допущений — например, что на каждом кресле могут с равной вероятностью ехать от 0 до max_occupants лыжников. Класс включает подъемник, загружает его и работает в бесконечном цикле. Четыре раза в секунду выводится текущая статистика:

import random

import time

 

class CorrectChair:

''' A Chair on a chairlift '''

max_occupants = 4

def __init__(self, id):

self.id = id

self.count = 0

 

def load(self, number):

new_val = self._check(self.count + number)

self.count = new_val

 

def unload(self, number):

new_val = self._check(self.count - number)

self.count = new_val

 

def _check(self, number):

if number < 0 or number > self.max_occupants:

raise ValueError('Invalid count:{}'.format(

number))

return number

 

NUM_CHAIRS = 100

 

chairs = []

for num in range(1, NUM_CHAIRS + 1):

chairs.append(CorrectChair(num))

 

def avg(chairs):

total = 0

for c in chairs:

total += c.count

return total/len(chairs)

 

in_use = []

transported = 0

while True:

# загрузка

loading = chairs.pop(0)

in_use.append(loading)

in_use[-1].load(random.randint(0, CorrectChair.max_occupants))

 

# выгрузка

if len(in_use) > NUM_CHAIRS / 2:

unloading = in_use.pop(0)

transported += unloading.count

unloading.unload(unloading.count)

chairs.append(unloading)

print('Loading Chair {} Count:{} Avg:{:.2} Total:{}'.format

(loading.id, loading.count, avg(in_use), transported))

time.sleep(.25)

Эта программа будет бесконечно выводить количество лыжников на подъемнике. Данные выводятся на терминал, но функция print может быть заменена кодом вывода данных в CSV-файл.

Изменив всего два числа (глобальное значение NUM_CHAIRS и атрибут класса CorrectChair.max_occupants), вы сможете изменить поведение модели для моделирования большего или меньшего подъемника. Вызов random.randint можно заменить функцией, которая более точно представляет распределение нагрузки.

21.8. Итоги

В этой главе классы были рассмотрены более подробно. Мы обсудили терминологию, связанную с классами. Можно говорить «объект» или «экземпляр»; эти термины можно считать синонимами. Каждый объект связан с некоторым классом. Класс — своего рода фабрика, определяющая поведение объектов/экземпляров.

Объект создается специальным методом, который называется конструктором. Этому методу присваивается имя __init__. Вы также можете определять собственные методы классов.

При создании класса необходимо как следует поразмыслить. Какими атрибутами должен обладать класс? Если атрибут остается постоянным для всех объектов, определите его в классе. Если атрибут уникален для объекта, задайте его в конструкторе.

21.9. Упражнения

1. Представьте, что вы проектируете приложение для банка. Как должна выглядеть модель клиента? Какими атрибутами она должна обладать? Какие методы она должна поддерживать?

2. Представьте, что вы создаете игру из серии Super Mario. Нужно определить класс для представления героя игры Марио. Как он будет выглядеть? Если вы не знакомы с играми серии Super Mario, используйте свою любимую видео- или настольную игру для моделирования игрока.

3. Создайте класс для моделирования твитов (сообщений в «Твиттере»). Если вы не знаете, что такое «Твиттер», в Википедии приводится следующее определение: «[…] социальная сеть для публичного обмена сообщениями при помощи веб-интерфейса, SMS, средств мгновенного обмена сообщениями или сторонних программ-клиентов для пользователей интернета любого возраста».

4. Создайте класс для моделирования бытового электроприбора (тостер, стиральная машина, холодильник и т.д.).

Назад: 20. Юникод
Дальше: 22. Субклассирование