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("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

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

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: 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 „hello world“

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

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

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) порядок поиска таким образом, что тот сохраняет указанный слева-направо порядок для каждого класса, который вызывает каждый родительский класс только единожды и является монотонным (значит, класс можно сделать наследником, не взаимодействуя с порядком предшествования его родителей). Обобщённые вместе, эти свойства позволяют разрабатывать надёжные и расширяемые классы, используя множественное наследование. С подробностями можно ознакомиться по этой ссылке: https://www.python.org/download/releases/2.3/mro/.

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

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

Так как есть веский случай для классовых приватных данных (в частности, чтобы избежать коллизии по именам, определенным в подклассах), существует поддержка для таких механизмов, называемая name manging. Любой идентификатор формы __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='')

Такой стиль доступа понятен, лаконичен и удобен. Использованием итераторов (iterators) пропитан язык 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. Выражения-генераторы

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

Примеры:

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