9. Классы

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

По сравнению с другими языками программирования, механизм классов в Python добавляет классы с минимумом нового синтаксиса и семантики. Это смесь механизмов классов из C++ и Modula-3. Классы в Python предоставляют полный набор стандартных возможностей Объектно-Ориентированного Программирования: механизм наследования поддерживает множественные базовые классы, производные классы могут переопределять любые методы своего базового класса или классов, и методы могут вызывать методы базовых классов с такими же именами. Объекты могут содержать данные произвольного объема и вида. Как и модули, классы обладают динамической природой Python: они создаются во время выполнения и могут быть изменены позже после создания.

В терминологии C++, обычно члены класса (включая данные) являются публичными (об исключениях читайте ниже Закрытые переменные), а все функции-члены — виртуальными. Как и в Modula-3, здесь нет сокращений для доступа к элементам объекта из его методов: функция-метод объявляется с явным первым аргументом, представляющим объект, который неявно подставляется при вызове. Как и в Smalltalk, сами классы являются объектами. Это обеспечивает семантику импорта и переименования. В отличие от C++ и Modula-3, встроенные типы могут использоваться как базовые классы для расширения пользователем. Так же, как и в C++, большинство встроенных операторов со специальным синтаксисом (арифметические операторы, индексирование и т. д.) могут быть переопределены для экземпляров класса.

(Не имея общепринятой терминологии, чтобы говорить о классах, я буду время от времени использовать термины Smalltalk и C++. Я бы использовал терминологию Modula-3, поскольку его объектно-ориентированная семантика ближе к Python, чем C++, но я думаю, что мало читателей знакомы с этим языком.)

9.1. Несколько слов об именах и объектах

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

9.2. Области видимости и пространства имён в Python

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

Давайте начнём с некоторых определений.

Пространство имен — это отображение имён на объекты. Большинство пространств имён в настоящее время реализованы как словари Python, но обычно это незаметно (за исключением производительности) и может измениться в будущем. Примерами пространств имён являются: набор встроенных имён (включая функции, такие как abs(), и встроенные имена исключений); глобальные имена в модуле; локальные имена в вызове функции. В некотором смысле, набор атрибутов объекта тоже образует пространство имён. Важная деталь, которую нужно знать о пространствах имён — это то, что нет совершенно никакой связи между именами из разных пространств имён; например, в двух разных модулях может быть определена функция maximize без какой-либо путаницы — пользователи модуля должны использовать имя модуля в качестве префикса при вызове функции.

Кстати, я использую слово атрибут для любого имени, следующего после точки — например, в выражении z.real, real — это атрибут объекта z. Строго говоря, обращения к именам в модулях являются обращением к атрибутам: в выражении modname.funcname, modname является объектом модуля, и funcname является его атрибутом. В этом случае есть прямое соответствие между атрибутами модуля и глобальными именами, объявленными в модуле: они используют одно и то же пространство имён! [1]

Атрибуты могут быть доступными только для чтения или изменяемыми. В последнем случае возможно присваивание атрибутам. Атрибуты модуля изменяемы: вы можете записать modname.the_answer = 42. Изменяемые атрибуты также могут быть удалены с помощью инструкции del. Например, del modname.the_answer удалит атрибут the_answer у объекта, названного modname.

Пространства имён создаются в разное время и имеют разные времена жизни. Пространство имён, содержащее встроенные имена, создаётся при запуске интерпретатора Python и никогда не удаляется. Глобальное пространство имён модуля создаётся при чтении определения модуля; обычно оно существует до завершения работы интерпретатора. Инструкции, исполняемые на верхнем уровне работы интерпретатора, считываемые из файла скрипта или интерактивно, считаются частью модуля __main__, поэтому относятся к собственному глобальному пространству имён. (Встроенные имена также существуют в модуле; он называется builtins.)

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

Область видимости — это текстовая область программы Python, в которой пространство имён доступно напрямую. «Доступно напрямую» здесь означает, что обращение к имени без префикса пытается найти его в этом пространстве имён.

