ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 382
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
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