справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Полиморфизм, или динамическое связывание и динамическая типизация Динамическое связывание (иногда, в контексте наследования, называется полиморфизм ) – это возможность использования экземпляра без учета его фактического типа. Данная возможность целиком обеспечивается меха- низмом поиска атрибутов в дереве наследования, описанным в предыду- щем разделе. Всякий раз, когда производится обращение к атрибуту, та- кое как obj.attr, поиск атрибута attr сначала выполняется в самом экзем- пляре, затем в определении класса экземпляра, а затем в базовых классах. Поиск прекращается, как только будет найден первый атрибут с требуе- мым именем. Важной особенностью процесса связывания является его независимость от того, какому типу принадлежит объект obj. То есть при обращении к атри- буту obj.name этот механизм будет работать с любым объектом obj, имею- щим атрибут name. Такое поведение иногда называют динамической типи- зацией (или утиной типизацией исходя из пословицы: «если это выглядит, крякает и ходит, как утка, значит, это утка»). Часто программисты пишут программы на языке Python, исходя из этого поведения. Например, когда необходимо создать модифицированную вер- сию существующего класса, на его основе можно либо создать произво- дный класс либо определить совершенно новый класс, который выглядит и действует как прежний, но никак с ним не связанный. Последний подход часто используется для ослабления связей между компонентами програм- мы. Например, можно написать программный код, который будет работать с объектами любых типов, при условии, что они обладают определенным набором методов. В качестве одного из наиболее типичных примеров можно привести различные объекты, напоминающие файлы, которые определя- ются в стандартной библиотеке. Хотя эти объекты своим поведением напо- минают файлы, тем не менее они не наследуют встроенный объект файла. Статические методы и методы классов По умолчанию предполагается, что все функции, присутствующие в опре- делении класса, будут оперировать экземпляром, который всегда передает- ся в виде первого аргумента self. Однако существуют еще два типа методов, которые можно определить. Статический метод – это обычная функция, которая просто включает- ся в пространство имен, определяемое классом. Она не оперирует какими- либо экземплярами. Для определения статических методов используется декоратор @staticmethod, как показано ниже: 166 Глава 7. Классы и объектно-ориентированное программирование class Foo(object): @staticmethod def add(x,y): return x + y Чтобы вызвать статический метод, достаточно просто добавить имя класса перед ним. При этом не требуется передавать ему какую-либо дополнитель- ную информацию. Например: x = Foo.add(3,4) # x = 7 Обычно статические методы используются для обеспечения различных способов создания новых экземпляров. В объявлении класса может быть только один метод __init__(), однако имеется возможность объявить аль- тернативные функции создания экземпляров, как показано ниже: class Date(object): def __init__(self,year,month,day): self.year = year self.month = month self.day = day @staticmethod def now(): t = time.localtime() return Date(t.tm_year, t.tm_mon, t.tm_day) @staticmethod def tomorrow(): t = time.localtime(time.time()+86400) return Date(t.tm_year, t.tm_mon, t.tm_day) ёё # Несколько примеров создания экземпляров a = Date(1967, 4, 9) b = Date.now() # Вызовет статический метод now() c = Date.tomorrow() # Вызовет статический метод tomorrow() Методы класса – это методы, которые оперируют самим классом как объ- ектом. Определяются они с помощью декоратора @classmethod. Метод клас- са отличается от метода экземпляра тем, что в первом аргументе, который в соответствии с соглашениями называется cls, ему передается класс. На- пример: class Times(object): factor = 1 @classmethod def mul(cls,x): return cls.factor*x ёё class TwoTimes(Times): factor = 2 ёё x = TwoTimes.mul(4) # Вызовет Times.mul(TwoTimes, 4) -> 8 Обратите внимание, что в данном примере класс TwoTimes передается мето- ду mul() как объект. Этот пример во многом искусственный, тем не менее существуют практические, и весьма тонкие, применения методов классов. Свойства 167 Например, предположим, что объявляется класс, наследующий класс Date, показанный выше, и немного модифицирующий его: class EuroDate(Date): # Изменена строка преобразования, чтобы обеспечить возможность # представления дат в европейском формате def __str__(self): return “%02d/%02d/%4d” % (self.day, self.month, self.year) Поскольку этот класс наследует класс Date, он обладает всеми его особенно- стями. Однако поведение методов now() и tomorrow() будет немного портить об- щую картину. Например, если вызвать метод EuroDate.now(), вместо объекта EuroDate он вернет объект Date. Это можно исправить, изменив метод класса: class Date(object): ... @classmethod def now(cls): t = time.localtime() # Создать объект соответствующего типа return cls(t.tm_year, t.tm_month, t.tm_day) ёё class EuroDate(Date): ... ёё a = Date.now() # Вызовет Date.now(Date) и вернет Date b = EuroDate.now() # Вызовет Date.now(EuroDate) и вернет EuroDate Одна из особенностей статических методов и методов класса состоит в том, что эти методы располагаются в том же пространстве имен, что и методы экземпляра. Вследствие этого они могут вызываться относительно экзем- пляра. Например: a = Date(1967,4,9) b = a.now() # Вызовет Date.now(Date) Это может быть источником недопонимания, потому что вызов a.now() в действительности никак не воздействует на экземпляр a. Такое поведение является одной из особенностей объектной системы языка Python, которые отличают его от других объектно-ориентированных языков программиро- вания, таких как Smalltalk и Ruby. В этих языках методы класса отделены от методов экземпляра. Свойства Обычно при обращении к атрибуту экземпляра или класса возвращается значение, сохраненное в этом атрибуте ранее. Свойство – это особая раз- новидность атрибута, который вычисляет свое значение при попытке об- ращения к нему. Ниже приводится простой пример: class Circle(object): def __init__(self,radius): self.radius = radius # Некоторые дополнительные свойства класса Circles 168 Глава 7. Классы и объектно-ориентированное программирование @property def area(self): return math.pi*self.radius**2 @property def perimeter(self): return 2*math.pi*self.radius Б лагодаря этому получившийся объект Circle обрел следующие особен- ности: >>> c = Circle(4.0) >>> c.radius 4.0 >>> c.area 50.26548245743669 >>> c.perimeter 25.132741228718345 >>> c.area = 2 Traceback (most recent call last): File “ AttributeError: can’t set attribute (Перевод: Трассировочная информация (самый последний вызов – самый нижний): Файл “ AttributeError: невозможно установить значение атрибута ) >>> В этом примере экземпляры класса Circle обладают переменной экземпля- ра c.radius, где хранится значение, и атрибутами c.area и c.perimeter, значе- ния которых вычисляются исходя из значения этой переменной. Декора- тор @property обеспечивает возможность обращения к методу, следующему за ним, как к простому атрибуту, без круглых скобок (), которые обычно добавляются, чтобы вызвать метод. Объект не имеет никаких отличитель- ных признаков, которые говорили бы о том, что значение атрибута вычис- ляется, – кроме вывода сообщения об ошибке, которое генерируется при попытке переопределить значение атрибута (о чем свидетельствует исклю- чение AttributeError в примере выше). Такой способ использования свойств имеет прямое отношение к реали- зации принципа единообразного доступа (Uniform Access Principle). Суть состоит в том, что когда объявляется класс, хорошо бы обеспечить мак- симальное единообразие доступа к нему. Без применения свойств доступ к одним атрибутам выглядел бы, как обращение к обычным атрибутам, например c.radius, а к другим – как к методам, например c.area(). Необ- ходимость запоминать, когда следует добавлять круглые скобки (), а ког- да – нет, лишь вносит лишнюю путаницу. Свойства помогают избавиться от этих неприятностей. Программисты на языке Python не всегда понимают, что сами методы не- явно интерпретируются, как свойства. Рассмотрим следующий класс: class Foo(object): def __init__(self,name): Свойства 169 self.name = name def spam(self,x): print(“%s, %s” % (self.name, x) Когда пользователь создаст экземпляр этого класса, например: f = Foo (“Гвидо”) , и попробует обратиться к атрибуту f.spam, он получит не объект функции spam, а то, что называется связанным методом, то есть объект, представляющий вызов метода, который будет выполнен при добавлении к нему оператора вызова (). Связанный метод напоминает частично под- готовленную функцию, для которой аргумент self уже имеет некоторое значение, но которой еще необходимо передать дополнительные аргумен- ты при вызове с помощью оператора (). Создание этого связанного метода производится функцией свойства, которая вызывается за кулисами. Когда с помощью декораторов @staticmethod и @classmethod создается статический метод или метод класса, фактически выбирается другая функция свой- ства, которая обеспечит немного иной способ обращения к этим методам. Например, декоратор @staticmethod просто возвращает функцию метода в том же виде, в каком получил ее, ничего не добавляя и не изменяя. Свойства также могут перехватывать операции по изменению и удалению атрибута. Делается это посредством присоединения к свойству специаль- ных методов изменения и удаления. Например: class Foo(object): def __init__(self,name): self.__name = name @property def name(self): return self.__name @name.setter def name(self,value): if not isinstance(value,str): raise TypeError(“Имя должно быть строкой!”) self.__name = value @name.deleter def name(self): raise TypeError(“Невозможно удалить атрибут name”) ёё f = Foo(“Гвидо”) n = f.name # вызовет f.name() – вернет функцию f.name = “Монти” # вызовет метод изменения name(f,”Монти”) f.name = 45 # вызовет метод изменения name(f,45) -> TypeError del f.name # вызовет метод удаления name(f) -> TypeError Сначала в этом примере с помощью декоратора @property и ассоциирован- ного с ним метода объявляется атрибут name как свойство, доступное толь- ко для чтения. Следующие ниже декораторы @name.setter и @name.deleter связывают дополнительные методы с операциями изменения и удаления атрибута name. Имена этих методов должны в точности совпадать с именем оригинального свойства. Обратите внимание, что в этих методах фактиче- ское значение свойства name сохраняется в атрибуте __name. Имя этого атри- бута не должно следовать каким-либо соглашениям, но оно должно отли- чаться от имени свойства, чтобы избежать неоднозначности. 170 Глава 7. Классы и объектно-ориентированное программирование В старом программном коде часто можно встретить определения свойств, выполненные с помощью функции property(getf=None, setf=None, delf=None, doc=None) , которой передаются методы с уникальными именами, реализую- щие необходимые операции. Например: class Foo(object): def getname(self): return self.__name def setname(self,value): if not isinstance(value,str): raise TypeError(“Имя должно быть строкой!”) self.__name = value def delname(self): raise TypeError(“Невозможно удалить атрибут name”) name = property(getname,setname,delname) Этот устаревший подход по-прежнему поддерживается, но использование декораторов позволяет получать более удобные определения классов. На- пример, при использовании декораторов функции get, set и delete не будут видны как методы. Дескрипторы При использовании свойств доступ к атрибутам управляется серией поль- зовательских функций get, set и delete. Такой способ управления атри- бутами может быть обобщен еще больше, за счет использования объекта дескриптора . Дескриптор – это обычный объект, представляющий значе- – это обычный объект, представляющий значе- это обычный объект, представляющий значе- обычный объект, представляющий значе- обычный объект, представляющий значе- объект, представляющий значе- объект, представляющий значе- , представляющий значе- представляющий значе- значе- значе- ние атрибута. За счет реализации одного или более специальных методов __get__() , __set__() и __delete__() он может подменять механизмы доступа к атрибутам и влиять на выполнение этих операций. Например: class TypedProperty(object): def __init__(self,name,type,default=None): self.name = “_” + name self.type = type self.default = default if default else type() def __get__(self,instance,cls): return getattr(instance,self.name,self.default) def __set__(self,instance,value): if not isinstance(value,self.type): raise TypeError(“Значение должно быть типа %s” % self.type) setattr(instance,self.name,value) def __delete__(self,instance): raise AttributeError(“Невозможно удалить атрибут”) ёё class Foo(object): name = TypedProperty(“name”,str) num = TypedProperty(“num”,int,42) В этом примере класс TypedProperty определяет дескриптор, выполняющий проверку типа при присваивании значения атрибуту и возбуждающий ис- ключение при попытке удалить атрибут. Например: Инкапсуляция данных и частные атрибуты 171 f = Foo() a = f.name # Неявно вызовет Foo.name.__get__(f,Foo) f.name = “Гвидо” # Вызовет Foo.name.__set__(f,”Guido”) del f.name # Вызовет Foo.name.__delete__(f) Э кземпляры дескрипторов могут создаваться только на уровне класса. Нельзя создавать объекты дескрипторов для каждого экземпляра в от- дельности, внутри метода __init__() или в других методах. Кроме того, имя атрибута, используемое для сохранения дескриптора в классе, имеет более высокий приоритет перед другими атрибутами на уровне экземпляров. Именно поэтому в предыдущем примере объекту дескриптора передается параметр name с именем, и именно поэтому полученное имя изменяется за счет добавления ведущего символа подчеркивания. Чтобы дескриптор мог сохранять значение атрибута в экземпляре, имя этого атрибута должно от- личаться от имени, используемого самим дескриптором. Инкапсуляция данных и частные атрибуты По умолчанию все атрибуты и методы класса являются общедоступными. Это означает, что все они доступны без каких-либо ограничений. Это также означает, что все атрибуты и методы базового класса будут унаследованы и доступны в производных классах. В объектно-ориентированном про- граммировании эта особенность часто бывает нежелательной, потому что позволяет легко получить информацию о внутреннем устройстве объекта и может привести к конфликту имен между объектами, созданными на основе базового и производного классов. Чтобы исправить эту проблему, все имена в определении класса, начина- ющиеся с двух символов подчеркивания, такие как __Foo, автоматически изменяются и обретают вид _Classname__Foo. Благодаря этому обеспечива- ется эффективный способ создания частных атрибутов и методов класса, потому что частные имена в порожденном классе не будут конфликтовать с такими же частными именами в базовом классе. Например: class A(object): def __init__(self): self.__X = 3 # Будет изменено на self._A__X def __spam(self): # Будет изменено на _A__spam() pass def bar(self): self.__spam() # Вызовет только метод A.__spam() ёё class B(A): def __init__(self): A.__init__(self) self.__X = 37 # Будет изменено на self._B__X def __spam(self): # Будет изменено на _B__spam() pass Хотя такая схема именования создает иллюзию сокрытия данных, на са- мом деле не существует механизма, который действительно препятствовал бы попыткам обратиться к «частным» атрибутам класса. Так, если имя 172 Глава 7. Классы и объектно-ориентированное программирование класса и имя частного атрибута известно заранее, к нему можно обратить- ся, использовав измененное имя. Класс может сделать такие атрибуты ме- нее заметными, переопределив метод __dir__(), который формирует список имен, возвращаемый функцией dir(), используемой для исследования объ- ектов. На первый взгляд, такое изменение имен выглядит, как дополнительная операция, на выполнение которой тратятся вычислительные ресурсы, но в реальности она выполняется один раз, на этапе определения класса. Она не производится в процессе выполнения методов и не влечет за собой сни- жение производительности программы. Кроме того, следует отметить, что подмена имен не происходит в таких функциях, как getattr(), hasattr(), setattr() или delattr(), где имена атрибутов передаются в виде строк. При работе с этими функциями для доступа к атрибутам необходимо явно ука- зывать измененные имена атрибутов в виде _Classname__name. Рекомендуется определять частные атрибуты с изменяемыми значениями через свойства. Тем самым вы подтолкнете пользователя к использованию имени свойства, а не самого объекта данных (что, вероятно, желательно для вас, иначе вы не стали бы оборачивать данные в свойство). Пример та- та- та- кого подхода приводится в следующем разделе. Создание частных методов обеспечивает для суперкласса возможность предотвратить переопределение и изменение реализаций этих методов в порожденных классах. Например, метод A.bar(), в примере выше, будет вызывать только метод A.__spam(), независимо от типа аргумента self или от наличия другого метода __spam() в производном классе. Наконец, не следует путать правила именования частных атрибутов клас- са с правилами именования «частных» определений в модуле. Типичная ошибка состоит в попытке определить частный атрибут класса с един- ственным символом подчеркивания в начале (например, _name). В модулях это соглашение об именовании позволяет препятствовать экспортирова- нию имен с помощью инструкции import *. Однако в классах этот прием не приводит к сокрытию атрибутов и не предотвращает конфликты имен, которые могут возникнуть, если в производном классе будет предпринята попытка определить новый атрибут или метод с тем же именем. |