В этой главе мы ввели составные типы данных, а также механизмы абстракции данных, реализованные конструкторами и селекторами. Используя передачу сообщений, мы можем напрямую приписать поведение абстрактным типам данных. Используя метафору объектов, мы можем связать представление данных с методами, которые работают с этими данными, что позволяет нам создавать модульные программы, управляемые данными, с локальным состоянием.
Однако мы всё ещё должны показать, что наша объектная система позволяет нам гибко комбинировать различные типы объектов в больших программах. Передача точечных операторов — это всего лишь один из способов использования нескольких объектов для построения комбинированных выражений. В этом разделе мы рассмотрим некоторые методы комбинирования и работы с объектами разных типов.
Мы говорили в начале этой главы, что поведение объектов должно быть похоже на данные, которые они представляют, включая создание собственного строкового представления. Строковые представления данных особенно важны в интерактивных языках программирования, таких как Python, где цикл «чтение-оценка-печать» требует, чтобы каждое значение имело некоторую форму строкового представления.
Строки обеспечивают базовую среду для обмена информацией между людьми. Последовательности символов могут отображаться на экране, печататься на бумаге, читаться вслух, преобразовываться в шрифт Брайля или передаваться по радио с использованием кода Морзе. Строки также являются фундаментальными для программирования, поскольку они могут представлять выражения Python. Для объекта мы можем захотеть создать строку, которая, будучи интерпретирована как выражение Python, приведёт к объекту, эквивалентному исходному.
Python требует, чтобы все объекты могли генерировать два различных строковых представления: одно — понятное человеку текстовое представление, а другое — представление, которое может быть интерпретировано Python как выражение. Функция str
возвращает удобочитаемую строку. Когда это возможно, функция repr
возвращает выражение Python, которое можно оценить как эквивалентный объект. Документация функции repr
объясняет эту функцию:
repr(object) -> string
Возвращает каноническое строковое представление объекта.
Для большинства типов объектов eval(repr(object)) == object.
Результатом вызова функции repr
для значения выражения является то, что Python печатает в интерактивной сессии.
>>> 12e12
12000000000000.0
>>> print(repr(12e12))
12000000000000.0
Когда нет выражения, которое могло бы быть оценено как исходное значение, Python создаёт прокси:
>>> repr(min)
'<built-in function min>'
Функция str
, как правило, совпадает с функцией repr
, но иногда она предоставляет более понятное текстовое представление. Например, мы видим разницу между str
и repr
в случае дат:
>>> from datetime import date
>>> today = date(2011, 9, 12)
>>> repr(today)
'datetime.date(2011, 9, 12)'
>>> str(today)
'2011-09-12'
Определение функции repr
сталкивается с новой проблемой: мы хотим, чтобы оно правильно применялось ко всем типам данных, даже к тем, которые не существовали во время реализации repr
. Мы хотим, чтобы она была похожа на полиморфную функцию, способную работать с множеством различных форм данных.
Передача сообщений предлагает решение этой проблемы: функция repr
вызывает функцию с именем __repr__
на своём параметре.
>>> today.__repr__()
'datetime.date(2011, 9, 12)'
Реализуя ту же функцию на пользовательских классах, мы можем расширить применимость repr
до любых классов, которые мы создадим позже. Этот пример подчёркивает ещё одно важное преимущество передачи сообщений: она обеспечивает механизм для расширения сферы действия существующих функций на новые объекты.
Функция str
реализуется аналогичным образом: она вызывает функцию с именем __str__
на своём параметре.
>>> today.__str__()
'2011-09-12'
Эти полиморфные функции являются примером более общего принципа: конкретная функция должна работать с несколькими типами данных. Здесь приведённый пример механизма передачи сообщений является лишь одним из членов семейства полиморфных функций.
Использование объектов или функций для управления сложностью данных является мощным инструментом для абстракции данных. Абстрактные типы данных позволяют нам устанавливать границы между представлением данных и функциями, используемыми для работы с данными. Однако в больших программах может быть не всегда уместно говорить о «базовом представлении» для определённого типа данных в контексте программы. Во-первых, у объекта может быть несколько практически эквивалентных представлений, и мы можем пожелать разработать систему, способную обрабатывать множественные представления.
Чтобы проиллюстрировать это, рассмотрим простой пример комплексных чисел, которые могут быть представлены двумя почти эквивалентными способами: прямоугольными координатами (действительная и мнимая части) и полярными координатами (модуль и угол). Иногда прямоугольное представление более уместно, а иногда полярное представление более уместно. Комплексные числа могут быть представлены обоими способами, и функции, работающие с комплексными числами, могут обрабатывать оба представления, так что такая система имеет смысл.
Что ещё более важно, крупные программные системы обычно разрабатываются многими людьми в течение длительного времени, и требования могут меняться со временем. В такой среде невозможно заранее согласовать схему представления данных. Помимо изоляции представления и использования данных через границы абстракции, нам необходимо изолировать границы различных схем проектирования и позволить различным схемам сосуществовать в одной программе. Кроме того, поскольку большие программы обычно создаются путём объединения существующих модулей, эти модули будут разрабатываться отдельно, и нам нужен стандарт, который позволит программистам постепенно объединять модули в большую систему. То есть не нужно повторно проектировать или реализовывать эти модули.
Начнём с простейшего примера комплексных чисел. Мы увидим, как передача сообщений позволяет нам разработать отдельные прямоугольные и полярные представления комплексных чисел и как мы используем полиморфный селектор для определения арифметических операций над комплексными числами (add_complex
и mul_complex
). Полиморфный селектор может получить доступ к частям комплексного числа независимо от способа представления чисел. Полученная система комплексных чисел содержит две различные абстрактные границы: они отделяют высокоуровневые операции от низкоуровневых представлений. Кроме того, существует вертикальная граница, которая позволяет нам независимо разрабатывать альтернативные представления.
Как примечание, мы разрабатываем систему, которая выполняет арифметические операции над комплексными числами в качестве простого, но нереалистичного примера использования полиморфных операций. Комплексные числа уже встроены в Python, но мы всё равно реализуем их здесь.
Подобно рациональным числам, комплексные числа естественно представляются парами. Комплексные числа можно рассматривать как двумерное пространство с двумя ортогональными осями, действительной осью и мнимой осью. С этой точки зрения комплексное число z = x + y * i
(где i*i = -1
) можно рассматривать как точку на плоскости, имеющую действительную часть x
и мнимую часть y
. Сложение комплексных чисел включает в себя сложение их действительных и мнимых частей.
При умножении комплексных чисел более естественным является представление в полярных координатах. Умножение двух комплексных чисел — это растяжение одного комплексного числа в соответствии с длиной другого комплексного числа, а затем поворот результата на угол другого комплексного числа.
Таким образом, комплексные числа имеют два разных представления, которые подходят для разных операций. Однако с точки зрения людей, пишущих программы, использующие комплексные числа, все операции с комплексными числами должны быть доступны независимо от того, какое представление использует компьютер.
Интерфейс. Передача сообщений не только обеспечивает способ сборки поведения и данных. Она также позволяет различным типам данных реагировать на одни и те же сообщения по-разному. Обмен общими сообщениями между объектами, приводящий к аналогичному поведению, является мощным средством абстракции.
Как и раньше, абстрактный тип данных определяется конструктором, селектором и дополнительными условиями для поведения. Связанное понятие — интерфейс, набор общих сообщений с их значениями. Объекты, способные представлять строки, реализуют общий интерфейс, который можно представить в виде строк.
В примере с комплексными числами интерфейс требует реализации четырёх арифметических операций: real
, imag
, magnitude
и angle
. Мы можем использовать эти сообщения для реализации сложения и умножения.
У нас есть два абстрактных типа данных комплексных чисел с разными конструкторами.
ComplexRI
конструирует комплексные числа из действительной и мнимой частей.ComplexMA
конструирует комплексные числа из модуля и угла.Используя эти сообщения и конструкторы, мы можем реализовать арифметику комплексных чисел:
>>> def add_complex(z1, z2):
return ComplexRI(z1.real + z2.real, z1.imag + z2.imag)
>>> def mul_complex(z1, z2):
return ComplexMA(z1.magnitude * z2.magnitude, z1.angle + z2.angle)
Термины «абстрактный тип данных» (ADT) и «интерфейс» связаны тонко. ADT описывает класс вещей с полным абстрактным представлением, в то время как интерфейс соответствует классу вещей, которые могут совместно использовать поведение. ``` import atan2
class ComplexRI(object): def init(self, real, imag): self.real = real self.imag = imag
@property
def magnitude(self):
return (self.real ** 2 + self.imag ** 2) ** 0.5
@property
def angle(self):
return atan2(self.imag, self.real)
def __repr__(self):
return 'ComplexRI({0}, {1})'.format(self.real, self.imag)
Второй вариант реализации с использованием модуля и угла предоставляет тот же интерфейс, поскольку он реагирует на один и тот же набор сообщений.
```python
from math import sin, cos
class ComplexMA(object):
def __init__(self, magnitude, angle):
self.magnitude = magnitude
self.angle = angle
@property
def real(self):
return self.magnitude * cos(self.angle)
@property
def imag(self):
return self.magnitude * sin(self.angle)
def __repr__(self):
return 'ComplexMA({0}, {1})'.format(self.magnitude, self.angle)
Фактически, наши реализации add_complex
и mul_complex
не завершены; каждый класс комплексных чисел может использоваться для любого параметра любой арифметической функции. Объектная система не связывает явно эти два типа комплексных чисел каким-либо образом (например, через наследование), что требует пояснения. Мы уже реализовали абстракцию комплексных чисел, используя общий набор сообщений и интерфейсов между двумя классами.
from math import pi
add_complex(ComplexRI(1, 2), ComplexMA(2, pi/2))
mul_complex(ComplexRI(0, 1), ComplexRI(0, 1))
Кодирование интерфейса для различных представлений имеет хорошие свойства. Классы, используемые для каждого представления, могут разрабатываться независимо; им нужно только следовать общим именам атрибутов, которые они разделяют. Этот интерфейс также является расширяемым. Если другой программист хочет добавить третье представление комплексных чисел к той же программе, им нужно будет использовать те же атрибуты для создания другого класса.
Специальные методы. Встроенные арифметические операторы могут быть расширены таким же образом, как и метод __repr__
; они являются именами специальных методов, соответствующими операторам арифметики, логики и последовательности Python.
Чтобы сделать наш код более читаемым, мы можем захотеть использовать операторы +
и *
при выполнении сложения и умножения комплексных чисел. Добавление следующих методов в два класса комплексных чисел позволит использовать эти операторы и функции operator
модуля add
и mul
.
ComplexRI.__add__ = lambda self, other: add_complex(self, other)
ComplexMA.__add__ = lambda self, other: add_complex(self, other)
ComplexRI.__mul__ = lambda self, other: mul_complex(self, other)
ComplexMA.__mul__ = lambda self, other: mul_complex(self, other)
Теперь мы можем использовать префиксные символы для наших пользовательских классов.
ComplexRI(1, 2) + ComplexMA(2, 0)
ComplexRI(0, 1) * ComplexRI(0, 1)
Расширенное чтение. Чтобы решить выражение, содержащее оператор +
, Python проверяет специальные методы __add__
левого операнда и __radd__
правого операнда. Если один из них найден, этот метод вызывается с другим операндом в качестве значения аргумента.
В Python решение выражения, содержащего любой тип оператора, включая оператор среза и логические операторы, следует аналогичному протоколу. Python документация перечисляет полные имена специальных методов. Dive into Python 3 описывает многие детали, связанные с интерпретатором Python в главе Имена специальных методов.
Наша реализация комплексных чисел создала два типа данных, которые могут быть преобразованы друг в друга с помощью функций add_complex
и mul_complex
. Теперь мы рассмотрим, как использовать ту же концепцию не только для определения обобщённых операций для разных представлений, но и для определения обобщённых операций, которые можно использовать для разных типов, не разделяющих общую структуру.
До сих пор определённые нами операции обрабатывали разные типы данных отдельно. Таким образом, существует отдельный пакет для сложения, например, двух рациональных чисел или двух комплексных чисел. Мы не учли, что имеет смысл определять операции между типами, например, складывать комплексное число с рациональным числом. Мы прошли через значительные трудности, вводя границы между частями программы, чтобы их можно было разрабатывать и понимать независимо.
Мы хотим ввести операции над типами в некотором точно контролируемом способе, позволяющем поддерживать их без серьёзного нарушения границ абстракции. Между желаемыми результатами могут возникнуть некоторые противоречия: мы хотим иметь возможность складывать рациональное число с комплексным числом, а также хотим использовать обобщённую функцию add
, которая правильно обрабатывает все типы значений. В то же время мы хотим изолировать детали комплексных и рациональных чисел, чтобы сохранить модульность программы.
Давайте используем встроенную объектную систему Python для переписать реализацию рациональных чисел. Как и раньше, мы будем хранить рациональные числа в нижнем уровне как числитель и знаменатель.
from fractions import gcd
class Rational(object):
def __init__(self, numer, denom):
g = gcd(numer, denom)
self.numer = numer // g
self.denom = denom // g
def __repr__(self):
return 'Rational({0}, {1})'.format(self.numer, self.denom)
Новая реализация рациональных чисел включает сложение и умножение, аналогичные предыдущим.
def add_rational(x, y):
nx, dx = x.numer, x.denom
ny, dy = y.numer, y.denom
return Rational(nx * dy + ny * dx, dx * dy)
def mul_rational(x, y):
return Rational(x.numer * y.numer, x.denom * y.denom)
Распределение по типам. Один из способов обработки операций над разными типами — разработать отдельную функцию для каждой возможной комбинации типов, которую можно использовать с этим типом. Например, мы могли бы расширить нашу реализацию комплексных чисел, чтобы предоставить функции для добавления комплексного числа к рациональному числу. Мы можем использовать механизм, называемый распределением по типам, для более общего предоставления этой функции.
Концепция распределения по типам заключается в написании функции, которая сначала проверяет тип полученного параметра, а затем выполняет соответствующий код. В Python тип объекта можно проверить с помощью встроенной функции type
.
def iscomplex(z):
return type(z) in (ComplexRI, ComplexMA)
def isrational(z):
return type(z) == Rational
Здесь мы полагаемся на факт, что каждый объект знает свой тип, и мы можем использовать функцию Python type
, чтобы получить тип. Даже если функция type
недоступна, мы всё равно можем реализовать iscomplex
и isrational
, используя Rational
, ComplexRI
и ComplexMA
.
Рассмотрим следующую реализацию add
, в которой явно проверяются типы двух параметров. Здесь мы не будем явно использовать специальные методы Python, такие как __add__
.
def add_complex_and_rational(z, r):
pass
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )