Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
ЧАСТЬ III ОБЪЕКТНО- ОРИЕНТИРОВАННЫЙ PYTHON 15 Объектно-ориентированное программирование и классы Объектно-ориентированное программирование, или ООП, — механизм языка программирования, позволяю- щий группировать переменные и функции в новые типы данных, называемые классами. На базе классов создаются объекты. Распределяя свой код по классам, можно разбить монолитную программу на меньшие части, которые проще по- нять и отладить. В небольших программах ООП добавляет не столько структуру, сколько рутину. Хотя некоторые языки (например, Java) требуют организации всего кода в классах, ООП-функциональность в Python не является обязательной. Программист может воспользоваться классами, если они ему нужны, или забыть про классы, если без них можно обойтись. В докладе разработчика Python Джека Дидериха (Jack Diederich) «Перестаньте писать классы» на конференции PyCon 2012 (https://youtu.be/o9pEzgHorH0/) рас- сматриваются некоторые ситуации, в которых программисты пишут классы, хотя можно было бы обойтись более простой функцией или модулем. Как бы то ни было, вам как программисту следует знать основы классов и их ис- пользования. Из этой главы вы узнаете, что такое классы, почему они используются в программах и какой синтаксис и концепции программирования лежат в их основе. ООП — обширная тема, и эта глава содержит только краткое введение в нее. Аналогия из реального мира: заполнение форм 317 Аналогия из реального мира: заполнение форм Скорее всего, вам неоднократно приходилось заполнять всевозможные формы, анкеты и бланки, бумажные или электронные: при посещении врача, для покупок в интернете или для приглашения на свадьбу. Формы предоставляют унифициро- ванный механизм сбора необходимой информации людьми или организациями. Разные формы предназначены для сбора разных видов информации. Врач описыва- ет состояние пациента, а на форме планирования свадьбы вы вводите информацию о приглашенных гостях. В Python термины «класс», «тип» и «тип данных» имеют одинаковый смысл. Класс, как и бумажная или электронная форма, представляет собой заготовку для создания объектов Python (также называемых экземплярами). Объекты содержат данные, которые представляют конкретного пациента, покупку в интернет-магазине или гостя на свадьбе. Классы напоминают пустые формы, а объекты, созданные на базе этих классов, — заполненные формы с реальными данными, которые требует форма. Так, на рис. 15.1 форму для подтверждения участия можно сравнить с клас- сом, а заполненную форму — с объектом. Классы и объекты также можно рассматривать как электронные таблицы (рис. 15.2). Просьба ответить до 16 июня. Просьба ответ ить до 16 июн я. Имя: Имя: Да, ябуду присутствовать. Да, ябуду присут ствовать. Нет, яне смогу присутствовать. Нет, яне смогу п рисутствовать. Количество гостей Количество гостей Элис Смит Классы Объекты 1 Рис. 15.1. Формы для приглашения гостей на свадьбу напоминают классы, а заполненные формы напоминают объекты 318 Глава 15.Объектно-ориентированное программирование и классы Рис. 15.2. Электронная таблица с данными гостей. Здесь RSVP означает «répondez s'il vous plaît» («просьба ответить») Заголовки столбцов определяют класс, а каждая отдельная строка таблицы — объект. Классы и объекты часто приравнивают к моделям элементов реального мира, но не путайте карту с территорией. Содержимое класса зависит от того, что должна сделать программа. На рис. 15.3 изображены некоторые объекты разных классов, представляющие одного и того же человека. Не считая имени, они содержат со- вершенно разную информацию. Кроме того, информация, содержащаяся в классах, зависит от потребностей вашей программы. Во многих учебниках ООП для примера используется класс Car , но при этом авторы забывают, что набор сведений, включаемых в класс, полностью зависит от того, какое приложение вы пишете. Не существует обобщенного класса Car , кото- рый бы включал метод honkHorn() (подать сигнал) или атрибут numberOfCupholders (количество подставок для стаканов) только потому, что этими характеристиками обладают реальные машины. Может быть, вы пишете веб-приложение для авто- салона, видеоигру с гонками или модель дорожного движения. Класс Car для ав- тосалона может содержать атрибуты milesPerGallon (количество миль на галлон) или manufacturersSuggestedRetailPrice (рекомендованная цена производителя), подобно тому как имена этих атрибутов могут быть включены в заголовки столбцов Создание объектов на базе классов 319 в электронных таблицах автосалона. Но в видеоигре и модели дорожного движения этих атрибутов не будет, потому что здесь они не актуальны. Класс Car для видео- игры может содержать метод explodeWithLargeFireball() (эффектно взорваться), но в приложение для автосалона и модели дорожного движения он не попадет… хочется надеяться. Рис. 15.3. Четыре объекта, созданные на базе разных классов. Объекты представляют одного и того же человека в зависимости от того, какая информация о человеке нужна приложению Создание объектов на базе классов Вы уже использовали классы и объекты в Python, даже если не создавали их сами. Вспомните модуль datetime , который содержит класс с именем date . Объекты клас- са datetime.date (также называемые объектами datetime.date или объектами date ) представляют конкретную дату. Введите следующий фрагмент в интерактивной оболочке, чтобы создать объект класса datetime.date : >>> import datetime >>> birthday = datetime.date(1999, 10, 31) # Передать год, месяц и день. >>> birthday.year 1999 >>> birthday.month 10 >>> birthday.day 31 >>> birthday.weekday() # weekday() - метод, на что указывают круглые скобки. 6 320 Глава 15.Объектно-ориентированное программирование и классы Атрибуты представляют собой переменные, связанные с объектом. Вызов datetime. date() создает новый объект date , инициализируемый аргументами 1999 , 10 , 31 , так что объект представляет дату 31 октября 1999 года. Эти аргументы присваиваются атрибу- там year , month и day класса date ; такие атрибуты присутствуют в каждом объекте date С подобной информацией метод weekday() класса может вычислить день недели. В приведенном примере он возвращает 6 — обозначение воскресенья, потому что согласно электронной документации Python возвращаемым значением weekday() является целое число от 0 (понедельник) до 6 (воскресенье). В документации так- же перечислены другие методы, содержащиеся в классе date . И хотя объект date содержит много атрибутов и методов, это все еще один объект, который можно сохранить в переменной, — такой как birthday в приведенном примере. Создание простого класса: WizCoin Создадим класс WizCoin , представляющий набор монет в вымышленной волшебной валюте. В этой валюте используются следующие номиналы: кнаты, сикли (29 кнатов) и галлеоны (17 сиклей, или 493 кната). Помните, что объекты класса WizCoin пред- ставляют количество монет разного номинала, а не денежную сумму. Условно говоря, этот класс сообщит, что у вас пять четвертаков и один гривенник, а не 1 р. 35 коп. Создайте файл с именем wizcoin.py и введите следующий код для создания класса WizCoin . Обратите внимание: имя метода __init__ начинается и завершается двумя символами подчеркивания (метод __init__ рассматривается в подразделе «Мето- ды, __init__() и self» этой главы): class WizCoin: ❶ def __init__(self, galleons, sickles, knuts): ❷ """Создание нового объекта WizCoin по значениям galleons, sickles и knuts.""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ВНИМАНИЕ: методы __init__() НИКОГДА не содержат команду return. def value(self): ❸ """The value (in knuts) of all the coins in this WizCoin object.""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) def weightInGrams(self): ❹ """Возвращает вес монет в граммах.""" return (self.galleons * 31.103) + (self.sickles * 11.34) + (self.knuts * 5.0) Программа определяет новый класс с именем WizCoin при помощи команды class ❶ При создании класса создается новый тип объектов. Использование команды Создание простого класса: WizCoin 321 class для определения класса напоминает команду def , задающую новые функции. Внутри блока кода, следующего за командой class , следуют три определения трех методов: __init__() (сокращение от initializer) ❷ , value() ❸ и weightInGrams() ❹ Обратите внимание: все методы имеют первый параметр с именем self , который будет рассмотрен в следующем разделе. По общепринятым соглашениям имена модулей (например, wizcoin в файле wiz- coin.py ) записываются в нижнем регистре, а имена классов (например, WizCoin ) на- чинаются с буквы верхнего регистра. К сожалению, некоторые классы стандартной библиотеки Python — такие как date — этому соглашению не следуют. Чтобы потренироваться в создании новых объектов класса WizCoin , введите сле- дующий исходный код в отдельном окне редактора и сохраните файл с именем wcexample1.py в одной папке с wizcoin.py : import wizcoin purse = wizcoin.WizCoin(2, 5, 99) # Целые числа передаются __init__(). ❶ print(purse) print('G:', purse.galleons, 'S:', purse.sickles, 'K:', purse.knuts) print('Total value:', purse.value()) print('Weight:', purse.weightInGrams(), 'grams') print() coinJar = wizcoin.WizCoin(13, 0, 0) # Целые числа передаются __init__(). ❷ print(coinJar) print('G:', coinJar.galleons, 'S:', coinJar.sickles, 'K:', coinJar.knuts) print('Total value:', coinJar.value()) print('Weight:', coinJar.weightInGrams(), 'grams') Вызовы WizCoin() ❶ и ❷ создают объекты WizCoin и выполняют для них код метода __init__() . В аргументах WizCoin() передаются три целых числа, которые пере- даются параметрам __init__() . Эти аргументы присваиваются атрибутам self. galleons , self.sickles и self.knuts объекта. Подобно тому как функция time. sleep() требует сначала импортировать модуль time и поставить префикс time. перед именем функции, мы также должны импортировать wizcoin и поставить префикс wizcoin. перед именем функции WizCoin() Результат выполнения программы выглядит приблизительно так: G: 2 S: 5 K: 99 Total value: 1230 Weight: 613.906 grams G: 13 S: 0 K: 0 Total value: 6409 Weight: 404.339 grams 322 Глава 15.Объектно-ориентированное программирование и классы Если вы получите сообщение об ошибке (например, ModuleNotFoundError: No module named 'wizcoin' ), убедитесь в том, что файлу присвоено имя wizcoin.py и он находится в одной папке с wcexample1.py Объекты WizCoin не имеют полезного строкового представления, поэтому при вы- воде purse и coinJar выводится адрес памяти в угловых скобках. (Из главы 17 вы узнаете, как изменить выводимое представление.) Подобно тому как для объекта строки можно вызвать метод lower() , мы можем вызвать методы value() и weightInGrams() для объектов WizCoin , присвоенных переменным purse и coinJar . Эти методы вычисляют результат по значениям атрибутов galleons , sickles и knuts объекта. Классы и ООП упрощают сопровождение кода — то есть код проще читается, изменяет- ся и расширяется в будущем. Изучим методы и атрибуты этого класса более подробно. Методы, __init__() и self Методы представляют собой функции, связанные с объектами некоторого класса. Вспомните, что lower() является методом строк — это означает, что он вызывается для объектов строк. Метод lower() можно вызвать для строки (например, 'Hello'. lower() ), но вызвать его для списка (например, ['dog', 'cat'].lower() ) не полу- чится. Также обратите внимание на то, что метод указывается после имени объекта: правильным считается код 'Hello'.lower() , а не lower('Hello') . В отличие от мето- да lower() функция len() не связана с одним конкретным типом данных; при вызове len() можно передавать строки, списки, словари и многие другие типы объектов. Как было показано в предыдущем разделе, объект создается вызовом имени клас- са как функции. Эта функция называется функцией-конструктором (или просто конструктором), потому что она конструирует новый объект. Также говорят, что конструктор создает новый экземпляр класса. При вызове конструктора Python создает новый объект, а затем выполняет метод __init__() . Наличие метода __init__() в классе не обязательно, но он почти всегда присутствует. Именно в методе __init__() обычно задаются исходные значения атрибутов. Например, я снова приведу метод __init__() класса WizCoin : def __init__(self, galleons, sickles, knuts): """Создание нового объекта WizCoin по значениям galleons, sickles и knuts.""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ВНИМАНИЕ: методы __init__() НИКОГДА не содержат команду return. Когда программа wcexample1.py вызывает WizCoin(2, 5, 99) , Python создает но- вый объект WizCoin и передает три аргумента ( 2 , 5 и 99 ) вызову __init__() . Но Создание простого класса: WizCoin 323 метод __init__() получает четыре параметра: self , galleons , sickles и knuts . Дело в том, что у каждого метода имеется первый параметр с именем self . Когда метод вызывается для объекта, этот объект автоматически передается в параметре self Остальные аргументы присваиваются параметрам как обычно. Если вы увидите со- общение об ошибке вида TypeError: __init__() takes 3 positional arguments but 4 were given (TypeError: __init__() получает 3 позиционных аргумента, но задано 4), скорее всего, вы забыли добавить параметр self в команду def метода. Присваивать первому параметру имя self необязательно; имя может быть любым. Однако имя self считается общепринятым, и выбор иного имени затруднит чтение вашего кода другими программистами Python. Когда вы читаете код, первый пара- метр self помогает быстро отличить методы от функций. Аналогичным образом, если в коде метода нигде не используется параметр self , это указывает на то, что, возможно, метод стоит оформить в виде функции. Аргументы 2 , 5 и 99 в вызове WizCoin(2, 5, 99) не присваиваются атрибутам нового объекта автоматически; чтобы это произошло, необходимо включить три команды присваивания в __init__() . Часто параметрам __init__() присваиваются имена, со- впадающие с именами атрибутов, но наличие self в self.galleons означает, что это атрибут объекта, а galleons — параметр. Сохранение аргументов конструктора в атри- бутах объекта — одна из типичных задач метода __init__() класса. Вызов datetime. date() в предыдущем разделе выполнял аналогичную операцию, хотя тогда пере- давались три аргумента для атрибутов year , month и day создаваемого объекта date Ранее мы вызвали функции int() , str() , float() и bool() для преобразования между типами данных — например, вызов str(3.1415) возвращал строковое значение '3.1415' для значения с плавающей точкой 3.1415 . Ранее в тексте они описывались как функции, но int , str , float и bool в действительности являются классами, а int() , str() , float() и bool() — конструкторами, которые возвращают новые объекты целого числа, строки, числа с плавающей точкой и логического зна- чения. Руководство по стилю Python рекомендует использовать для имен классов «верблюжью» схему с первой буквой в верхнем регистре, хотя многие встроенные классы Python этому соглашению не следуют. Вызов функции-конструктора WizCoin() возвращает новый объект WizCoin , но ме- тод __init__() не может содержать команды return с возвращаемым значением. При попытке добавить возвращаемое значение выдается ошибка TypeError: __init__() should return None (TypeError: __init__() должен возвращать None). Атрибуты Атрибутами называются переменные, связанные с объектом. В документации Python атрибут описывается как «любое имя, следующее после точки». Вспомните выражение birthday.year из предыдущего раздела: атрибут year — имя, следующее после точки. 324 Глава 15.Объектно-ориентированное программирование и классы Каждый объект содержит собственный набор атрибутов. Когда программа wcexample1.py создает два объекта WizCoin и сохраняет их в переменных purse и coinJar , их атрибуты имеют разные значения. К ним можно обращаться и при- сваивать значения, как и к любой другой переменной. Чтобы потренироваться в присваивании значений атрибутов, откройте в редакторе окно с новым файлом и введите следующий код, сохранив его в файле wcexample2.py в одной папке с файлом wizcoin.py : import wizcoin change = wizcoin.WizCoin(9, 7, 20) print(change.sickles) # Выводит 7. change.sickles += 10 print(change.sickles) # Выводит 17. pile = wizcoin.WizCoin(2, 3, 31) print(pile.sickles) # Выводит 3. pile.someNewAttribute = 'a new attr' # Создается новый атрибут. print(pile.someNewAttribute) При выполнении этой программы результат выглядит так: 7 17 3 a new attr Атрибуты объекта также можно сравнить с ключами словаря. Вы можете читать и изменять связанные с ними значения, а также присваивать новые атрибуты объ- екту. Формально методы также считаются атрибутами класса. Приватные атрибуты и приватные методы В таких языках, как C++ или Java, атрибуты могут помечаться как имеющие при- ватный уровень доступа. Это означает, что компилятор или интерпретатор позволит обращаться к атрибутам объектов этого класса только коду методов этого класса. В языке Python такого ограничения не существует. Все атрибуты и методы факти- чески имеют открытый уровень доступа: код за пределами класса может обратиться к любым атрибутам любых объектов этого класса и изменять их. Впрочем, приватный доступ полезен. Например, объекты класса BankAccount могут содержать атрибут balance , который должен быть доступен только для методов класса BankAccount . По этим причинам в Python принято начинать имена приват- ных атрибутов и методов с одного символа подчеркивания. Технически ничто не мешает коду за пределами класса обращаться к приватным атрибутам и методам, но на практике лучше обращаться к ним только из методов класса. Создание простого класса: WizCoin 325 Откройте в редакторе окно с новым файлом, введите следующий код и сохраните его с именем privateExample.py . В нем объекты класса BankAccount содержат приват- ные атрибуты _name и _balance , к которым должны обращаться напрямую только методы deposit() и withdraw() : class BankAccount: def __init__(self, accountHolder): # Методы BankAccount могут обращаться к self._balance, но код # за пределами класса этого делать не должен: self._balance = 0 ❶ self._name = accountHolder ❷ with open(self._name + 'Ledger.txt', 'w') as ledgerFile: ledgerFile.write('Balance is 0\n') def deposit(self, amount): if amount <= 0: ❸ return # Отрицательные "зачисления" недопустимы. self._balance += amount with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ❹ ledgerFile.write('Deposit ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') def withdraw(self, amount): if self._balance < amount or amount < 0: ❺ return # Не хватает средств на счете или снимается # отрицательная сумма. self._balance -= amount with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ❻ ledgerFile.write('Withdraw ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') acct = BankAccount('Alice') # Создание учетного счета. acct.deposit(120) # _balance можно изменять через deposit() acct.withdraw(40) # _balance можно изменять через withdraw() # Изменение _name или _balance за пределами BankAccount нежелательно, но возможно: acct._balance = 1000000000 ❼ acct.withdraw(1000) acct._name = 'Bob' # Теперь изменяется счет Боба! ❽ acct.withdraw(1000) # Операция регистрируется в BobLedger.txt! При выполнении программы privateExample.py создаваемые файлы содержат не- корректную информацию, потому что _balance и _name изменялись за пределами класса, что привело к недействительным состояниям. Файл AliceLedger.txt содержит непонятно откуда взявшуюся огромную сумму: Balance is 0 Deposit 120 Balance is 120 Withdraw 40 326 Глава 15.Объектно-ориентированное программирование и классы Balance is 80 Withdraw 1000 Balance is 999999000 Файл BobLedger.txt содержит необъяснимый баланс, хотя мы никогда не создавали объект BankAccount для пользователя Bob : Withdraw 1000 Balance is 999998000 Хорошо спроектированные классы в целом автономны, и они должны предоставлять методы для присваивания атрибутам допустимых значений. Атрибуты _balance и _name помечены как приватные ❶ и ❷ , а значение класса BankAccount должно изме- няться только при помощи методов deposit() и withdraw() . Эти два метода содержат проверки ❸ и ❺ , которые проверяют, что атрибут _balance не переводится в недействи- тельное состояние (например, ему не присваивается отрицательное целое значение). Методы также регистрируют каждую операцию для текущего баланса ❹ и ❻ Код за пределами класса, изменяющий эти атрибуты (например, команды acct._ balance = 1000000000 ❼ или acct._name = 'Bob' ❽ ), может перевести объект в некор- ректное состояние и создать ошибки. Соблюдение соглашений об использовании префикса _ для приватного доступа упрощает отладку. Вы точно знаете: ошибку нужно искать в коде класса, а не в коде всей программы. В отличие от Java и других языков, Python не требует определения открытых get- и set-методов для приватных атрибутов. Вместо этого в Python используются свойства (см. главу 17). |