Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
Обратная сторона наследования Главный недостаток наследования связан с тем, что любые будущие изменения в родительском классе обязательно наследуются всеми его дочерними классами. В большинстве случаев такое жесткое связывание — именно то, что требуется. 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 " 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 не только предоставляет объектно-ориентированные средства, но и не требует обязательного их использо- вания, если они плохо подходят для целей вашей программы. |