Spec-Zone.ru › Python 3.9

Классы

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

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

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

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

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

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

9.2. Области видимости и пространства имён Python

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

Начнём с некоторых определений.

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

Кстати, я использую слово атрибут для любого имени, следующего за точкой — например, в выражении z.real, real является атрибутом объекта z. Строго говоря, ссылки на имена в модулях являются ссылками на атрибуты: в выражении modname.funcname, modname является объектом модуля, а funcname является атрибутом его. В этом случае существует непосредственное отображение между атрибутами модуля и глобальными именами, определёнными в модуле: они используют одно и то же пространство имён! 1

Атрибуты могут быть только для чтения или изменяемыми. В последнем случае возможно присвоение атрибутам. Атрибуты модулей изменяемы: вы можете записать modname.the_answer = 42. Изменяемые атрибуты также могут быть удалены с помощью оператора del. Например, del modname.the_answer удалит атрибут the_answer из объекта, названного modname.

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

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

Область видимости — это текстовая область программы Python, где пространство имён напрямую доступно. «Прямой доступ» здесь означает, что неквалифицированная ссылка на имя пытается найти имя в пространстве имён.

Хотя области видимости определяются статически, они используются динамически. В любое время во время выполнения существует 3 или 4 вложенные области видимости, пространства имён которых напрямую доступны:

  • внутренняя область видимости, которая проверяется в первую очередь, содержит локальные имена
  • области видимости любых окружающих функций, которые проверяются, начиная с ближайшей внешней области видимости, содержат имена, не являющиеся локальными, но также не глобальными
  • предпоследняя область видимости содержит текущие глобальные имена модуля
  • внешняя область видимости (проверяется в последнюю очередь) — это пространство имён, содержащее встроенные имена

Если имя объявлено глобальным, все ссылки и присвоения напрямую переходят к средней области видимости, содержащей глобальные имена модуля. Для повторной привязки переменных, найденных за пределами внутренней области видимости, можно использовать оператор nonlocal; если переменная не объявлена как nonlocal, эти переменные являются только для чтения (попытка записи в такую переменную просто создаст новую локальную переменную во внутренней области видимости, оставив идентично названную внешнюю переменную неизменной).

Обычно локальная область видимости ссылается на локальные имена текущей (по тексту) функции. За пределами функций локальная область видимости ссылается на то же самое пространство имён, что и глобальная область видимости: пространство имён модуля. Определения классов помещают ещё одно пространство имён в локальную область видимости.

Важно понимать, что области видимости определяются по тексту: глобальная область видимости функции, определённой в модуле, — это пространство имён модуля, независимо от того, откуда и под каким псевдонимом вызывается функция. С другой стороны, фактический поиск имён выполняется динамически, во время выполнения — однако, определение языка эволюционирует к статическому разрешению имён, на этапе «компиляции», поэтому не полагайтесь на динамическое разрешение имён! (Фактически, локальные переменные уже определяются статически.)

Особая особенность Python заключается в том, что — если ни один оператор global или nonlocal не имеет эффекта — присвоения именам всегда выполняются во внутренней области видимости. Присвоения не копируют данные — они просто привязывают имена к объектам. То же самое относится к удалениям: оператор del x удаляет привязку x из пространства имён, к которому обращается локальная область видимости. Фактически, все операции, которые вводят новые имена, используют локальную область видимости: в частности, операторы import и определения функций привязывают имя модуля или функции в локальной области видимости.

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

9.2.1. Пример областей видимости и пространств имён

Вот пример, демонстрирующий, как ссылаться на различные области видимости и пространства имён, и как global и nonlocal влияют на привязку переменных:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

Вывод примера кода:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

Обратите внимание, как локальное присваивание (по умолчанию) не изменило привязку scope_test к spam. Присвоение nonlocal изменило привязку scope_test к spam, а присвоение global изменило привязку на уровне модуля.

Вы также можете видеть, что до присваивания global привязки к spam не существовало.

9.3. Первый взгляд на классы

Классы вводят немного новой синтаксической конструкции, три новых типа объектов и некоторую новую семантику.

9.3.1. Синтаксис определения класса

Простейшая форма определения класса выглядит так:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

Определения классов, как и определения функций (def), должны быть выполнены перед тем, как они окажут какое-либо влияние. (Вы можете поместить определение класса в ветвь оператора if или внутри функции.)

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

При входе в определение класса создается новое пространство имен, используемое в качестве локальной области видимости — таким образом, все присваивания локальным переменным попадают в это новое пространство имен. В частности, определения функций связывают здесь имя новой функции.

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

9.3.2. Объекты класса

Объекты класса поддерживают два вида операций: ссылки на атрибуты и создание экземпляров.

Ссылки на атрибуты используют стандартный синтаксис, используемый для всех ссылок на атрибуты в Python: obj.name. Допустимые имена атрибутов — это все имена, которые были в пространстве имен класса при создании объекта класса. Итак, если определение класса выглядело так:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

то MyClass.i и MyClass.f являются допустимыми ссылками на атрибуты, возвращающими целое число и объект функции соответственно. К атрибутам класса также можно обращаться по присваиванию, поэтому вы можете изменить значение MyClass.i присваиванием. __doc__ также является допустимым атрибутом, возвращающим строку документации, принадлежащую классу: "A simple example class".

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

x = MyClass()

создает новый экземпляр класса и присваивает этот объект локальной переменной x.

Операция создания экземпляра («вызов» объекта класса) создает пустой объект. Многие классы предпочитают создавать объекты с настроенным начальным состоянием. Поэтому класс может определить специальный метод с именем __init__(), например так:

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

Когда класс определяет метод __init__(), создание экземпляра класса автоматически вызывает __init__() для вновь созданного экземпляра класса. Таким образом, в этом примере новый инициализированный экземпляр можно получить так:

x = MyClass()

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

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

9.3.3. Объекты экземпляров

Что мы можем сделать с объектами экземпляров? Единственные операции, понятные объектам экземпляров, — это обращение к атрибутам. Существует два вида допустимых имен атрибутов: данные атрибуты и методы.

данные атрибуты соответствуют «переменным экземпляра» в Smalltalk и «членам данных» в C++. Данные атрибуты не нужно объявлять; как локальные переменные, они появляются при первом присваивании. Например, если x — это экземпляр MyClass созданный выше, следующий фрагмент кода выведет значение 16, не оставляя следа:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

Другой вид обращения к атрибутам экземпляра — это метод. Метод — это функция, которая «принадлежит» объекту. (В Python термин «метод» не уникален для экземпляров класса: и другие типы объектов могут иметь методы. Например, у объектов списков есть методы append, insert, remove, sort и так далее. Однако в последующем обсуждении мы будем использовать термин «метод» исключительно для обозначения методов объектов экземпляров класса, если не указано иное.)

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

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

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

x.f()

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

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

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

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

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

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

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

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

class Dog:

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

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

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

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

class Dog:

    tricks = []             # mistaken use of a class variable

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

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

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

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

class Dog:

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

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

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

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

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

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

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

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

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

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

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

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

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

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

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

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

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

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

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

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

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

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

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

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

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

class DerivedClassName(modname.BaseClassName):

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

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

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

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

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

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

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

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

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

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

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

Динамический порядок необходим, потому что все случаи множественного наследования демонстрируют одну или несколько диамондных отношений (где, по крайней мере, один из родительских классов может быть достигнут через несколько путей от самого нижнего класса). Например, все классы наследуют от object, поэтому любой случай множественного наследования предоставляет более одного пути для достижения object. Чтобы предотвратить доступ к базовым классам более одного раза, динамический алгоритм линеаризует порядок поиска таким образом, чтобы сохранить порядок слева направо, указанный в каждом классе, чтобы каждый родитель вызывался только один раз, и чтобы он был монотонным (что означает, что класс может быть подклассом без влияния на порядок приоритета его родителей). Вместе эти свойства позволяют создавать надёжные и расширяемые классы с множественным наследованием. Для получения более подробной информации см. https://www.python.org/download/releases/2.3/mro/.

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

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

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

Это искажение выполняется без учёта синтаксического расположения идентификатора, до тех пор, пока оно встречается в определении класса.

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

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

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

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

class MappingSubclass(Mapping):

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

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

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

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

То же самое ограничение применяется к getattr(), setattr() и delattr(), а также при прямой ссылке на __dict__.

9.7. Разное

Иногда бывает полезно иметь тип данных, подобный 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
<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–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/tutorial/classes.html

Spec-Zone.ru

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