Хотя области видимости определяются статически, но используются они динамически. В любой момент исполнения программы у неё есть 3 или 4 вложенные области видимости, чьи пространства имён доступны напрямую:

  • самая внутренняя область видимости, которая просматривается в первую очередь, содержит локальные имена

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

  • предпоследняя область видимости включает глобальные имена текущего модуля

  • самая внешняя областью видимости (просматривается последней) является пространством имён, содержащим встроенные имена

Если имя объявлено глобальным, то все обращения и присваивания к нему передаются непосредственно в предпоследнюю область видимости, содержащую глобальные имена модуля. Для переназначения переменных из внешних областей видимости, можно использовать инструкцию nonlocal. Без такого объявления, эти переменные доступны только для чтения (попытка записи в такую ​​переменную просто создаст новую локальную переменную во внутренней области видимости, оставив внешнюю переменную с таким же именем неизменной).

Как правило, локальная область видимости ссылается на локальные имена (буквально) текущей функции. За пределами функции локальная область видимости ссылается на то же пространство имён, что и глобальная область видимости: пространство имён модуля. Определение класса помещает ещё одно пространство имён в локальную область видимости.

Важно понимать, что области видимости определяются буквально: глобальная область видимости функции определяется модулем, в котором эта функция определена, независимо от того, откуда или под каким именем она вызывается. С другой стороны, фактический поиск имён выполняется динамически, во время выполнения кода. Однако язык постепенно движется в сторону статического разрешения имён на «этапе компиляции», поэтому не стоит полагаться на динамическое разрешение! (На самом деле локальные переменные уже определяются статически.)

Особой причудой Python является то, что если не задействованы инструкции global или nonlocal, то присваивание имён направлены в самую глубокую область видимости. Присваивание не копирует данные — оно лишь связывает имена с объектами. То же верно и для удаления: инструкция del x удаляет привязку x из пространства имён локальной области видимости. Фактически, все операции, которые вводят новые имена, используют локальную область видимости: в частности, инструкция import и определение функций связывают имя модуля или функции в локальной области видимости.

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

9.2.1. Пример областей видимости и пространств имён

Вот демонстрация того, как обращаться к разным областям видимости и пространствам имён, и как global и nonlocal влияют на привязку переменных:

def scope_test():
    def do_local():
        spam = "локальный спам"

    def do_nonlocal():
        nonlocal spam
        spam = "нелокальный спам"

    def do_global():
        global spam
        spam = "глобальный спам"

    spam = "тестовый спам"
    do_local()
    print("После локального присваивания:", spam)
    do_nonlocal()
    print("После нелокального присваивания:", spam)
    do_global()
    print("После глобального присваивания:", spam)

scope_test()
print("В глобальной области видимости:", spam)

При выполнении кода будет выведено следующее:

После локального присваивания: тестовый спам
После нелокального присваивания: нелокальный спам
После глобального присваивания: нелокальный спам
В глобальной области видимости: глобальный спам

Заметьте, как локальное присваивание (используемое по умолчанию) не изменяет значение переменной spam в scope_test. С другой стороны, присваивание с использованием ключевого слова nonlocal изменило значение spam в scope test, и присваивание с использованием global изменяет переменные на уровне модуля.

Также можно увидеть, что до присваивания spam с использованием global не существовало привязки этого имени в области видимости модуля.

9.3. Первый взгляд на классы

Классы привносят немного нового синтаксиса, три новых типа объектов и новую семантику.

9.3.1. Синтаксис определения класса

Самая простая форма определения класса выглядит так:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

Определение класса, как и определение функции (инструкция def), должно быть выполнено перед тем, как оно будет иметь эффект. (Можно поместить определение класса в ветку инструкции if или внутри функции.)

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

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

Когда определение класса заканчивается (обычным завершением блока), создается объект класса. По сути, это обёртка вокруг содержимого пространства имён, созданного определением класса. Мы узнаем больше об объектах классов в следующем разделе. Исходная локальная область видимости (которая действовала непосредственно перед входом в определение класса) восстанавливается, и объект класса привязывается здесь к имени, указанному в заголовке определения класса (ClassName в примере).

9.3.2. Объекты классов

