справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Глава 7 . Классы и объектно-ориентированное программирование Классы – это механизм создания новых типов объектов. Эта глава подроб- но рассказывает о классах, но в ней не следует видеть всеохватывающее руководство по объектно-ориентированному программированию и про- ектированию. Здесь предполагается, что читатель уже имеет некото- рый опыт работы со структурами данных и владеет приемами объектно- ориентированного программирования в других языках программирова- ния, таких как C или Java. (Дополнительные сведения о терминологии и внутренней реализации объектов приводятся в главе 3 «Типы данных и объекты».) Инструкция class Класс определяет набор атрибутов, ассоциированных с ним, и используе- мых коллекцией объектов, которые называются экземплярами. Обычно класс – это коллекция функций (известных, как методы), переменных (из- вестных, как переменные класса) и вычисляемых атрибутов (известных, как свойства). Класс объявляется с помощью инструкции class. Тело класса составляет последовательность инструкций, которые выполняются на этапе определе- ния класса. Например: class Account(object): num_accounts = 0 def __init__(self,name,balance): self.name = name self.balance = balance Account.num_accounts += 1 def __del__(self): Account.num_accounts -= 1 def deposit(self,amt): self.balance = self.balance + amt Экземпляры класса 159 def withdraw(self,amt): self.balance = self.balance - amt def inquiry(self): return self.balance Значения, создаваемые при выполнении тела класса, помещаются в объ- ект класса, который играет роль пространства имен, во многом подобно модулю. Например, ниже показано, как осуществляется доступ к членам класса Account: Account.num_accounts Account.__init__ Account.__del__ Account.deposit Account.withdraw Account.inquiry Важно отметить, что сама по себе инструкция class не создает никаких эк- земпляров класса (например, в предыдущем примере не создается ника- ких объектов типа Accounts). Вместо этого выполняется подготовка атрибу- тов, общих для всех экземпляров, которые будут созданы позднее. В этом смысле определение класса можно представить как шаблон. Функции, определяемые внутри класса, называются методами экземпля- ров . Метод экземпляра – это функция, оперирующая экземпляром класса, который передается ей в первом аргументе. В соответствии с соглашения- ми этому аргументу дается имя self, хотя точно так же можно использовать любое другое имя. Функции deposit(), withdraw() и inquiry(), определяемые предыдущем примере, – это методы экземпляра. Переменные класса, такие как num_accounts, – это значения, которые со- вместно используются всеми экземплярами класса (то есть они не могут со- держать отдельные значения для каждого из экземпляров). В данном слу- чае переменная хранит количество созданных экземпляров класса Account. Экземпляры класса Экземпляры класса создаются обращением к объекту класса, как к функ- ции. В результате этого создается новый экземпляр, который затем пере- дается методу __init__() класса. В число аргументов, передаваемых методу __init__() , входят: вновь созданный экземпляр self и дополнительные ар- гументы, указанные при вызове объекта класса. Например: # Создать новый счет a = Account(“Гвидо”, 1000.00) # Вызовет Account.__init__(a,”Гвидо”,1000.00) b = Account(“Билл”, 10.00) Внутри метода __init__() создаются атрибуты экземпляра путем присваи- вания значений атрибутам объекта self. Например, инструкция self.name = name создаст атрибут name экземпляра. После того как вновь созданный экземпляр будет возвращен пользователю, к его атрибутам, как и к атри- бутам класса, можно будет обратиться с помощью оператора точки (.), как показано ниже: 160 Глава 7. Классы и объектно-ориентированное программирование a.deposit(100.00) # Вызовет Account.deposit(a,100.00) b.withdraw(50.00) # Вызовет Account.withdraw(b,50.00) name = a.name # Получить имя владельца счета Оператор точки (.) отвечает за доступ к атрибутам. При обращении к атри- буту полученное значение может поступать из нескольких мест. Например, выражение a.name в предыдущем примере вернет значение атрибута name экземпляра a. Но выражение a.deposit вернет значение атрибута deposit (метод) класса Account. При обращении к атрибуту поиск требуемого име- ни сначала производится в экземпляре, и если он не увенчается успехом, поиск выполняется в классе экземпляра. Это дает классам возможность обеспечить совместное использование своих атрибутов всеми его экземпля- рами. Правила видимости Классы определяют собственные пространства имен, но они не образуют области видимости для имен, используемых внутри методов. То есть при реализации классов ссылки на атрибуты и методы должны быть полно- стью квалифицированы. Например, ссылки на атрибуты в методах всег- да должны производиться относительно имени self. По этой причине для примера выше следует использовать ссылку self.balance, а не balance. То же относится и к вызовам методов из других методов, как показано в следую- щем примере: class Foo(object): def bar(self): print(“bar!”) def spam(self): bar(self) # Ошибка! Обращение к ‘bar’ приведет к исключению NameError self.bar() # Правильно Foo.bar(self) # Тоже правильно Отсутствие собственной области видимости в методах классов – это одна из особенностей, отличающих Python от C++ или Java. Для тех, кому раньше приходилось работать с этими языками программирования, отмечу, что аргумент self в языке Python – это то же самое, что указатель this. Необ- ходимость явного использования self обусловлена тем, что язык Python не предоставляет средств явного объявления переменных, аналогичных, на- пример, int x или float y в языке C. Без этого невозможно определить, что подразумевает операция присваивания в методе, – сохранение значения в локальной переменной или в атрибуте экземпляра. Явное использование имени self устраняет эту неоднозначность – все значения, сохраняемые с помощью self, становятся частью экземпляра, а все остальные операции присваивания действуют с локальными переменными. Наследование Наследование – это механизм создания новых классов, призванный на- строить или изменить поведение существующего класса. Оригинальный класс называют базовым классом или суперклассом. Новый класс назы- Наследование 161 вают производным классом или подклассом. Когда новый класс создается с использованием механизма наследования, он «наследует» атрибуты базо- вых классов. Однако производный класс может переопределить любой из этих атрибутов и добавить новые атрибуты. Наследование определяется перечислением в инструкции class имен ба- зовых классов через запятые. В случае отсутствия подходящего базово- го класса определяется наследование класса object, как было показано в преды дущих примерах. object – это класс, который является родоначаль- ником всех объектов в языке Python и предоставляет реализацию по умол- Python и предоставляет реализацию по умол- и предоставляет реализацию по умол- чанию некоторых общих методов, таких как __str__(), создающий строко- вое представление объекта для вывода инструкцией print. Наследование часто используется для переопределения поведения суще- ствующих методов. Например, ниже приводится специализированная вер- сия класса Account, где переопределяется метод inquiry(), который время от времени будет возвращать значение баланса, превышающее фактическое значение, – в надежде, что невнимательный клиент превысит сумму счета и будет подвергнут большому штрафу при последующем платеже по воз- никшему кредиту: import random class EvilAccount(Account): def inquiry(self): if random.randint(0,4) == 1: return self.balance * 1.10 # Внимание: патентуем идею else: return self.balance ёё c = EvilAccount(“Джордж”, 1000.00) c.deposit(10.0) # Вызов Account.deposit(c,10.0) available = c.inquiry() # Вызов EvilAccount.inquiry(c) В этом примере экземпляры класса EvilAccount будут идентичны экземпля- рам класса Account, за исключением переопределенного метода inquiry(). Поддержка механизма наследования в языке Python реализована за счет незначительного расширения оператора точки (.). А именно, если поиск атрибута в экземпляре или в классе экземпляра не увенчался успехом, он продолжается в базовом классе. Этот процесс продолжается, пока не оста- нется не просмотренных базовых классов. Это объясняет, почему вызов c.deposit() в предыдущем примере приводит к вызову метода deposit(), объ- явленного в классе Account. Подкласс может добавлять к экземплярам новые атрибуты, определяя соб- ственную версию метода __init__(). Например, следующая версия класса EvilAccount добавляет новый атрибут evilfactor: class EvilAccount(Account): def __init__(self,name,balance,evilfactor): Account.__init__(self,name,balance) # Вызов метода инициализации # базового класса Account self.evilfactor = evilfactor def inquiry(self): if random.randint(0,4) == 1: 162 Глава 7. Классы и объектно-ориентированное программирование return self.balance * self.evilfactor else: return self.balance Когда производный класс определяет собственный метод __init__(), методы __init__() базовых классов перестают вызываться автоматически. Поэтому производный класс должен следить за выполнением инициализации базо- вых классов, вызывая их методы __init__(). В предыдущем примере этот прием можно наблюдать в строке, где вызывается метод Account.__init__(). Если базовый класс не имеет своего метода __init__(), этот шаг может быть пропущен. Если заранее не известно, определяется ли в базовом классе ме- тод __init__(), для надежности можно вызвать его без аргументов, потому что в конечном итоге всегда существует реализация по умолчанию, кото- рая просто ничего не делает. Иногда в производном классе требуется переопределить метод, но при этом желательно вызвать оригинальную реализацию. В этом случае можно явно вызвать оригинальный метод базового класса, передав ему экземпляр self в первом аргументе, как показано ниже: class MoreEvilAccount(EvilAccount): def deposit(self,amount): self.withdraw(5.00) # Вычесть плату за “удобство” EvilAccount.deposit(self,amount) # А теперь пополнить счет Главная хитрость в этом примере состоит в том, что класс EvilAccount в дей- ствительности не реализует метод deposit(). Вместо него вызывается метод класса Account. И хотя этот программный код работает, у тех, кто будет чи- тать его, могут возникнуть вопросы (например, обязан ли был класс Evil- Account реализовать метод deposit()). Чтобы избежать подобной путаницы, можно использовать функцию super(), как показано ниже: class MoreEvilAccount(EvilAccount): def deposit(self,amount): self.withdraw(5.00) # Вычесть плату за удобство super(MoreEvilAccount,self).deposit(amount)# А теперь пополнить счет Функция super(cls, instance) возвращает специальный объект, позволяю- щий выполнять поиск атрибутов в базовых классах. При использовании этой функции интерпретатор будет искать атрибуты, следуя обычным пра- вилам поиска в базовых классах. Это позволяет избежать жесткой при- вязки к имени базового класса и более ясно говорит о намерениях (то есть вы готовы вызвать предыдущую реализацию метода, независимо от того, в каком базовом классе она находится). К сожалению, синтаксис функции super() оставляет желать лучшего. В Python 3 можно использовать упро- Python 3 можно использовать упро- 3 можно использовать упро- щенную инструкцию super().deposit(amount), чтобы выполнить необходи- мые вычисления, показанные в примере. Однако в Python 2 необходимо использовать более многословную версию. В языке Python поддерживается множественное наследование. Это дости- гается за счет указания нескольких базовых классов. Рассмотрим следую- щую коллекцию классов: Наследование 163 class DepositCharge(object): fee = 5.00 def deposit_fee(self): self.withdraw(self.fee) ёё class WithdrawCharge(object): fee = 2.50 def withdraw_fee(self): self.withdraw(self.fee) ёё # Класс, использующий механизм множественного наследования class MostEvilAccount(EvilAccount, DepositCharge, WithdrawCharge): def deposit(self,amt): self.deposit_fee() super(MostEvilAccount,self).deposit(amt) def withdraw(self,amt): self.withdraw_fee() super(MostEvilAcount,self).withdraw(amt) При использовании множественного наследования порядок поиска атрибу- тов становится более сложным, потому что появляется несколько возмож- ных путей поиска. Следующие инструкции иллюстрируют эту сложность: d = MostEvilAccount(“Dave”,500.00,1.10) d.deposit_fee() # Вызовет DepositCharge.deposit_fee(). fee == 5.00 d.withdraw_fee() # Вызовет WithdrawCharge.withdraw_fee(). fee == 5.00 ?? В этом примере методы deposit_fee() и withdraw_fee() имеют уникальные имена и обнаруживаются в соответствующих базовых классах. Однако соз- дается ощущение, что метод withdraw_fee() работает с ошибкой, потому что он не использует значение атрибута fee, инициализированного в его классе. Это обусловлено тем, что атрибут fee – это переменная класса, объявленная в двух различных базовых классах. В работе используется одно из этих зна- чений, но какое? (Подсказка: это значение атрибута DepositCharge.fee.) Чтобы обеспечить поиск атрибутов при множественном наследовании, все базовые классы включаются в список, в порядке от «более специали- зированных» к «менее специализированным». Затем, когда производится поиск, интерпретатор просматривает этот список, пока не найдет первое определение атрибута. В примере выше класс EvilAccount является более специализированным, чем класс Account, потому что он наследует класс Account. То же относит- ся и к классу MostEvilAccount, DepositCharge считается более специализиро- ванным, чем WithdrawCharge, потому что он стоит первым в списке базовых классов. Порядок поиска в базовых классах можно увидеть, если вывести содержимое атрибута __mro__ класса. Например: >>> MostEvilAccount.__mro__ ( 164 Глава 7. Классы и объектно-ориентированное программирование >>> В большинстве случаев правила составления этого списка «интуитивно понятны». То есть производный класс всегда проверяется раньше его ба- зовых классов, а если класс имеет несколько родителей, они всегда будут проверяться в том порядке, в каком были перечислены в объявлении клас- са. Однако точный порядок просмотра базовых классов в действительности намного сложнее и его нельзя описать с помощью какого-либо «простого» алгоритма, такого как поиск «снизу-вверх» или «слева-направо». Для упо- рядочения используется алгоритм C3-линеаризации, описанный в доку- менте «A Monotonic Superclass Linearization for Dylan» (Монотонная ли- A Monotonic Superclass Linearization for Dylan» (Монотонная ли- Monotonic Superclass Linearization for Dylan» (Монотонная ли- Monotonic Superclass Linearization for Dylan» (Монотонная ли- Superclass Linearization for Dylan» (Монотонная ли- Superclass Linearization for Dylan» (Монотонная ли- Linearization for Dylan» (Монотонная ли- Linearization for Dylan» (Монотонная ли- for Dylan» (Монотонная ли- for Dylan» (Монотонная ли- Dylan» (Монотонная ли- Dylan» (Монотонная ли- » (Монотонная ли- неаризация суперкласса для языка Dylan) (авторы К. Баррет (K. Barrett) и другие, был представлен на конференции OOPSLA’96). Одна из малоза- OOPSLA’96). Одна из малоза- ’96). Одна из малоза- метных особенностей этого алгоритма состоит в том, что его реализация в языке Python препятствует созданию определенных иерархий классов с возбуждением исключения TypeError. Например: class X(object): pass class Y(X): pass class Z(X,Y): pass # TypeError. # Невозможно определить непротиворечивый порядок # разрешения имен методов В данном случае алгоритм разрешения имен методов препятствует созда- нию Z, потому что он не может определить осмысленный порядок поиска в базовых классах. Например, класс X находится в списке родительских классов перед классом Y, поэтому он должен просматриваться первым. Од- нако класс Y является более специализированным, потому что наследует класс X. Поэтому если первым будет проверяться класс X, это не позволит отыскать специализированные реализации методов в классе Y, унаследо- ванных от класса X. На практике такие ситуации должны возникать очень редко, и если возникают, это обычно свидетельствует о более серьезных ошибках, допущенных при проектировании программы. Как правило, в большинстве программ лучше стараться избегать мно- жественного наследования. Однако иногда множественное наследование используется для объявления так называемых классов-примесей. Класс- примесь, обычно определяющий набор методов, объявляется, чтобы потом «подмешивать» его в другие классы, с целью расширения их функциональ- ных возможностей (почти как макроопределение). Обычно предполагает- ся, что существуют другие методы, и методы в классах-примесях встраи- ваются поверх них. Эту возможность иллюстрируют классы DepositCharge и WithdrawCharge, объявленные в предыдущем примере. Эти классы добав- ляют новые методы, такие как deposit_fee(), в классы, включающие их в число базовых классов. Однако вам едва ли потребовалось бы, например, создавать экземпляры самого класса DepositCharge. В действительности эк- земпляр этого класса едва ли мог бы иметь практическую пользу (он об- ладает всего одним методом, который сам по себе не может даже работать правильно). Полиморфизм, или динамическое связывание и динамическая типизация 165 И еще одно последнее замечание: если в этом примере потребуется испра- вить проблему со ссылкой на атрибут fee, методы deposit_fee() и withdraw_ fee() можно изменить так, чтобы они обращались к атрибуту напрямую, используя имя класса вместо ссылки self (например, DepositChange.fee). |