Главная страница
Навигация по странице:

  • Наследование и полиморфизм

  • __init__() distance_from_origin()__eq__() __repr__() __str__() Circle xy radius__new__()__init__()

  • Использование свойств для управления доступом к атрибутам

  • Создание полных и полностью интегрированных типов данных

  • Программирование на Python 3. Руководство издательство СимволПлюс


    Скачать 3.74 Mb.
    НазваниеРуководство издательство СимволПлюс
    Дата10.11.2022
    Размер3.74 Mb.
    Формат файлаpdf
    Имя файлаПрограммирование на Python 3.pdf
    ТипРуководство
    #780382
    страница34 из 74
    1   ...   30   31   32   33   34   35   36   37   ...   74

    FuzzyBool
    , стр. 292

    Собственные классы
    285
    Встроенная функция repr() вызывает специальный ме
    тод __repr__() указанного объекта и возвращает его ре
    зультат. Возвращаемая строка может быть одного из двух видов. Один вид – когда возвращаемая строка с по
    мощью функции eval() может быть преобразована в объ
    ект, эквивалентный тому, что был передан функции repr()
    . Второй вид используется, когда такое преобразо
    вание невозможно. Примеры таких ситуаций будут по
    казаны позднее. Ниже показано, как можно выполнить преобразование объекта Point в строку и обратно – в объ
    ект Point:
    p = Shape.Point(3, 9)
    repr(p) # вернет: 'Point(3, 9)'
    q = eval(p.__module__ + "." + repr(p))
    repr(q) # вернет: 'Point(3, 9)'
    При вызове функции eval() мы должны передать имя модуля, если использовалась инструкция import Shape.
    (Это не требуется, если импортирование выполнялось иным способом, например, from Shape import Point.) Каж
    дому объекту интерпретатор Python присваивает не
    сколько частных атрибутов, один из которых __module__ –
    строка, хранящая имя модуля объекта, в данном случае "Shape"
    После выполнения этого фрагмента в нашем распоряже
    нии будет два объекта класса Point, p и q, с одинаковыми значениями атрибутов, поэтому операция сравнения го
    ворит о том, что они равны. Функция eval() возвращает результат выполнения переданной ей строки, которая должна содержать допустимую инструкцию языка Py
    thon.
    def __str__(self):
    return "({0.x!r}, {0.y!r})".format(self)
    Встроенная функция str() работает точно так же, как функция repr(),
    за исключением того, что она вызывает специальный метод __str__()
    объекта. Результатом работы этого метода должна быть строка, пред
    назначенная для восприятия человеком и которую не предполагается передавать функции eval(). Если продолжить предыдущий пример,
    вызов str(p) (или str(q)) вернул бы строку '(3, 9)'.
    Мы закончили рассмотрение простого класса Point, а также некоторых подробностей, которые важно знать, но не обязательно применять на практике. Класс Point хранит координаты (x, y) – важную часть дан
    ных, необходимых для представления окружностей, с которых мы на
    чали эту главу. В следующем подразделе будет показано, как создать собственный класс Circle, наследующий класс Point, чтобы нам не
    Метод
    str.
    format()
    , стр. 100
    Инструкция
    import
    , стр. 230
    Динамиче
    ское выпол
    нение про
    граммного кода, стр. 400

    286
    Глава 6. Объектно/ориентированное программирование приходилось дублировать программный код, создающий атрибуты x и y или метод distance_from_origin().
    Наследование и полиморфизм
    Класс Circle построен на основе класса Point, с использованием меха
    низма наследования. Класс Circle добавляет один атрибут данных (ra
    dius
    ) и три новых метода. Кроме того, он переопределяет несколько ме
    тодов класса Point. Ниже приводится полное определение класса:
    class Circle(Point):
    def __init__(self, radius, x=0, y=0):
    super().__init__(x, y)
    self.radius = radius def edge_distance_from_origin(self):
    return abs(self.distance_from_origin()  self.radius)
    def area(self):
    return math.pi * (self.radius ** 2)
    def circumference(self):
    return 2 * math.pi * self.radius def __eq__(self, other):
    return self.radius == other.radius and super().__eq__(other)
    def __repr__(self):
    return "Circle({0.radius!r}, {0.x!r}, {0.y!r})".format(self)
    def __str__(self):
    return repr(self)
    Наследование реализуется просто, посредством перечисления класса
    (или классов), который должен быть унаследован нашим классом,
    в строке с инструкцией class.
    1
    В данном случае мы наследуем класс
    Point
    – иерархия дерева наследования класса Circle приводится на рис. 6.3.
    Внутри метода __init__() мы используем функцию super() для вызова метода __init__() базового класса – он создает и инициализирует атри
    буты self.x и self.y. Пользователи класса могут попытаться опреде
    лить недопустимое значение радиуса, например –2. В следующем под
    разделе мы покажем, как предотвратить появление этой проблемы,
    для повышения устойчивости атрибутов используя свойства.
    Методы area() и circumference() достаточно очевидны. Метод edge_dis
    tance_from_origin()
    в ходе производимых вычислений вызывает метод distance_from_origin()
    . Так как класс Circle не реализует свой метод
    1
    Множественное наследование, абстрактные типы данных и другие, более сложные приемы объектноориентированного программирования рассмат
    риваются в главе 8.

    Собственные классы
    287
    distance_from_origin()
    , интерпретатор найдет и будет использовать ме
    тод базового класса Point. Сравните это с переопределением метода
    __eq__()
    . Этот метод сравнивает радиус окружности с радиусом другой окружности, и если они равны, то при помощи функции super() явно вызывается метод __eq__() базового класса. Если бы мы не использова
    ли функцию super(), мы могли бы попасть в бесконечную рекурсию,
    поскольку метод Circle.__eq__() продолжал бы вызывать сам себя. Об
    ратите также внимание на то, что в вызов super() мы не передаем аргу
    мент self, потому что интерпретатор сделает это автоматически.
    Ниже приводится пара примеров использования:
    p = Shape.Point(28, 45)
    c = Shape.Circle(5, 28, 45)
    p.distance_from_origin() # вернет: 53.0
    c.distance_from_origin() # вернет: 53.0
    Мы можем вызвать метод distance_from_origin() как для экземпляра класса Point, так и для экземпляра класса Circle, потому что класс
    Circle наследует класс Point.
    Полиморфизм подразумевает, что любой объект данного класса может использоваться, как если бы это был объект любого из базовых его классов. По этой причине, когда создается подкласс, нам требуется реализовать только необходимые дополнительные методы и переопре
    делить только те существующие методы, которые нам хотелось бы за
    менить. Переопределяя методы, мы можем в случае необходимости ис
    пользовать реализацию базовых классов, применяя функцию super()
    внутри переопределяемых методов.
    object
    __new__()
    __init__()
    __eq__()
    __repr__()
    __str__()
    Point x
    y
    __new__()
    __init__()
    distance_from_origin()
    __eq__()
    __repr__()
    __str__()
    Circle x
    y radius
    __new__()
    __init__()
    distance_from_origin()
    edge_distance_from_origin()
    area()
    circumference()
    __eq__()
    __repr__()
    __str__()
    Обозначения унаследован реализован
    переопределен
    Рис. 6.3. Дерево наследования класса Circle

    288
    Глава 6. Объектно/ориентированное программирование
    В случае с классом Circle мы реализовали дополнительные методы, та
    кие как area() и circumference(), и переопределили методы, которые не
    обходимо было изменить. Переопределить методы __repr__() и __str__()
    было необходимо потому, что без этого использовались бы методы ба
    зового класса, возвращающие строки с представлением класса Point,
    а не Circle. Переопределить методы __init__() и __eq__() было необ
    ходимо потому, что нам необходимо было учесть тот факт, что класс
    Circle имеет один дополнительный атрибут; и в обоих случаях была использована реализация базового класса, чтобы минимизировать объем работы, которую необходимо было выполнить.
    Классы Point и Circle можно считать полными, посколь
    ку они соответствуют нашим требованиям. Мы могли до
    бавить в них дополнительные методы, например, другие специальные методы сравнения, если бы нам было необ
    ходимо упорядочивать объекты классов Point и Circle.
    Еще можно было бы реализовать в классах Point и Circle метод копирования. В большинстве классов Python от
    сутствует метод copy() (за исключением dict.copy() и set.
    copy()
    ). Если нам потребуется скопировать экземпляр класса Point или Circle, мы легко можем сделать это, им
    портировав модуль copy и использовав функцию copy.co
    py()
    . (В случае с объектами классов Point и Circle нет не
    обходимости использовать функцию copy.deepcopy(), по
    тому что они содержат только неизменяемые перемен
    ные экземпляра.)
    Использование свойств для управления
    доступом к атрибутам
    В предыдущем подразделе класс Point поддерживал метод distance_
    from_origin()
    , а класс Circle – методы area(), circumference() и edge_di
    stance_from_origin()
    . Все эти методы возвращают единственное значе
    ние типа float, поэтому, с точки зрения пользователя классов, они точ
    но так же могли бы быть атрибутами данных, но доступными только для чтения. В файле ShapeAlt.py представлена альтернативная реали
    зация классов Point и Circle, где все упомянутые методы представля
    ются как свойства. Это позволяет нам писать программный код, как показано ниже:
    circle = Shape.Circle(5, 28, 45) # предполагается, что модуль ShapeAlt
    # был импортирован под именем Shape circle.radius # вернет: 5
    circle.edge_distance_from_origin # вернет: 48.0
    Ниже приводится реализация методов чтения для свойств area и edge_
    distance_from_origin класса ShapeAlt.Circle:
    Поверхно
    стное и глубокое копирование, стр. 173

    Собственные классы
    289
    @property def area(self):
    return math.pi * (self.radius ** 2)
    @property def edge_distance_from_origin(self):
    return abs(self.distance_from_origin  self.radius)
    Если мы реализуем только методы чтения, как это было сделано здесь,
    свойства будут доступны только для чтения. Программный код реали
    зации свойства area остался тем же самым, что и в реализации метода area()
    . Программный код реализации свойства edge_distance_from_ori
    gin несколько изменился, потому что теперь он обращается к свойству distance_from_origin базового класса, а не к методу distance_from_ori
    gin()
    . Самое заметное отличие между реализациями заключается в на
    личии декоратора property. Декоратор – это функция, которая в каче
    стве аргумента принимает функцию или метод и возвращает «декори
    рованную» версию, то есть версию функции или метода, измененную некоторым способом. Декоратор обозначается первым символом «@»
    в имени. Пока просто воспринимайте декораторы как элемент синтак
    сиса – в главе 8 будет показано, как можно создавать собственные де
    кораторы.
    Функциядекоратор property() – это встроенная функция, и она может принимать до четырех аргументов: функцию чтения, функцию запи
    си, функцию удаления и строку документирования. Фактически ис
    пользование имени @property равносильно вызову функции property()
    с единственным аргументом – функцией чтения. Мы могли бы создать свойство area, как показано ниже:
    def area(self):
    return math.pi * (self.radius ** 2)
    area = property(area)
    Мы редко используем такой синтаксис, потому что использование де
    коратора выглядит короче и понятнее.
    В предыдущем подразделе мы отмечали отсутствие проверки значе
    ний, записываемых в атрибут radius класса Circle. Мы можем реализо
    вать такую проверку, преобразовав атрибут radius в свойство. Для этого не потребуется изменять реализацию метода Circle.__init__(); любой другой программный код, обращающийся к атрибуту Circle.radius, бу
    дет продолжать корректно работать, только теперь значения будут проходить проверку при записи.
    Как правило, программисты, создающие программы на языке Python,
    используют свойства, а не явные методы чтения и записи (например,
    getRadius()
    и setRadius()), которые обычно используются в других язы
    ках программирования. Это обусловлено тем, что атрибут данных очень легко можно превратить в свойство, что никак не скажется на программном коде, использующем класс.

    290
    Глава 6. Объектно/ориентированное программирование
    Чтобы превратить атрибут в свойство, доступное для чтения и записи,
    нам необходимо создать частный атрибут, который будет являться фактическим хранилищем данных и будет использоваться методами чтения и записи. Ниже приводится полная реализация методов чте
    ния и записи, а также строка документирования:
    @property def radius(self):
    """Радиус окружности
    >>> circle = Circle(2)
    Traceback (most recent call last):
    AssertionError: radius must be nonzero and nonnegative
    >>> circle = Circle(4)
    >>> circle.radius = 1
    Traceback (most recent call last):
    AssertionError: radius must be nonzero and nonnegative
    >>> circle.radius = 6
    """
    return self.__radius
    @radius.setter def radius(self, radius):
    assert radius > 0, "radius must be nonzero and nonnegative"
    self.__radius = radius
    Чтобы убедиться, что записываемое значение радиуса больше нуля,
    используется инструкция assert; после проверки значение радиуса со
    храняется в частном атрибуте self.__radius. Примечательно, что мето
    ды чтения и записи (и метод удаления, если бы он нам потребовался)
    имеют одно и то же имя – они отличаются только декораторами, и де
    кораторы соответствующим образом переименовывают методы, чтобы исключить конфликты имен.
    Декоратор метода записи может показаться немного необычным на первый взгляд. Каждое создаваемое свойство имеет атрибут getter,
    setter или deleter, поэтому, как только свойство radius будет создано,
    появятся атрибуты radius.getter, radius.setter и radius.deleter. В ат
    рибут radius.getter декоратором @property записывается ссылка на ме
    тод чтения. Другие два атрибута устанавливаются интерпретатором так, что они ничего не делают (поэтому в атрибут ничего нельзя запи
    сать или удалить его), если они не были использованы как декорато
    ры; тогда они замещаются декорируемыми ими методами.
    Метод инициализации Circle.__init__() содержит инструкцию self.ra
    dius = radius
    . При выполнении она превратится в вызов метода записи для свойства radius, поэтому, если при создании объекта Circle будет указано недопустимое значение, будет возбуждено исключение Asser
    tionError
    . Точно так же, если будет произведена попытка установить недопустимое значение свойства radius у существующего объекта

    Собственные классы
    291
    класса Circle, снова будет вызван метод записи, который возбудит ис
    ключение. Строка документирования включает в себя доктесты, про
    веряющие корректное возбуждение исключений в этих случаях.
    Типы Point и Circle являются нашими собственными типами данных,
    обладающими достаточным объемом функциональных возможностей,
    чтобы быть полезными. Большинство типов данных, которые нам при
    дется создавать, будут похожи на эти типы данных, но иногда будет возникать необходимость в самостоятельном создании собственного полного типа данных. Пример такого типа данных мы увидим в сле
    дующем подразделе.
    Создание полных и полностью интегрированных
    типов данных
    В создании полного типа данных можно пойти двумя путями. Первый состоит в том, чтобы создать тип данных с самого начала. Хотя тип данных будет наследовать класс object (как и любой другой класс Py
    thon), тем не менее придется реализовать все атрибуты данных и мето
    ды (за исключением метода __new__()). Другой путь состоит в том, что наследовать существующий тип данных, напоминающий тот, что мы собираемся создать. В этом случае основная работа обычно связана с переопределением тех методов, поведение которых необходимо изме
    нить, и с «ликвидацией» тех методов, которые вообще являются неже
    лательными.
    В следующем подразделе мы реализуем тип данных FuzzyBool, начав с нуля, а в подразделе, следующем за ним, мы реализуем тот же самый тип данных, но при этом воспользуемся механизмом наследования,
    чтобы уменьшить объем работы, которую необходимо выполнить.
    Встроенный тип bool имеет два возможных значения (True и False), но в некоторых областях ИИ (искусственный интеллект) используется нечеткая логика, опирающаяся на значения, соответствующие поня
    тиям «истина» и «ложь», а также на промежуточные между ними.
    В наших реализациях мы будем использовать значения с плавающей точкой, где 0.0 будет соответствовать значению False, а 1.0 – значению
    True
    . В этой системе координат значение 0.5 будет обозначать 50про
    центную истинность, 0.25 – 25процентную истинность и т. д. Ниже приводятся несколько примеров использования (они работают совер
    шенно одинаково с любой из двух реализаций) :
    a = FuzzyBool.FuzzyBool(.875)
    b = FuzzyBool.FuzzyBool(.25)
    a >= b # вернет: True bool(a), bool(b) # вернет: (True, False)


    a # вернет: FuzzyBool(0.125)
    a & b # вернет: FuzzyBool(0.25)
    b |= FuzzyBool.FuzzyBool(.5) # теперь b имеет значение: FuzzyBool(0.5)
    "a={0:.1%} b={1:.0%}".format(a, b) # вернет: 'a=87.5% b=50%'

    292
    Глава 6. Объектно/ориентированное программирование
    Нам необходимо, чтобы тип FuzzyBool поддерживал полный набор опе
    раторов сравнения (<, <=, ==, !=, >=, >) и три основные логические опера
    ции: НЕ (), И (&) и ИЛИ (|). В дополнение к логическим операциям нам необходимо реализовать пару других логических методов – con
    junction()
    и disjunction(), способных принимать произвольное число значений типа FuzzyBool и возвращающих соответствующие результа
    ты типа FuzzyBool. И для полноты типа нам потребуется реализовать возможность преобразования в типы bool, int, float и str, а также обес
    печить получение репрезентативной формы, совместимой с функцией eval()
    . Наконец, тип FuzzyBool должен поддерживать спецификаторы формата метода str.format(), он должен иметь возможность использо
    ваться в качестве ключей словаря или членов множеств, значения ти
    па FuzzyBool должны быть неизменяемыми, при условии поддержки комбинированных операторов присваивания (&= и |=), чтобы обеспе
    чить дополнительные удобства в использовании.
    В табл. 6.1 (стр. 283) перечислены специальные методы операций сравнения, в табл. 6.2 (стр. 294) перечислены фундаментальные спе
    циальные методы и в табл. 6.3 (стр. 296) перечислены арифметиче
    ские специальные методы, включая методы реализации битовых опе
    раторов (, & и |), которые применительно к типу FuzzyBool играют роль логических операторов, а также арифметические операторы + и

    , ко
    торые в типе FuzzyBool не будут реализованы, как не имеющие смысла.
    1   ...   30   31   32   33   34   35   36   37   ...   74


    написать администратору сайта