Объекты классов поддерживают два вида операций: обращение к атрибутам и создание экземпляров.

Обращение к атрибуту использует стандартный в Python синтаксис для получения доступа к любым атрибутам: obj.name. Допустимыми именами атрибутов считаются те имена, которые находились в пространстве имён класса во время его создания. Таким образом, если определение класса выглядит так:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

то MyClass.i и MyClass.f являются корректными обращениями к атрибутам, возвращающими соответственно целое число и объект функции. Атрибутам класса также можно присваивать, поэтому вы можете изменить значение MyClass.i. Также существует атрибут __doc__, возвращающий строку документации, принадлежащую классу: "A simple example class".

Создание экземпляра класса использует синтаксис вызова функции. Достаточно представить, что объект класса — это функция без параметров, которая возвращает новый экземпляр этого класса. Например (используя определённый выше класс):

x = MyClass()

создаёт новый экземпляр класса и сохраняет этот объект в локальную переменную x.

Операция создания экземпляра («вызов» объекта класса) создаёт пустой объект. Многим классам нравится создавать объекты, инициализированные определённым начальным состоянием. Поэтому класс может определить специальный метод с именем __init__(), например:

def __init__(self):
    self.data = []

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

x = MyClass()

Конечно, метод __init__() может иметь аргументы для большей гибкости. В этом случае аргументы, переданные оператору создания экземпляра класса, передаются в __init__(). Например,

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. Объекты экземпляров

Что мы можем делать с объектами-экземплярами? Единственная операция, которую понимают экземпляры — обращение к атрибутам. Есть два вида допустимых имён атрибутов: данные и методы.

Атрибуты-данные соответствуют «переменным экземпляра» в Smalltalk и «членам данных» в C++. Атрибуты данных не нужно объявлять; как и локальные переменные, они возникают, когда им впервые присваиваются значения. Например, если x является экземпляром MyClass, созданным выше, следующий фрагмент кода напечатает значение 16, не оставив после себя никаких следов:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

Другой вид атрибута экземпляра — метод. Метод — это функция, которая «принадлежит» объекту.

Методы, доступные у объекту-экземпляру, зависят от его класса. По определению, все атрибуты класса, которые являются функциями, создают соответствующие методы у его экземпляров. В нашем примере x.f — ссылка на метод, потому что MyClass.f — это функция; в отличие от x.i, потому что MyClass.i не является функцией. Но x.f — не то же самое, что MyClass.f — это объект метода, а не объект функции.

9.3.4. Объекта методов

Обычно метод вызывается сразу после обращения к нему:

x.f()

Если x = MyClass(), как указано выше, этот вызов вернёт строку 'hello world'. Однако вызывать метод сразу необязательно: x.f — это объект-метод, его можно сохранить и вызвать позже. Например:

xf = x.f
while True:
    print(xf())

будет продолжать печатать hello world до конца времён.

Что же именно происходит при вызове метода? Вы могли заметить, что x.f() был вызван без аргумента, хотя в определении функции f() указан аргумент. Куда же он подевался? Разумеется, Python должен выбросить исключение, если вызвать функцию, которой нужен аргумент, без него — даже если аргумент на самом деле не используется…

На самом деле, вы, вероятно, уже догадались: особенность методов в том, что объект-экземпляр передаётся в функцию как первый аргумент. В нашем примере вызов x.f() полностью совпадает с MyClass.f(x). Вообще, вызов метода со списком из n аргументов эквивалентен вызову соответствующей функции со списком аргументов, полученным вставкой объекта-экземпляра метода перед первым аргументом.

В целом методы работают следующим образом. Когда у экземпляра запрашивается атрибут, который не является данными, происходит поиск в классе экземпляра. Если найденный атрибут является функцией, то ссылка на объект-экземпляр и функция упаковываются в объект-метод. Когда объект-метод вызывается с некоторым списком аргументов, создаётся новый список из объекта-экземпляра и этих аргументов, и затем функция вызывается с этим новым списком аргументов.

9.3.5. Переменные класса и экземпляра

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

