9. Классы

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

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

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

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

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

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

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

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

namespace (пространство имен) представляет собой отображение имен на объекты. Большинство пространств имен в настоящее время реализованы как словари Python, но в любом случае это обычно не заметно (за исключением производительности), и это может измениться в будущем. Примеры пространств имен: множество встроенных имен (содержащие такие функции, как abs() (docs.python.org/3/library/functions.html#abs) и встроенные имена исключений); глобальные имена в модуле и локальные имена в вызове функции. В определенном смысле набор атрибутов объекта также образует пространство имен. Важная деталь в знаниях о пространствах имен заключается в том, что нет абсолютно никакой связи между именами в разных пространствах; например, в двух различных модулях может быть определена функция maximize, и это не создаст путаницы - пользователи модулей должны использовать префикс в виде имени модуля перед именем функции.

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

Атрибуты могут быть только для чтения или изменяемыми. В последнем случае, возможно присвоение атрибутов. Атрибуты модуля доступны для записи: вы можете написать modname.the_answer = 42. Записываемые атрибуты могут также быть удалены с помощью оператора del (docs.python.org/3/reference/simple_stmts.html#del). Например, del modname.the_answer удалит атрибут the_answer у объекта modname.

Пространства имен создаются в различные моменты времени и имеют разные периоды жизни. Пространство, содержащее встроенные имена, создается, когда запускается интерпретатор Python, и никогда не удаляется. Глобальное пространство имен для модуля создается, когда определение модуля считывается; обычно пространство имен модуля также существует до выхода из интерпретатора. Операторы, выполняющиеся на высшем уровне вызова интерпретатора, либо читающиеся из файла скрипта или интерактивно, рассматриваются как часть модуля под названием __main__ (docs.python.org/3/library/__main__.html#module-__main__), поэтому они имеют свое собственные глобального пространство имен. (Встроенные имен на самом деле находятся в модуле, который называется builtins (docs.python.org/3/library/builtins.html#module-builtins).)

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

scope (область видимости) является буквальной областью Python-программы, где непосредственно доступно пространство имен. "Непосредственно доступно" здесь означает, что "неспецифическая" ссылка на имя пытается найти имя в пространстве имен.

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

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

Если имя объявлено как глобальное, то все ссылки и присвоения переходят непосредственно к средней области видимости, содержащей глобальные имена модуля. Для переназначения переменных, находящихся вне внутренней области видимости, может быть использован оператор nonlocal (docs.python.org/3/reference/simple_stmts.html#nonlocal); если такого объявления нет, эти переменные доступны только для чтения (попытка записи в такую переменную будет просто создавать новую локальную переменную в самой внутренней области видимости, в результате чего внешняя переменная с таким же именем остается без изменений).

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

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

Специальная причуда Python - если нет оператора global, присвоения имен всегда идут в самые внутренние области. Присваивания не копируют данные - они просто связывают имена с объектами. То же самое верно для удалений: оператор del x убирает привязку х из пространства имен, на которые ссылается локальная область видимости. На самом деле все операции, которые вводят новые имена используют локальную область: в частности, оператор import (docs.python.org/3/reference/simple_stmts.html#import) и определения функций связывают модуль или имя функции в локальной области видимости.

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

9.2.1. Пример областей видимости (scopes) и пространств имен (namespaces)

Это пример демонстрирует, как ссылаться на различные области и пространства имен, и как 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

Заметьте, что присвоение local (которое происходит по умолчанию) не изменило связывания spam в scope_test. Присвоение nonlocal изменило связывание spam в scope_test, и присвоение global изменило связывание до уровня модуля.

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

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

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

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

Простейшая форма определения класса выглядит следующим образом:

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

Определения классов, как и определения функций (операторы def (docs.python.org/3/reference/compound_stmts.html#def)), должны быть выполнены до их вызова. (Предположительно вы может поместить определение класса в ветке оператора if (docs.python.org/3/reference/compound_stmts.html#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()

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

Операция создания экземпляра (вызов объекта класса) создает пустой объект. Многие классы при создании экземпляров настроены на определенное исходное состояние. Поэтому в классе может быть определен специальный метод с именем __init__() (docs.python.org/3/reference/datamodel.html#object.__init__), например:

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

Если класс определяет метод __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++. Нет необходимости объявлять атрибуты данных; как локальные переменные, они начинают существовать, когда им впервые происходит присваивание. Например, если х является экземпляром MyClass, созданного выше, следующий фрагмент кода выведет значение 16:

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

Другой вид ссылки на атрибут экземпляра - это method. Метод является функцией, которая "принадлежит" объекту. (В 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(х). В общем, вызов метода со списком 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

Как уже говорилось в разделе 9.1., разделяемые данные могут иметь возможно неожиданные эффекты в случае привлечения изменяемых (docs.python.org/3/glossary.html#term-mutable) объектов, таких как списки и словари. Например, список 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. Некоторые замечания

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

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

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

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

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

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

# Функция определяется вне класса
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, который ссылается на функции-объекты, и, следовательно, все они являются методами экземпляров класса C - h есть точный эквивалент 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 фактически являются 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) ложно, так как float не является подклассом int.

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

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

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

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

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

Динамическое следование необходимо потому, что все случаи множественного наследования обладают одним или несколькими взаимосвязанными отношениями (где по крайней мере один из родительских классов может быть доступен через множество путей от самого нижнего класса). Например, все классы унаследованы от object, так что любой случай множественного наследования обеспечивает более одного пути, чтобы добраться до object. Чтобы сохранить базовые классы от доступа более одного раза, динамический алгоритм делает линейным порядок поиска таким образом, чтобы сохранить порядок слева-направо, указанный в каждом классе, который вызывает каждого родителя только один раз (это означает, что от класса можно создать подкласс не затрагивая порядок приоритетов его родителей). Взятые вместе эти свойства делают возможным создание надежных и расширяемых классов с множественным наследованием. Более подробно см. python.org/download/releases/2.3/mro/.

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

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

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

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

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

9.7. Различные заметки

Иногда бывает полезно иметь тип данных похожий на запись в Паскале или структуру в C, связав вместе несколько именованных элементов данных. Определение пустого класса превосходно представит такую возможность:

class Employee:
    pass
 
john = Employee() # Создает пустую запись employee
 
# Заполняются поля записи
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

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

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

9.8. Итераторы

К этому моменту вы возможно заметили, что большинство контейнерных объектов позволяют совершить по ним циклический проход с помощью оператора for (docs.python.org/3/reference/compound_stmts.html#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 (docs.python.org/3/library/exceptions.html#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 ?
    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. Генераторы

Генераторы (docs.python.org/3/glossary.html#term-generator) представляют собой простой и мощный инструмент для создания итераторов. Они написаны как обычные функции, но используют оператор yield (docs.python.org/3/reference/simple_stmts.html#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__(), которые создаются автоматически.

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

В дополнение к автоматическому созданию метода и сохранению состояния программы, когда генераторы прекращают работу, они автоматически возбуждают StopIteration (docs.python.org/3/library/exceptions.html#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))         # dot product
260
 
>>> from math import pi, sin
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
 
>>> 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__ является атрибутом, но не глобальным именем. Очевидно, что его использование нарушает абстракцию реализации пространства имен, и должно быть ограничена к вещам, как завершающие действия отладчиков.

Создано