Spec-Zone.ru › Python 3.14

Классы

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

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

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

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

Если имя объявлено глобальным, все обращения и присваивания выполняются непосредственно в предпоследней области видимости, содержащей глобальные имена модуля. Чтобы повторно связать переменные, найденные вне самой внутренней области видимости, можно использовать инструкцию 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. Разное

Иногда бывает полезен тип данных, похожий на паскалевскую «запись» или структуру 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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/tutorial/classes.html

Spec-Zone.ru

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