9. Классы

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

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

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

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

9.1. Пара слов о терминологии

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

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

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

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

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

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

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

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

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

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

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

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

Если имя объявлено как global, то все ссылки и присвоения приходят прямо к среднему уровню, содержащему глобальные имена модуля. Чтобы вернуть привязку для переменных, найденных вне внутреннего уровня видимости, может использоваться оператор nonlocal; без объявления nonlocal такие переменные — только для чтения (попытка записать в такую переменную просто создаст новую локальную переменную в самой внутренней области, оставляя идентично названную внешнюю переменную неизменной).

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

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

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

Оператор global можно использовать для того, чтобы объявить определённые переменные как привязанные к глобальной области видимости и указывает, что их переназначения должны происходить в ней; оператор nonlocal помечает переменные как привязанные к окружающей их области видимости и указывает, что их переназначения должны происходить в ней.

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

Приведём пример, показывающий, каким образом можно ссылаться на разные области видимости и пространства имён, и как global и nonlocal влияют на привязку переменной:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("После локального присвоения:", spam)
    do_nonlocal()
    print("После nonlocal присвоения:", spam)
    do_global()
    print("После global присвоения:", spam)

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

Выходные данные кода примера:

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

Обратите внимание, что локальное присваивание (работающее по умолчанию) не изменило scope_test связывание spam. Присвоение nonlocal изменило scope_test связывание spam, и 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:
    """Простой пример класса"""
    i = 12345

    def f(self):
        return 'Привет мир'

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

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

x = MyClass()

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

Операция создания экземпляра (instantiation) создаёт объект данного класса. Большая часть классов предпочитает создавать экземпляры, имеющие определённое начальное состояние. Поэтому класс может определить специальный метод с именем __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. Объекты экземпляра

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

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

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

Другим видом ссылки на атрибут экземпляра является метод. Метод является функцией, которая «принадлежит» объекту. (В Python, термин метод не является уникальным для экземпляра класса: у других типов объектов также могут быть свои методы. Например, объекты списка содержат методы, называемые append, insert, remove, sort и т. д. Тем не менее, ниже под термином «метод» мы будем понимать только методы объектов-экземпляров классов, пока отдельно не будет указано иное.)

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

9.3.4. Объекты-методы

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

x.f()

В 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 = 'canine'         # переменная класса, общая для всех экземпляров

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

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # разделяются всеми собаками
'canine'
>>> e.kind                  # разделяются всеми собаками
'canine'
>>> d.name                  # уникальный для d
'Fido'
>>> e.name                  # уникальный для e
'Buddy'

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

class Dog:

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

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

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

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # неожиданно для всех собак
['roll over', 'play dead']

Для правильной конструкции класса следует использовать переменную экземпляра:

class Dog:

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

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

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

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

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

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

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

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

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

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

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

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

class C:
    f = f1

    def g(self):
        return 'hello world'

    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)

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

Каждое значение является объектом, и поэтому имеет свой класс (также называемый его типом). Хранится как object.__class__.

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

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

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

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

class DerivedClassName(modname.BaseClassName):

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

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

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

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

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(). Это подход известен в некоторых других языках множественного наследования как «вызов-следующего-метода» («Call-next-method») и является более мощным, чем супер-вызов, найденный в языках с единственным наследованием.

Динамическое упорядочивание (dynamic ordering) является важным, поскольку все вариации множественного наследования проявляют в себе эффект ромбовых отношений (когда как минимум один родительский класс может быть доступен различными путями из низшего в иерархии класса). Например, все классы наследуются от object, так что множественное наследование в любом виде предоставляет более одного пути для того, чтобы достичь object. Чтобы защитить базовые классы от двойных и более запросов, динамический алгоритм «выпрямляет» (linearizes) порядок поиска таким образом, что тот сохраняет указанный слева-направо порядок для каждого класса, который вызывает каждый родительский класс только единожды и является монотонным (значит, класс можно сделать наследником, не взаимодействуя с порядком предшествования его родителей). Обобщённые вместе, эти свойства позволяют разрабатывать надёжные и расширяемые классы, используя множественное наследование. С подробностями можно ознакомиться по этой ссылке.

9.6. Приватные переменные

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

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

Искажение имени полезно для разрешения субклассам переопределения методов без поломки обращения к методам между классами. Например:

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. Всякая всячина

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

class Employee:
    pass

john = Employee()  # Создать пустую запись о сотруднике

# Заполнить поля записи
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

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

У объектов-методов экземпляров также есть свои атрибуты: m.__self__ — это экземпляр объекта с методом m(), а m.__func__ — объект-функция, соответствующая методу.

9.8. Итераторы

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

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':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 = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> 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('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

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

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

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

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

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

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

9.10. Выражения-генераторы

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

Примеры:

>>> sum(i*i for i in range(10))                 # сумма квадратов
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # скалярное произведение
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']

Сноски

[1]За одним исключением. У объектов модулей есть секретный атрибут только для чтения, называемый __dict__, который возвращает словарь, используемый для реализации пространства имён модуля; имя __dict__ является атрибутом, но не глобальным именем. Очевидно, что использование этого нарушает абстракцию реализации пространства имён, и потому следует ограничить такое использование для вещей типа пост-мортем отладчики.