class Dog:

    kind = 'собачий'         # переменная класса, общая для всех экземпляров

    def __init__(self, name):
        self.name = name    # переменная экземпляра, уникальная для каждого объекта

>>> d = Dog('Фидо')
>>> e = Dog('Бадди')
>>> d.kind                  # общее для всех собак
'собачий'
>>> e.kind                  # общее для всех собак
'собачий'
>>> d.name                  # уникально для d
'Фидо'
>>> e.name                  # уникально для e
'Бадди'

Как обсуждалось в Несколько слов об именах и объектах, разделяемые данные могут привести к неожиданным последствиям, если они являются изменяемыми объектами, как списки и словари. Например, список tricks в следующем коде не следует использовать как переменную класса, так как в противном случае один и тот же список будет разделяться всеми экземплярами Dog:

class Dog:

    tricks = []             # ошибочное использование переменной класса

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Фидо')
>>> e = Dog('Бадди')
>>> d.add_trick('сделать переворот')
>>> e.add_trick('прикинуться мёртвым')
>>> d.tricks                # неожиданно общие данные для всех собак
['сделать переворот', 'прикинуться мёртвым']

Правильно спроектированный класс должен использовать вместо этого переменную экземпляра:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # создаётся новый пустой список для каждой собаки

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Фидо')
>>> e = Dog('Бадди')
>>> d.add_trick('сделать переворот')
>>> e.add_trick('прикинуться мёртвым')
>>> d.tricks
['сделать переворот']
>>> e.tricks
['прикинуться мёртвым']

9.4. Полезные замечания

Если в классе и в экземпляре встречаются атрибуты с одинаковыми именем, используется атрибут экземпляра:

>>> class Warehouse:
...    purpose = 'хранение'
...    region = 'запад'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
хранение запад
>>> w2 = Warehouse()
>>> w2.region = 'восток'
>>> print(w2.purpose, w2.region)
хранение восток

К атрибутам данных можно обращаться как через методы, так и через обычных пользователей («клиентов») объекта. Другими словами, классы не позволяют реализовать чисто абстрактные типы данных. На практике, ничего в Python не позволяет полностью скрыть данные — всё основано на соглашениях. (С другой стороны, реализация Python на C может полностью скрыть детали реализации и контролировать доступ к объекту, если это необходимо; это может использоваться расширениями на C.)

Клиенты должны использовать атрибуты данных осторожно — они могут нарушить инварианты, поддерживаемые методами, изменяя их напрямую. Обратите внимание, что клиенты могут добавлять свои собственные атрибуты к экземпляру объекта, не нарушая работу методов, если избегают конфликтов имён — опять же, соглашения об именах могут уберечь от множества проблем.

Внутри методов нет никакой сокращённой формы для обращения к атрибутам данных (или другим методам!). На самом деле, это повышает читаемость кода: нет риска перепутать локальные переменные и переменные экземпляра при беглом просмотре метода.

Первый аргумент метода часто называют self. Это всего лишь общепринятая договорённость: имя self не имеет никакого особого значения в Python. Однако если не придерживаться этой практики, ваш код может стать менее читабельным для других программистов, а некоторые инструменты анализа кода, вроде обозревателей классов, могут полагаться на такое соглашение.

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

# Функция определена вне класса
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'привет мир'

    h = g

Теперь f, g и h являются атрибутами класса C, которые ссылаются на объекты функций, и, следовательно, все они являются методами экземпляров класса Ch в точности эквивалентен g. Обратите внимание, что такая практика обычно только сбивает с толку читателя программы.

Методы могут вызывать другие методы с помощью аргумента self:

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

Методы могут ссылаться на глобальные имена так же, как обычные функции. Глобальная область видимости метода — это модуль, в котором он определён. (Класс никогда не используется как глобальная область видимости.) Хотя поводы использовать глобальные данные внутри метода встречаются редко, сама глобальная область видимости нередко бывает полезна. Например, методы могут обращаться к функциям и модулям, импортированным на уровне модуля, а также к функциям и классам, определённым в нём. Обычно класс, содержащий метод, сам определяется в глобальной области видимости, и в следующем разделе мы рассмотрим причины, по которым метод может захотеть ссылаться на свой класс.

