Spec-Zone.ru › Python 3.13

Классы

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

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

В терминологии C++ обычно члены класса (включая данные) являются открытыми (см. исключение ниже в разделе Закрытые переменные), а все функции-члены — виртуальными. Как и в Modula-3, нет сокращенной записи для обращения из методов к членам объекта: функция-метод объявляется с явным первым аргументом, представляющим объект, который при вызове передается неявно. Как и в 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.)

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

Область видимости — это текстовая область программы 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

Обратите внимание, что локальное присваивание (используемое по умолчанию) не изменило привязку spam в scope_test. Присваивание с помощью nonlocal изменило привязку spam в scope_test, а присваивание с помощью 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()

Если x = 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'

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

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

9.6. Переменные экземпляра с ограниченным доступом

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

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

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

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

У объектов методов экземпляра тоже есть атрибуты: 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. Генераторы

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

© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/tutorial/classes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API