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__ также является допустимым атрибутом и возвращает строку docstring, принадлежащую классу: "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

Другой вид ссылки на атрибут экземпляра - это метод. Метод - это функция, которая «принадлежит» объекту. (В 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'         # 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, которые ссылаются на объекты функций, и, следовательно, все они являются методами экземпляров 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 фактически являются 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. Чтобы к базовым классам не обращались более одного раза, динамический алгоритм линеаризует порядок поиска таким образом, чтобы сохранялся порядок слева направо, указанный в каждом классе, чтобы каждый родитель вызывался только один раз, и чтобы он был монотонным (это означает, что класс может быть подклассом без изменения порядка старшинства его родителей). В совокупности эти свойства позволяют разрабатывать надежные и расширяемые классы с множественным наследованием. Более подробно см. https://www.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   # 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» в Си, объединяющий несколько именованных элементов данных. Для этого подойдет пустое определение класса:

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

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

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

9.8. Итераторы

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

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

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

>>> s = 'abc'
>>> it = iter(s)
>>> it
<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']

Сноски

1

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

Вернуться на верх