Любое значение в Python — это объект, а значит относится к какому-то классу (который также называется его типом). Он хранится как object.__class__.

9.5. Наследование

Разумеется, механизм языка программирования не достоин называться «классами», без поддержки наследования. Синтаксис определения производного класса выглядит так:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

Имя BaseClassName должно быть определено в пространстве имён, доступном из области, содержащей определение производного класса. Вместо имени базового класса допускаются также другие произвольные выражения. Это может быть полезно, например, когда базовый класс определён в другом модуле:

class DerivedClassName(modname.BaseClassName):

Определение дочернего класса выполняется так же, как и определение класса без родителей. Когда объект класса создаётся, он запоминает своего родителя. Это будет использоваться для разрешения ссылок на атрибуты: если запрашиваемый атрибут не найден в классе, его поиск продолжится у родителя. Это правило применяется рекурсивно, если базовый класс сам является производным.

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

Производные классы могут переопределять методы своих базовых классов. Поскольку методы не имеют специальных привилегий при вызове других методов того же объекта, метод базового класса, который вызывает другой метод, определённый в том же базовом классе, может в конечном итоге вызвать метод производного класса, который переопределяет его. (Для программистов C++: все методы Python фактически являются virtual.)

На самом деле переопределённый метод в производном классе может расширять, а не просто заменять одноименный метод базового класса. Есть простой способ вызвать метод базового класса напрямую: просто вызовите BaseClassName.methodname(self, arguments). Иногда это бывает полезно и для клиентов. (Обратите внимание, что это работает только в том случае, если базовый класс доступен как BaseClassName в глобальной области видимости.)

Python имеет две встроенные функции, которые работают с наследованием:

  • Используйте isinstance() для проверки типа экземпляра: isinstance(obj, int) будет True, только если obj.__class__ имеет значение int или какой-либо производный от int класс.

  • Используйте issubclass() для проверки наследования классов: issubclass(bool, int) имеет значение True, поскольку bool является подклассом int. Однако issubclass(float, int) имеет значение False, поскольку float не является подклассом int.

9.5.1. Множественное наследование

Python также поддерживает форму множественного наследования. Определение класса с несколькими базовыми классами выглядит следующим образом:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

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

На самом деле всё немного сложнее; порядок разрешения методов изменяется динамически для поддержки совместных вызовов super(). Этот подход известен в некоторых других языках с множественным наследованием как вызов следующего метода и является более мощным, чем вызов метода базового класса, встречающийся в языках с одиночным наследованием.

Динамическое упорядочивание необходимо, поскольку во всех случаях множественного наследования присутствует одно или несколько ромбовидных отношений (где хотя бы к одному из родительских классов можно получить доступ через несколько путей из самого нижнего класса). Например, все классы наследуются от object, поэтому в любом случае множественного наследования существует более одного пути для достижения object. Чтобы предотвратить многократное обращение к базовым классам, динамический алгоритм линеаризует порядок поиска таким образом, чтобы сохранить порядок слева направо, указанный в каждом классе, посетить каждого родителя только один раз и обеспечить монотонность (это означает, что класс может быть подклассом, не затрагивая порядок обхода его родителей). В совокупности эти свойства позволяют проектировать надежные и расширяемые классы с множественным наследованием. Более подробную информацию см. в The Python 2.3 Method Resolution Order.

9.6. Закрытые переменные

В Python не существует «закрытых» переменные экземпляра, доступ к которым был бы возможен только изнутри объекта. Однако существует соглашение, которому следует большая часть кода Python: начинающееся с подчеркивания имя (например, _spam) должно рассматриваться как непубличная часть API (будь то функция, метод или элемент данных). Такое имя следует рассматривать как деталь реализации, которая может измениться без уведомления.

Поскольку существует допустимый вариант использования закрытых членов класса (а именно, чтобы избежать конфликтов имён с именами, определенными подклассами), существует ограниченная поддержка такого механизма, называемого искажением имён. Любой идентификатор вида __spam (как минимум два ведущих подчёркивания и не более одного завершающего) заменяется на уровне исходного текста на _classname__spam, где classname— это имя текущего класса без ведущих подчёркиваний. Это искажение выполняется без учета синтаксической позиции идентификатора, если только он встречается внутри определения класса.

