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

  • Функции isinstance() и issubclass()

  • Когда использовать объектно-ориентированные статические средства и средства уровня классов

  • Термины объектно-ориентированного программирования

  • Когда наследование не используется

  • Чистыйкод дляпродолжающи х


    Скачать 7.85 Mb.
    НазваниеЧистыйкод дляпродолжающи х
    Дата13.05.2023
    Размер7.85 Mb.
    Формат файлаpdf
    Имя файлаPython_Chisty_kod_dlya_prodolzhayuschikh_2022_El_Sveygart.pdf
    ТипДокументы
    #1127485
    страница36 из 40
    1   ...   32   33   34   35   36   37   38   39   40
    Обратная сторона наследования
    Главный недостаток наследования связан с тем, что любые будущие изменения в родительском классе обязательно наследуются всеми его дочерними классами.
    В большинстве случаев такое жесткое связывание — именно то, что требуется.

    344
    Глава 16.Объектно-ориентированное программирование и наследование
    Но в некоторых ситуациях требования к коду плохо вписываются в модель на- следования.
    Представьте, что в программе моделирования дорожного движения используются классы
    Car
    ,
    Motorcycle и
    LunarRover
    . Они содержат похожие методы — такие как startIgnition()
    и changeTire()
    . Вместо того чтобы копировать этот код в каждый класс, можно создать родительский класс
    Vehicle
    , которому будут наследовать классы
    Car
    ,
    Motorcycle и
    LunarRover
    . Если теперь вам потребуется исправить ошибку, скажем, в методе changeTire()
    , изменения придется вносить только в од- ном месте. Это особенно полезно, если классу
    Vehicle наследуют десятки разных классов, связанных с транспортными средствами. Код этих классов будет выглядеть примерно так:
    class Vehicle:
    def __init__(self):
    print('Vehicle created.')
    def startIgnition(self):
    pass # Здесь размещается код зажигания.
    def changeTire(self):
    pass # Здесь размещается код замены шин.
    class Car(Vehicle):
    def __init__(self):
    print('Car created.')
    class Motorcycle(Vehicle):
    def __init__(self):
    print('Motorcycle created.')
    class LunarRover(Vehicle):
    def __init__(self):
    print('LunarRover created.')
    Но все будущие изменения в
    Vehicle также будут распространяться и на эти классы.
    Что произойдет, если понадобится добавить метод changeSparkPlug()
    ? У машин и мотоциклов есть свечи зажигания, но у луноходов (
    LunarRover
    ) их нет. Предпочи- тая композицию наследованию, можно создать раздельные классы
    CombustionEngine
    (двигатель внутреннего сгорания) и
    ElectricEngine
    (электрический двигатель).
    Затем мы проектируем класс
    Vehicle
    , так чтобы он содержал атрибут engine
    — либо
    CombustionEngine
    , либо
    ElectricEngine
    — с соответствующими методами:
    class CombustionEngine:
    def __init__(self):
    print('Combustion engine created.')
    def changeSparkPlug(self):
    pass # Здесь размещается код замены свечи зажигания.
    class ElectricEngine:

    Как работает наследование
    345
    def __init__(self):
    print('Electric engine created.')
    class Vehicle:
    def __init__(self):
    print('Vehicle created.')
    self.engine = CombustionEngine() # Используется по умолчанию.
    --snip-- class LunarRover(Vehicle):
    def __init__(self):
    print('LunarRover created.')
    self.engine = ElectricEngine()
    Возможно, вам придется переписать большие объемы кода, особенно если програм- ма содержит несколько классов, наследующих существующему классу
    Vehicle
    : все вызовы vehicleObj.changeSparkPlug()
    должны быть преобразованы в vehicleObj.
    engine.changeSparkPlug()
    для каждого объекта класса
    Vehicle или его субклассов.
    Так как столь значительные изменения могут привести к появлению ошибок, воз- можно, вы предпочтете, чтобы метод changeSparkPlug()
    для
    LunarVehicle не делал ничего. В этом случае в питоническом стиле следует присвоить changeSparkPlug значение
    None внутри класса
    LunarVehicle
    :
    class LunarRover(Vehicle):
    changeSparkPlug = None def __init__(self):
    print('LunarRover created.')
    В строке changeSparkPlug
    =
    None используется синтаксис, описанный в разделе
    «Атрибуты классов» этой главы. В следующем фрагменте переопределяется ме- тод changeSparkPlug()
    , унаследованный от
    Vehicle
    , и при вызове его с объектом
    LunarRover происходит ошибка:
    >>> myVehicle = LunarRover()
    LunarRover created.
    >>> myVehicle.changeSparkPlug()
    Traceback (most recent call last):
    File "", line 1, in
    TypeError: 'NoneType' object is not callable
    Эта ошибка позволит быстро и непосредственно узнать о проблеме, если вы по- пытаетесь вызвать этот неподходящий метод с объектом
    LunarRover
    . Любые до- черние классы
    LunarRover также унаследуют значение
    None для changeSparkPlug()
    Сообщение об ошибке
    TypeError:
    'NoneType'
    object is not callable
    (TypeError: объект 'NoneType'
    не может вызываться) информирует, что программист класса
    LunarRover намеренно задал для метода changeSparkPlug()
    значение
    None
    . Если бы такой метод не существовал изначально, то вы бы получили сообщение об ошибке

    346
    Глава 16.Объектно-ориентированное программирование и наследование
    NameError:
    name
    'changeSparkPlug'
    is not defined error message
    (NameError: имя 'changeSparkPlug' не определено).
    Применение наследования для создания классов нередко оборачивается слож- ностью и противоречиями. Часто вместо наследования лучше воспользоваться композицией.
    Функции isinstance() и issubclass()
    Если вы хотите узнать тип объекта, можно передать объект встроенной функции type()
    , о которой я упоминал в предыдущей главе. Но если вы выполняете про- верку типа для объекта, лучше воспользоваться более гибкой встроенной функ- цией isinstance()
    . Функция isinstance()
    вернет
    True
    , если объект относится к заданному классу или одному из его субклассов. Введите следующий фрагмент в интерактивной оболочке:
    >>> class ParentClass:
    ... pass
    >>> class ChildClass(ParentClass):
    ... pass
    >>> parent = ParentClass() # Создать объект ParentClass.
    >>> child = ChildClass() # Создать объект ChildClass.
    >>> isinstance(parent, ParentClass)
    True
    >>> isinstance(parent, ChildClass)
    False
    >>> isinstance(child, ChildClass)

    True
    >>> isinstance(child, ParentClass)

    True
    Функция isinstance()
    указывает, что объект
    ChildClass из child является экзем- пляром
    ChildClass

    и экземпляром
    ParentClass

    . Выглядит логично, так как объект
    ChildClass может рассматриваться как разновидность объекта
    ParentClass
    Во втором аргументе также можно передать кортеж объектов классов, чтобы про- верить, относится ли первый аргумент к одному из классов в кортеже:
    >>> isinstance(42, (int, str, bool)) # True, если 42 имеет тип int, str или bool.
    True
    Встроенная функция issubclass()
    — она используется реже — может сообщить, является ли объект класса, переданный в первом аргументе, субклассом (или от- носится к тому же классу), что и объект класса, переданный во втором аргументе:

    Методы классов
    347
    >>> issubclass(ChildClass, ParentClass) # ChildClass субклассирует ParentClass.
    True
    >>> issubclass(ChildClass, str) # ChildClass не субклассирует str.
    False
    >>> issubclass(ChildClass, ChildClass) # ChildClass имеет тип ChildClass.
    True
    Как и в случае с isinstance()
    , во втором аргументе issubclass()
    можно передать кортеж аргументов, чтобы проверить, является ли первый аргумент субклассом одного из классов в кортеже. Принципиальное отличие isinstance()
    и issubclass()
    заключается в том, что issubclass()
    передаются два объекта классов, а isinstance()
    передается объект и объект класса.
    Методы классов
    Метод класса связывается с классом, а не с его отдельными объектами (в отли- чие от обычных методов). Метод класса можно узнать в коде по двум признакам: по декоратору
    @classmethod перед командой def метода и по использованию cls в первом параметре, как показано в следующем примере.
    class ExampleClass:
    def exampleRegularMethod(self):
    print('This is a regular method.')
    @classmethod def exampleClassMethod(cls):
    print('This is a class method.')
    # Вызов метода класса без создания экземпляра:
    ExampleClass.exampleClassMethod()
    obj = ExampleClass()
    # С предыдущей строкой эти две строки эквивалентны:
    obj.exampleClassMethod()
    obj.__class__.exampleClassMethod()
    Параметр cls работает как self
    , не считая того, что self содержит ссылку на объект, а параметр cls
    — ссылку на класс объекта. Это означает, что код метода класса не может обратиться к атрибутам отдельных объектов или вызывать обычные методы объекта. Метод класса может вызывать только другие методы класса или обращать- ся к атрибутам класса. Мы используем имя cls
    , потому что class является ключе- вым словом Python и, как и любые другие ключевые слова (такие как if
    , while или import
    ), оно не может использоваться для имен параметров. Методы класса часто вызываются через объект класса (например,
    ExampleClass.exampleClassMethod()
    ).
    Но их также можно вызывать с любым экземпляром класса — например, obj.
    exampleClassMethod()

    348
    Глава 16.Объектно-ориентированное программирование и наследование
    Методы классов используются не так часто. Самый распространенный сценарий использования — определение альтернативных конструкторов, кроме
    __init__()
    Например, что если функция-конструктор может получать либо строку с данными, необходимыми новому объекту, либо строку с именем файла, который содержит данные для нового объекта? Список параметров метода
    __init__()
    не должен быть длинным и запутанным. Вместо этого можно воспользоваться методом класса, воз- вращающим новый объект.
    Для примера создадим класс
    AsciiArt
    . Как было показано в главе 14, ASCII-графика формирует изображение из символов текста.
    class AsciiArt:
    def __init__(self, characters):
    self._characters = characters
    @classmethod def fromFile(cls, filename):
    with open(filename) as fileObj:
    characters = fileObj.read()
    return cls(characters) def display(self):
    print(self._characters)
    # Другие методы AsciiArt...
    face1 = AsciiArt(' _______\n' +
    '| . . |\n' +
    '| \\___/ |\n' +
    '|_______|')
    face1.display()
    face2 = AsciiArt.fromFile('face.txt')
    face2.display()
    Класс
    AsciiArt содержит метод
    __init__()
    , которому может передаваться набор символов в виде строки. Он также содержит метод класса fromFile()
    , которому мо- жет передаваться строка с именем текстового файла, содержащего ASCII-графику.
    Оба метода создают объекты
    AsciiArt
    Если запустить эту программу и если существует файл face.txt с изображением в ASCII-графике, результат будет выглядеть примерно так:
    _______
    | . . |
    | \___/ |
    |_______|
    _______
    | . . |
    | \___/ |
    |_______|

    Атрибуты классов
    349
    Метод класса fromFile()
    несколько упрощает код по сравнению с выполнением всех операций в
    __init__()
    У методов классов есть и другое преимущество: субкласс
    AsciiArt может наследо- вать свой метод fromFile()
    (и переопределить его при необходимости). Это объ- ясняет, почему в метод fromFile()
    класса
    AsciiArt включен вызов cls(characters)
    , а не
    AsciiArt(characters)
    . Вызов cls()
    также будет работать в субклассах
    AsciiArt в неизменном виде, потому что класс
    AsciiArt не фиксируется в методе. Но вызов
    AsciiArt()
    всегда будет вызывать метод
    __init__()
    класса
    AsciiArt вместо мето- да
    __init__()
    субкласса. Рассматривайте cls как объект, представляющий класс.
    По аналогии с тем, как обычные методы всегда должны использовать параметр self где-то в коде, метод класса всегда использует свой параметр cls
    . Если в коде вашего метода класса параметр cls никогда не применяется, это признак того, что метод класса, возможно, стоило бы оформить в виде обычной функции.
    Атрибуты классов
    Атрибут класса представляет собой переменную, которая принадлежит классу, а не объекту. Атрибуты класса создаются внутри класса, но вне любых методов, подобно тому как глобальные переменные создаются в файле
    .py
    , но за пределами любых функций.
    Ниже приведен пример атрибута класса с именем count
    , который отслеживает количество созданных объектов
    CreateCounter
    :
    class CreateCounter:
    count = 0 # This is a class attribute.
    def __init__(self):
    CreateCounter.count += 1
    print('Objects created:', CreateCounter.count) # Выводит 0.
    a = CreateCounter()
    b = CreateCounter()
    c = CreateCounter()
    print('Objects created:', CreateCounter.count) # Выводит 3.
    Класс
    CreateCounter содержит один атрибут класса с именем count
    . Все объекты
    CreateCounter совместно используют этот атрибут (вместо того, чтобы иметь соб- ственные атрибуты count
    ). Это объясняет, почему строка
    CreateCounter.count
    +=
    1
    в функции-конструкторе подсчитывает все созданные объекты
    CreateCounter
    При запуске этой программы результат выглядит так:
    Objects created: 0
    Objects created: 3

    350
    Глава 16.Объектно-ориентированное программирование и наследование
    Атрибуты классов используются редко. Даже пример с подсчетом количества соз- данных объектов
    CreateCounter проще реализуется на базе глобальной переменной вместо атрибута класса.
    Статические методы
    Статический метод не имеет параметра self или cls
    . По сути статические мето- ды представляют собой обычные функции, потому что они не могут обращаться к атрибутам и методам класса или его объектов. Необходимость в использовании статических методов в Python возникает очень редко. Если вы решите воспользо- ваться ими, вам наверняка стоит подумать об использовании обычной функции.
    Чтобы определить статический метод, поставьте декоратор
    @staticmethod перед соответствующей командой def
    . Пример статического метода:
    class ExampleClassWithStaticMethod:
    @staticmethod def sayHello():
    print('Hello!')
    # Объект не создается, перед sayHello() указывается имя класса:
    ExampleClassWithStaticMethod.sayHello()
    Между статическим методом sayHello()
    в классе
    ExampleClassWithStaticMethod и функцией sayHello()
    почти нет различий. Вполне возможно, что вы предпочтете функцию, потому что ее можно вызывать без указания имени класса.
    Статические методы чаще встречаются в других языках, где отсутствуют гибкие возможности языка Python. Статические методы в Python моделируют функцио- нальность других языков, но не обладают особой практической ценностью.
    Когда использовать объектно-ориентированные
    статические средства и средства уровня классов
    Вам редко могут понадобиться методы классов, атрибуты классов и статические методы. Если вы думаете: «А нельзя ли вместо этого использовать функцию или глобальную переменную?», то скорее всего вам не стоит использовать метод класса, атрибут класса или статический метод. Они рассматриваются в этой книге только по одной причине: чтобы вы узнали их, если они встретятся вам в коде, но пользоваться ими я не рекомендую. Они могут пригодиться, если вы создаете собственный фреймворк с нетривиальным семейством классов, которые должны субклассироваться программистами, использующими фреймворк. Скорее всего, при написании обычных приложений Python они вам не понадобятся.

    Термины объектно-ориентированного программирования
    351
    Дополнительную информацию об этих возможностях, о том, почему они вам нужны
    (или не нужны), вы можете почерпнуть в публикациях Филипа Дж. Эби (Phillip
    J. Eby) «Python Is Not Java» (https://dirtsimple.org/2004/12/python-is-not-java.html) и Райана Томайко (Ryan Tomayko) «The Static Method Thing» (https://tomayko.com/
    blog/2004/the-static-method-thing).
    Термины объектно-ориентированного
    программирования
    В объяснениях ООП часто встречаются специальные термины: наследование, инкапсуляция, полиморфизм и т. д. Нельзя сказать, что знать их абсолютно необ- ходимо, но следует хотя бы понимать их смысл на базовом уровне. Наследование уже упоминалось ранее, а о других терминах я расскажу сейчас.
    Инкапсуляция
    Слово «инкапсуляция» имеет два распространенных и взаимосвязанных опреде- ления. Первое: инкапсуляцией называется объединение взаимосвязанных данных и кода в одно целое. В сущности, именно это и делают классы: они объединяют взаимосвязанные атрибуты и методы. Например, наш класс
    WizCoin инкапсулирует три целых числа (
    knuts
    , sickles и galleons
    ) в одном объекте
    WizCoin
    Второе определение: инкапсуляцией называется механизм сокрытия инфор- мации, позволяющий объектам скрывать сложные подробности реализации, то есть внутреннее устройство объекта. Пример такого рода встречался в подраз- деле «Приватные атрибуты и приватные методы», с. 324, где объекты
    BankAccount предоставляют методы deposit()
    и withdraw()
    для сокрытия подробностей работы с атрибутами
    _balance
    . Функции позволяют осуществить похожую цель создания
    «черного ящика» — например, алгоритм вычисления квадратного корня функцией math.sqrt()
    не виден пользователю. Все, что вам нужно знать, — эта функция воз- вращает квадратный корень того числа, которое ей было передано.
    Полиморфизм
    Полиморфизм позволяет рассматривать объекты одного типа как объекты другого типа. Например, функция len()
    возвращает длину переданного ей аргумента. Функ- ции len()
    можно передать строку, чтобы узнать, сколько символов она содержит, но len()
    также можно передать список или словарь, чтобы узнать, сколько элементов или пар «ключ — значение» они содержат. Эта разновидность полиморфизма на- зывается параметрическим полиморфизмом, или обобщением, потому что она может работать с объектами многих разных типов.

    352
    Глава 16.Объектно-ориентированное программирование и наследование
    Термином «полиморфизм» также иногда обозначают ситуативный (ad hoc) по-
    лиморфизм, или перегрузку операторов, когда операторы (такие как
    +
    или
    *
    ) де- монстрируют разное поведение в зависимости от типа объектов, с которыми они работают. Например, оператор
    +
    выполняет математическое сложение для двух целых чисел или чисел с плавающей точкой, но с двумя строками он выполняет конкатенацию. Перегрузке операторов посвящена глава 17.
    Когда наследование не используется
    Наследование легко приводит к излишнему переусложнению классов. Как выра- зился Лучано Рамальо (Luciano Ramalho), «Упорядочение объектов в аккуратную иерархию апеллирует к нашему чувству порядка; программисты делают это просто для развлечения». Мы создаем классы, субклассы и субсубклассы, когда того же эффекта можно было бы достичь с одним классом или парой функций. Но вспом- ните принцип из «Дзен Python» (глава 6): «Простое лучше, чем сложное».
    Использование ООП позволяет разделить код на меньшие единицы (в данном слу- чае классы), которые проще понять, чем один большой файл
    .py с сотнями функций, не следующих в каком-то определенном порядке. Наследование полезно, когда вы определяете несколько функций, работающих с одной структурой данных словаря или списка. Объединение таких функций в класс принесет пользу.
    Однако возможны и ситуации, когда создавать класс или использовать наследо- вание не обязательно.
    Если ваш класс состоит из методов, в которых совсем не используются па- раметры self или cls
    , удалите класс и примените функции вместо методов.
    Если вы создали родителя, который имеет только один дочерний класс, но объекты родительского класса нигде не используются, их можно объединить в один класс.
    Если вы создаете более трех или четырех уровней субклассирования, вероят- но, вы злоупотребляете наследованием. Переработайте субклассы и уберите лишние.
    Как и в примере с двумя версиями программы «Крестики-нолики» из предыду- щей главы, вы можете отказаться от использования классов и при этом иметь вполне работоспособную, избавленную от ошибок программу. Не думайте, что программу непременно нужно проектировать в виде сложной паутины классов.
    Простое работоспособное решение лучше сложного, которое не работает. Джоэл
    Спольски (Joel Spolsky) пишет об этом в своем сообщении в блоге «Don’t Let the
    Astronaut Architects Scare You» (https://www.joelonsoftware.com/2001/04/21/dont-
    let-architecture-astronauts-scare-you/).

    Множественное наследование
    353
    Вы должны знать, как работают такие объектно-ориентированные концепции, как наследование, потому что они помогают организовать код, а также упрощают разработку и отладку. Благодаря своей гибкости Python не только предоставляет объектно-ориентированные средства, но и не требует обязательного их использо- вания, если они плохо подходят для целей вашей программы.
    1   ...   32   33   34   35   36   37   38   39   40


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