9. Занятия

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

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

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

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

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

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

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 изменило привязку на уровне модуля.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Допустимые имена методов экземпляра объекта зависят от его класса. По определению, все атрибуты класса, которые являются функциональными объектами, определяют соответствующие методы его экземпляров. Итак, в нашем примере 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, и также возможно, что может быть написана программа class browser, которая опирается на такое соглашение.

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

# 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. Всякие мелочи

Иногда бывает полезно использовать тип данных, подобный «записи» в Pascal или «структуре» в C, объединяющий несколько именованных элементов данных. Идиоматический подход заключается в использовании 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

Ознакомившись с механикой, лежащей в основе протокола iterator, легко добавить поведение итератора в свои классы. Определите метод __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']

Сноски

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