Spec-Zone.ru › Python 3.12

Классы

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

По сравнению с другими языками программирования, механизм классов 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

Другой тип обращений к атрибутам экземпляра — это метод. Метод — это функция, которая «принадлежит» объекту.

Допустимые имена методов объекта экземпляра зависят от его класса. По определению, все атрибуты класса, являющиеся объектами функций, определяют соответствующие методы его экземпляров. Так в нашем примере x.f является допустимым обращением к методу, поскольку MyClass.f является функцией, но x.i нет, поскольку MyClass.i не является. Но x.f не то же самое, что и MyClass.f — это объект метода, а не объект функции.

9.3.4. Объекты методов

Обычно метод вызывается сразу после его связывания:

x.f()

В примере MyClass это вернёт строку 'hello world'. Однако нет необходимости вызывать метод сразу: x.f — это объект метода, который можно сохранить и вызвать позже. Например:

xf = x.f
while True:
    print(xf())

будет продолжать выводить hello world до бесконечности.

Что происходит, когда вызывается метод? Вы могли заметить, что x.f() был вызван без аргумента выше, хотя определение функции для f() указывало на аргумент. Что случилось с аргументом? Разве Python не выдаст исключение, когда функция, требующая аргумента, вызывается без него — даже если аргумент фактически не используется…

На самом деле, вы могли догадаться о ответе: особенность методов заключается в том, что объект экземпляра передаётся как первый аргумент функции. В нашем примере вызов x.f() точно эквивалентен MyClass.f(x). В общем случае, вызов метода с списком из n аргументов эквивалентен вызову соответствующей функции со списком аргументов, в котором перед первым аргументом вставлен объект экземпляра метода.

В общем случае методы работают следующим образом. Когда ссылаются на не-данные атрибуты экземпляра, ищется класс экземпляра. Если имя соответствует допустимому атрибуту класса, являющемуся объектом функции, ссылки на объект экземпляра и объект функции упаковываются в объект метода. При вызове объекта метода со списком аргументов создаётся новый список аргументов из объекта экземпляра и списка аргументов, и вызывается объект функции с этим новым списком аргументов.

9.3.5. Переменные класса и экземпляра

Как правило, переменные экземпляра предназначены для данных, уникальных для каждого экземпляра, а переменные класса — для атрибутов и методов, общих для всех экземпляров класса:

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

Как обсуждалось в Заметка о именах и объектах, общие данные могут иметь неожиданные последствия, когда используются изменяемые объекты, такие как списки и словари. Например, список tricks в следующем коде не следует использовать в качестве переменной класса, так как только один список будет использоваться всеми экземплярами Dog:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

Правильное проектирование класса должно использовать переменную экземпляра:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4. Случайные замечания

Если одно и то же имя атрибута встречается как в экземпляре, так и в классе, то при поиске атрибута приоритет отдаётся экземпляру:

>>> class Warehouse:
...    purpose = 'storage'
...    region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

Данные атрибуты могут быть обработаны методами, а также обычными пользователями («клиентами») объекта. Другими словами, классы не подходят для реализации чистых абстрактных типов данных. Фактически, ничего в Python не позволяет принудительно скрывать данные — всё основано на соглашениях. (С другой стороны, реализация Python, написанная на C, может полностью скрыть детали реализации и контролировать доступ к объекту при необходимости; это может использоваться расширениями Python, написанными на C.)

Клиенты должны с осторожностью использовать данные атрибуты — клиенты могут нарушить инварианты, поддерживаемые методами, изменяя данные атрибуты. Обратите внимание, что клиенты могут добавлять свои собственные атрибуты данных к экземпляру объекта, не затрагивая при этом корректность методов, при условии, что конфликтов имён не возникает — опять же, соглашение об именах может сэкономить много головной боли.

Нет сокращений для ссылки на атрибуты данных (или другие методы!) внутри методов. Я считаю, что это на самом деле повышает читаемость методов: нет возможности спутать локальные переменные и переменные экземпляра при беглом просмотре метода.

Часто первый аргумент метода называется self. Это всего лишь соглашение: имя self не имеет абсолютно никакого специального значения для Python. Однако следует отметить, что несоблюдение соглашения может сделать ваш код менее читаемым для других программистов Python, а также возможно, что программа просмотра классов может полагаться на такое соглашение.

Любой объект функции, являющийся атрибутом класса, определяет метод для экземпляров этого класса. Не обязательно, чтобы определение функции было текстово заключено в определении класса: присвоение объекта функции локальной переменной в классе также допустимо. Например:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

Теперь f, g и h — все атрибуты класса C, которые ссылаются на объекты функций, и, следовательно, все они являются методами экземпляров C — h точно эквивалентно g. Обратите внимание, что эта практика обычно только запутывает читателя программы.

Методы могут вызывать другие методы, используя атрибуты методов аргумента self:

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

Методы могут ссылаться на глобальные имена так же, как и обычные функции. Глобальная область, связанная с методом, — это модуль, содержащий его определение. (Класс никогда не используется как глобальная область.) Хотя редко встречаются веские причины для использования глобальных данных в методе, существует множество законных применений глобальной области: например, функции и модули, импортированные в глобальную область, могут использоваться методами, а также функции и классы, определенные в ней. Обычно класс, содержащий метод, сам определен в этой глобальной области, и в следующем разделе мы найдем некоторые веские причины, по которым метод мог бы захотеть обратиться к собственному классу.

Каждое значение является объектом и, следовательно, имеет класс (также называемый его типом). Он хранится как object.__class__.

9.5. Наследование

Конечно, функция языка не заслуживала бы названия «класс», не поддерживая наследования. Синтаксис определения производного класса выглядит следующим образом:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

