9. Занятия

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Самая внешняя область (поиск выполняется в последнюю очередь) - это пространство имен, содержащее встроенные имена

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

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

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

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

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

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

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

def scope_test():
    def do_local():
        spam = "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 (которое используется по умолчанию) не изменило привязку scope_testк spam. Назначение nonlocal изменило привязку scope_testк spam, а назначение global изменило привязку на уровне модуля.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def f(self):
        return 'hello world'

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

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

x = MyClass()

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

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

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

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

x = MyClass()

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

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

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

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

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

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

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

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

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

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

x.f()

В примере с 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'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

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

class Dog:

    tricks = []             # mistaken use of a class variable

    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                # unexpectedly shared by all dogs
['roll over', 'play dead']

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

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

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

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

# Function defined outside the class
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) является False, поскольку float не является подклассом int.

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

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

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

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

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

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

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   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __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» в Паскале или «struct» в Си, объединяющий несколько именованных элементов данных. Идиоматический подход заключается в использовании dataclasses для этой цели:

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    dept: str
    salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000

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

Instance method objects тоже имеют атрибуты: m.__self__ - это объект экземпляра с методом m(), а m.__func__ - это function object, соответствующий методу.

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
<str_iterator object at 0x10c90e650>
>>> 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:
    """Iterator for looping over a sequence backwards."""
    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. Генераторы

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

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

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

Примеры:

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

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

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

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

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

Сноски