Spec-Zone.ru › Python 3.8

Классы

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

По сравнению с другими языками программирования, механизм классов Python добавляет классы с минимальным количеством новой синтаксической конструкции и семантики. Это смесь механизмов классов C++ и Modula-3. Классы Python предоставляют все стандартные функции объектно-ориентированного программирования: механизм наследования классов позволяет использовать несколько базовых классов, производный класс может переопределять любые методы своего базового класса или классов, и метод может вызвать метод базового класса с тем же именем. Объекты могут содержать произвольное количество и типы данных. Как и в случае с модулями, классы обладают динамической природой Python: они создаются во время выполнения и могут быть дополнительно изменены после создания.

В терминологии C++, обычно члены класса (включая данные члены) являются общедоступными (кроме случаев, указанных ниже Приватные переменные), и все функции-члены являются виртуальными. Как и в Modula-3, нет сокращений для ссылки на члены объекта из его методов: функция метода объявляется с явным первым аргументом, представляющим объект, который неявно предоставляется вызовом. Как и в Smalltalk, сами классы являются объектами. Это обеспечивает семантику импорта и переименования. В отличие от C++ и Modula-3, встроенные типы могут использоваться в качестве базовых классов для расширения пользователем. Также, как и в C++, большинство встроенных операторов со специальным синтаксисом (арифметические операторы, индексирование и т. д.) могут быть переопределены для экземпляров класса.

(Ввиду отсутствия общепринятой терминологии для разговора о классах, я буду время от времени использовать термины Smalltalk и C++. Я бы использовал термины Modula-3, поскольку его объектно-ориентированная семантика ближе к семантике Python, чем C++, но я предполагаю, что мало кто из читателей слышал о нём.)

9.1. Несколько слов о именах и объектах

Объекты обладают индивидуальностью, и к одному объекту могут быть привязаны несколько имён (в разных областях видимости). Это известно как алиасинг в других языках. Обычно это не заметно при первом знакомстве с Python, и им можно безопасно пренебречь при работе с неизменяемыми базовыми типами (числа, строки, кортежи). Однако алиасинг оказывает возможное неожиданное влияние на семантику кода Python, включающего изменяемые объекты, такие как списки, словари и большинство других типов. Это обычно используется для пользы программы, так как псевдонимы ведут себя как указатели в некоторых отношениях. Например, передача объекта является дешёвой операцией, поскольку реализация передаёт только указатель; и если функция изменяет переданный в качестве аргумента объект, вызывающий код увидит это изменение — это исключает необходимость в двух различных механизмах передачи аргументов, как в Паскале.

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. Разное

Иногда полезно иметь тип данных, похожий на Pascal «record» или C «struct», объединяющий несколько именованных элементов данных. Определение пустого класса подойдет:

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
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
<iterator object at 0x00A1DB50>
>>> 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__ является атрибутом, но не глобальным именем. Очевидно, использование этого нарушает абстракцию реализации пространства имен и должно быть ограничено такими вещами, как отладчики пост-mortem.

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/tutorial/classes.html

Spec-Zone.ru

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