Классы
Классы позволяют объединять данные и функциональность. Создание нового класса создаёт новый тип объектов, позволяя создавать новые экземпляры этого типа. К каждому экземпляру класса можно прикрепить атрибуты для хранения его состояния. У экземпляров класса также могут быть методы (определённые классом), изменяющие их состояние.
По сравнению с другими языками программирования механизм классов 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
Другой вид обращения к атрибуту экземпляра — это метод. Метод — это функция, которая «принадлежит» объекту. (В Python термин «метод» не ограничивается экземплярами классов: методы могут быть и у объектов других типов. Например, у объектов-списков есть методы append, insert, remove, sort и т. д. Однако далее, если явно не указано иное, мы будем использовать термин «метод» исключительно для методов объектов-экземпляров класса.)
Допустимые имена методов объекта экземпляра зависят от его класса. По определению все атрибуты класса, являющиеся объектами функций, задают соответствующие методы его экземпляров. Поэтому в нашем примере x.f — допустимая ссылка на метод, поскольку MyClass.f является функцией, а x.i — нет, поскольку MyClass.i не является функцией. Однако x.f — это не то же самое, что MyClass.f: первое является объектом метода, а не объектом функции.
9.3.4. Объекты методов
Обычно метод вызывают сразу после получения ссылки на него:
x.f()
В примере с MyClass это вернёт строку 'hello world'. Однако вызывать метод сразу необязательно: x.f — это объект метода, его можно сохранить и вызвать позднее. Например:
xf = x.f
while True:
print(xf())
будет бесконечно выводить hello world.
Что именно происходит при вызове метода? Возможно, вы заметили, что выше x.f() был вызван без аргумента, хотя в определении функции f() был указан аргумент. Куда он делся? Ведь Python должен вызвать исключение, если функцию, требующую аргумент, вызвать без него — даже если этот аргумент на самом деле не используется…
Возможно, вы уже догадались: особенность методов заключается в том, что объект экземпляра передаётся функции в качестве первого аргумента. В нашем примере вызов x.f() в точности эквивалентен MyClass.f(x). В общем случае вызов метода со списком из n аргументов эквивалентен вызову соответствующей функции со списком аргументов, в начало которого перед первым аргументом вставлен объект экземпляра метода.
В целом методы работают следующим образом. При обращении к атрибуту экземпляра, не являющемуся атрибутом данных, выполняется поиск в классе экземпляра. Если имя обозначает допустимый атрибут класса, являющийся объектом функции, ссылки на объект экземпляра и объект функции объединяются в объект метода. При вызове объекта метода со списком аргументов создаётся новый список аргументов, состоящий из объекта экземпляра и исходного списка аргументов, после чего объект функции вызывается с этим новым списком.
9.3.5. Переменные класса и экземпляра
В общем случае переменные экземпляра предназначены для данных, уникальных для каждого экземпляра, а переменные класса — для атрибутов и методов, общих для всех экземпляров класса:
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
Как обсуждалось в разделе Несколько слов об именах и объектах, общие данные могут приводить к неожиданным последствиям при использовании изменяемых объектов, таких как списки и словари. Например, список 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. Чтобы базовые классы не обходились более одного раза, динамический алгоритм линеаризует порядок поиска, сохраняя порядок слева направо, заданный в каждом классе, вызывая каждый родительский класс только один раз и оставаясь монотонным (то есть производный класс можно создать без изменения порядка приоритета его родителей). В совокупности эти свойства позволяют проектировать надёжные и расширяемые классы с множественным наследованием. Подробнее см. https://www.python.org/download/releases/2.3/mro/.
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']
Сноски
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/tutorial/classes.html