См. также

Спецификации искажения закрытых имён для получения подробной информации и особых случаев.

Изменение имён полезно для того, чтобы позволить подклассам переопределять методы, не нарушая вызовы методов внутри самого класса. Например:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # закрытая копия исходного метода update()

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # новая сигнатура метода update()
        # но __init__() при этом остаётся работоспособным
        for item in zip(keys, values):
            self.items_list.append(item)

Приведённый выше пример будет работать, даже если MappingSubclass введёт идентификатор __update, поскольку он заменяется на _Mapping__update в классе Mapping и на _MappingSubclass__update в классе MappingSubclass соответственно.

Обратите внимание, что правила искажения имён созданы главным образом для предотвращения случайных ошибок. При желании всё ещё можно получить доступ к переменной, которая считается закрытой, или изменить её. Это может быть даже полезно в особых случаях, например, при отладке.

Обратите внимание, что код, переданный в exec() или eval(), не считает имя класса вызывающего кода текущим классом. Это похоже на эффект инструкции global, действие которой также ограничено кодом, скомпилированным вместе. Такое же ограничение применяется к функциям getattr(), setattr() и delattr(), а также при прямом обращении к __dict__.

9.7. Разное и полезное

Иногда бывает полезно иметь тип данных, похожий на «запись» в Pascal или «структуру» в C, объединяющий несколько именованных элементов данных. Идиоматический подход заключается в использовании для этой цели dataclasses:

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    dept: str
    salary: int
>>> john = Employee('Джон', 'компьютерная лаборатория', 1000)
>>> john.dept
'компьютерная лаборатория'
>>> john.salary
1000

Фрагмент кода Python, который ожидает определённый абстрактный тип данных, часто может вместо этого принимать класс, который эмулирует методы этого типа данных. Например, если у вас есть функция, которая форматирует некоторые данные из файлового объекта, вы можете определить класс с методами read() и readline(), которые получают вместо этого данные из строкового буфера и передать его в качестве аргумента.

Объекты метода экземпляра также имеют атрибуты: m.__self__ — это объект экземпляра с методом m(), а :attr: m.__func__ <method.__func__> — это объект функции, соответствующий методу.

9.8. Итераторы

К этому моменту вы, вероятно, заметили, что большинство объектов-контейнеров можно перебрать с помощью инструкции for:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'один':1, 'два':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

Этот стиль доступа ясен, краток и удобен. Использование итераторов пронизывает и унифицирует Python. За кулисами инструкция for вызывает iter() для объекта-контейнера. Функция возвращает объект итератора, который определяет метод __next__(), получающий элементы контейнера по одному за раз. Когда элементов больше нет, __next__() вызывает исключение StopIteration, которое сообщает циклу for завершить выполнение. Вы можете вызвать метод __next__(), используя встроенную функцию next(). Этот пример показывает, как всё это работает:

>>> s = 'абв'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'а'
>>> next(it)
'б'
>>> next(it)
'в'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

Познакомившись с механизмом протокола итерации, легко добавить поведение итератора в свои классы. Определите метод __iter__(), который возвращает объект с методом __next__(). Если класс определяет __next__(), то __iter__() может просто вернуть self:

class Reverse:
    """Итератор для прохода по последовательности в обратном порядке."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('спам')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
м
а
п
с

9.9. Генераторы

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

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('гольф'):
...     print(char)
...
ф
ь
л
о
г

Всё, что можно сделать с помощью генераторов, можно сделать и с помощью итераторов на основе классов, как описано в предыдущем разделе. Что делает генераторы такими компактными, так это то, что методы __iter__() и __next__() создаются автоматически.

Ещё одной ключевой особенностью является то, что локальные переменные и состояние выполнения автоматически сохраняются между вызовами. Это сделало функцию более простой в написании и более понятной, чем подход с использованием переменных экземпляра, таких как self.index и self.data.

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

9.10. Генераторные выражения

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

Примеры:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

Примечания