Классы
Классы предоставляют способ объединения данных и функциональности. Создание нового класса создаёт новый тип объекта, позволяя создавать новые экземпляры этого типа. Каждый экземпляр класса может иметь присоединённые к нему атрибуты для поддержания его состояния. Экземпляры классов также могут иметь методы (определённые классом) для изменения его состояния.
По сравнению с другими языками программирования, механизм классов 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 изменило привязку на уровне модуля.
Также можно заметить, что до присваивания с помощью 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.11/tutorial/classes.html