Имя BaseClassName должно быть определено в пространстве имен, доступном из области, содержащей определение производного класса. Вместо имени базового класса также допускаются другие произвольные выражения. Это может быть полезно, например, когда базовый класс определён в другом модуле:

class DerivedClassName(modname.BaseClassName):

Выполнение определения производного класса происходит так же, как и для базового класса. При создании объекта класса базовый класс запоминается. Это используется для разрешения ссылок на атрибуты: если запрашиваемый атрибут не найден в классе, поиск продолжается в базовом классе. Это правило применяется рекурсивно, если базовый класс сам является производным от другого класса.

В создании экземпляров производных классов нет ничего особенного: DerivedClassName() создаёт новый экземпляр класса. Ссылки на методы разрешаются следующим образом: соответствующий атрибут класса ищется, спускается вниз по цепочке базовых классов при необходимости, и ссылка на метод допустима, если это даёт объект функции.

Производные классы могут переопределять методы своих базовых классов. Поскольку методы не имеют специальных привилегий при вызове других методов того же объекта, метод базового класса, вызывающий другой метод, определённый в том же базовом классе, может в конечном итоге вызвать метод производного класса, который его переопределяет. (Для программистов C++: все методы в Python фактически virtual.)

Метод переопределения в производном классе, возможно, хочет расширить, а не просто заменить метод базового класса с тем же именем. Существует простой способ прямого вызова метода базового класса: просто вызовите BaseClassName.methodname(self, arguments). Это иногда бывает полезно и для клиентов. (Обратите внимание, что это работает только в том случае, если базовый класс доступен как BaseClassName в глобальной области.)

В Python есть две встроенные функции, работающие с наследованием:

  • Используйте isinstance() для проверки типа экземпляра: isinstance(obj, int) будет True только если obj.__class__ является int или каким-либо классом, производным от int.
  • Используйте issubclass() для проверки наследования классов: issubclass(bool, int) является True, так как bool является подклассом int. Однако, issubclass(float, int) является False , так как float не является подклассом int.

9.5.1. Множественное наследование

Python также поддерживает форму множественного наследования. Определение класса с несколькими базовыми классами выглядит следующим образом:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

Для большинства целей в простейших случаях можно считать, что поиск атрибутов, унаследованных от родительского класса, осуществляется по принципу глубины, слева направо, не производя повторного поиска в том же классе, где существует перекрытие в иерархии. Таким образом, если атрибут не найден в DerivedClassName, он ищется в Base1, а затем (рекурсивно) в базовых классах Base1, и если он не был найден там, он ищется в Base2, и так далее.

На самом деле, это немного сложнее; порядок разрешения метода динамически изменяется для поддержки кооперативных вызовов super(). Этот подход известен в некоторых других языках множественного наследования как call-next-method и более мощнее, чем вызов super, встречающийся в языках с одинарным наследованием.

Динамический порядок необходим, потому что все случаи множественного наследования демонстрируют одну или несколько диамантных взаимосвязей (где по крайней мере один из родительских классов может быть достигнут через несколько путей от наименее производного класса). Например, все классы наследуются от object, поэтому любой случай множественного наследования предоставляет более одного пути для достижения object. Чтобы не допустить доступа к базовым классам более одного раза, динамический алгоритм линеаризует порядок поиска таким образом, чтобы сохранить порядок слева направо, указанный в каждом классе, который вызывает каждый родительский класс только один раз и который является монотонным (что означает, что класс может быть подклассом, не влияя на порядок следования его родителей). В совокупности эти свойства позволяют проектировать надёжные и расширяемые классы с множественным наследованием. Более подробную информацию см. в Порядок разрешения метода Python 2.3.

9.6. Приватные переменные

«Приватные» переменные экземпляра, доступные только изнутри объекта, в Python не существуют. Однако существует соглашение, которому следует большинство кодов Python: имя, начинающееся с подчеркивания (например, _spam), должно рассматриваться как непубличная часть API (будь то функция, метод или член данных). Оно должно рассматриваться как деталь реализации и может быть изменено без предварительного уведомления.

Поскольку для членов класса-приватных существует обоснованный случай использования (а именно, чтобы избежать конфликтов имен с именами, определёнными подклассами), существует ограниченная поддержка такого механизма, называемого изменением имени. Любой идентификатор вида __spam (как минимум два ведущих подчеркивания, как максимум одно заключительное подчеркивание) текстово заменяется на _classname__spam, где classname — текущее имя класса с удалёнными ведущими подчеркиваниями. Это изменение имени выполняется без учёта синтаксической позиции идентификатора, при условии, что оно встречается внутри определения класса.

См. также

См. спецификации изменения имени приватных членов для деталей и специальных случаев.

Изменение имени полезно для того, чтобы подклассы могли переопределять методы, не нарушая вызовы методов внутри класса. Например:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Приведённый выше пример будет работать даже если MappingSubclass введёт идентификатор __update, так как он заменяется на _Mapping__update в классе Mapping и _MappingSubclass__update в классе MappingSubclass соответственно.

Обратите внимание, что правила изменения имени в основном предназначены для предотвращения случайных ошибок; всё ещё возможно получить доступ к или изменить переменную, которая считается приватной. Это может быть даже полезно в особых случаях, например, в отладчике.

Обратите внимание, что код, переданный в exec() или eval(), не учитывает имя класса вызывающего класса как текущего класса; это аналогично эффекту оператора global, эффект которого также ограничен кодом, который скомпилирован в байт-код вместе. То же ограничение применяется к getattr(), setattr() и delattr(), а также при прямой ссылке на __dict__.

9.7. Разное

Иногда полезно иметь тип данных, подобный Pascal «record» или C «struct», объединяющий несколько именованных элементов данных. Приемлемый подход — использовать 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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/tutorial/classes.html

Spec-Zone.ru

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