Классы
Классы предоставляют способ объединения данных и функциональности вместе. Создание нового класса создает новый тип объекта, позволяющий создавать новые экземпляры этого типа. Каждый экземпляр класса может иметь атрибуты, прикрепленные к нему для поддержания его состояния. Экземпляры классов также могут иметь методы (определяемые классом) для изменения его состояния.
По сравнению с другими языками программирования, механизм классов Python добавляет классы с минимальным количеством новых синтаксиса и семантики. Это смесь механизмов классов, найденных в C++ и Modula-3. Классы Python предоставляют все стандартные функции объектно-ориентированного программирования: механизм наследования классов позволяет использовать несколько базовых классов, производный класс может переопределять любые методы своего базового класса или классов, и метод может вызвать метод базового класса с тем же именем. Объекты могут содержать произвольное количество и виды данных. Как и в случае с модулями, классы обладают динамической природой Python: они создаются во время выполнения и могут быть далее изменены после создания.
В терминологии C++, обычно члены класса (включая члены данных) являются public (за исключением см. ниже Приватные переменные), и все функции членов являются virtual. Как и в 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; если переменная не объявлена как 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
Обратите внимание, как локальное присваивание (которое является по умолчанию) не изменило привязку scope_test к spam. Присваивание с nonlocal изменило привязку scope_test к spam, а присваивание с global изменило привязку на уровне модуля.
Также можно заметить, что ранее привязки к spam не было до присваивания с global.
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__update в классе Mapping и на _MappingSubclass__update в классе MappingSubclass соответственно.
Обратите внимание, что правила изменения имени в основном предназначены для предотвращения случайных ошибок; всё же можно получить доступ к или изменить переменную, считающуюся приватной. Это может быть полезно в особых случаях, таких как отладка.
Обратите внимание, что код, переданный exec() или eval(), не учитывает имя класса вызывающего класса в качестве текущего класса; это аналогично эффекту инструкции global, эффект которого аналогичным образом ограничен кодом, который скомпилирован в байткоды вместе.
То же самое ограничение применяется к getattr(), setattr() и delattr(), а также при прямой ссылке на __dict__.
9.7. Разное
Иногда бывает полезно иметь тип данных, похожий на «запись» в Pascal или «структуру» в C, объединяющий несколько именованных элементов данных. Принятый способ — использовать dataclasses для этой цели:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000
Часть кода Python, ожидающая определённый абстрактный тип данных, часто может принять класс, эмулирующий методы этого типа данных вместо него. Например, если у вас есть функция, которая форматирует данные из объекта файла, вы можете определить класс с методами read() и readline(), которые получают данные из буфера строки вместо него, и передать его как аргумент.
Объекты методов экземпляра также имеют атрибуты: 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.13/tutorial/classes.html