Spec-Zone.ru › Python 3.12

Классы

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

По сравнению с другими языками программирования механизм классов 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()

В примере с 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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/tutorial/classes.html

Spec-Zone.ru

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