Классы
Классы предоставляют способ объединения данных и функциональности. Создание нового класса создает новый тип объекта, позволяя создавать новые экземпляры этого типа. Каждый экземпляр класса может иметь присоединенные к нему атрибуты для поддержания его состояния. Экземпляры класса также могут иметь методы (определенные его классом) для изменения его состояния.
По сравнению с другими языками программирования, механизм классов 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; если не объявлено 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 изменило привязку на уровне модуля.
Вы также можете увидеть, что до присваивания с 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__update в классе Mapping и _MappingSubclass__update в классе MappingSubclass соответственно.
Обратите внимание, что правила искажения имён в основном предназначены для предотвращения случайных ошибок; все ещё возможно получить доступ к или изменить переменную, которая считается приватной. Это может быть полезно в особых случаях, таких как в отладчике.
Обратите внимание, что код, переданный exec() или eval() не учитывает имя класса вызывающего класса как текущий класс; это подобно эффекту оператора global, действие которого ограничено кодом, который был скомпилирован в байт-код вместе.
То же самое ограничение применяется к getattr(), setattr() и delattr(), а также при прямой ссылке на __dict__.
9.7. Разное
Иногда бывает полезно иметь тип данных, подобный паскалевскому «записи» или C-структуре, объединяющий несколько именованных элементов данных. Идиоматический подход заключается в использовании dataclasses для этой цели:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000
Часть кода Python, ожидающая определённый абстрактный тип данных, часто может получить класс, эмулирующий методы этого типа данных вместо него. Например, если у вас есть функция, которая форматирует данные из объекта файла, вы можете определить класс с методами read() и readline(), которые получают данные из буфера строк вместо этого и передать его как аргумент.
Объекты методов экземпляра также имеют атрибуты: m.__self__ — это объект экземпляра с методом m(), а m.__func__ — это объект функции, соответствующий методу.
9.8. Итераторы
К этому моменту вы, вероятно, заметили, что большинство контейнерных объектов можно перебирать с помощью оператора for:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
Этот стиль доступа ясен, лаконичен и удобен. Использование итераторов пронизывает и объединяет Python. Под капотом оператор for вызывает iter() для контейнерного объекта. Функция возвращает объект-итератор, который определяет метод __next__(), который получает доступ к элементам контейнера по одному за раз. Когда больше нет элементов, __next__() поднимает исключение StopIteration, которое сообщает циклу for о завершении. Вы можете вызвать метод __next__() с помощью встроенной функции next(); этот пример показывает, как все это работает:
>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
Увидев механизм протокола итератора, легко добавить поведение итератора в ваши классы. Определите метод __iter__(), который возвращает объект с методом __next__(). Если класс определяет __next__(), то __iter__() может просто вернуть self:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print(char)
...
m
a
p
s
9.9. Генераторы
Генераторы — простой и мощный инструмент для создания итераторов. Они пишутся как обычные функции, но используют оператор yield всякий раз, когда они хотят вернуть данные. Каждый раз, когда на них вызывается next(), генератор возобновляется с того места, где он остановился (он запоминает все значения данных и какую инструкцию выполнил в последний раз). Пример показывает, что генераторы могут быть тривиально простыми в создании:
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
>>> for char in reverse('golf'):
... print(char)
...
f
l
o
g
Всё, что можно сделать с генераторами, также можно сделать с итераторами на основе классов, как описано в предыдущем разделе. То, что делает генераторы такими компактными, заключается в том, что методы __iter__() и __next__() создаются автоматически.
Ещё одна ключевая особенность заключается в том, что локальные переменные и состояние выполнения автоматически сохраняются между вызовами. Это упростило написание функции и сделало её более понятной по сравнению с подходом, использующим переменные экземпляров, такие как self.index и self.data.
В дополнение к автоматическому созданию методов и сохранению состояния программы, при завершении генераторов они автоматически поднимают StopIteration. В сочетании эти особенности упрощают создание итераторов без дополнительных усилий, помимо написания обычной функции.
9.10. Выражения-генераторы
Некоторые простые генераторы могут быть записаны лаконично в виде выражений, используя синтаксис, аналогичный списковым включениям, но с круглыми скобками вместо квадратных. Эти выражения предназначены для ситуаций, когда генератор используется непосредственно вложенной функцией. Выражения-генераторы более компактны, но менее универсальны, чем полные определения генераторов, и, как правило, более дружественны к памяти, чем эквивалентные списковые включения.
Примеры:
>>> sum(i*i for i in range(10)) # sum of squares 285 >>> xvec = [10, 20, 30] >>> yvec = [7, 5, 3] >>> sum(x*y for x,y in zip(xvec, yvec)) # dot product 260 >>> unique_words = set(word for line in page for word in line.split()) >>> valedictorian = max((student.gpa, student.name) for student in graduates) >>> data = 'golf' >>> list(data[i] for i in range(len(data)-1, -1, -1)) ['f', 'l', 'o', 'g']
Примечания
-
1 -
За исключением одного. Объекты модулей имеют скрытый только для чтения атрибут, называемый
__dict__, который возвращает словарь, используемый для реализации пространства имён модуля; имя__dict__является атрибутом, но не глобальным именем. Очевидно, использование этого нарушает абстракцию реализации пространства имён и должно быть ограничено такими вещами, как отладчики после завершения работы.
© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.10/tutorial/classes.html