Главная страница
Навигация по странице:

  • Статические методы и методы классов

  • Инкапсуляция данных и частные атрибуты

  • справочник по Python. мм isbn 9785932861578 9 785932 861578


    Скачать 4.21 Mb.
    Названиемм isbn 9785932861578 9 785932 861578
    Анкорсправочник по Python
    Дата08.05.2022
    Размер4.21 Mb.
    Формат файлаpdf
    Имя файлаBizli_Python-Podrobnyy-spravochnik.440222.pdf
    ТипСправочник
    #518195
    страница14 из 82
    1   ...   10   11   12   13   14   15   16   17   ...   82
    Полиморфизм, или динамическое связывание
    и динамическая типизация
    Динамическое связывание
    (иногда, в контексте наследования, называется
    полиморфизм
    ) – это возможность использования экземпляра без учета его фактического типа. Данная возможность целиком обеспечивается меха- низмом поиска атрибутов в дереве наследования, описанным в предыду- щем разделе. Всякий раз, когда производится обращение к атрибуту, та- кое как 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 “”, line 1, in
    AttributeError: can’t set attribute
    (Перевод:
    Трассировочная информация (самый последний вызов – самый нижний):
    Файл “”, строка 1, в
    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 *. Однако в классах этот прием не приводит к сокрытию атрибутов и не предотвращает конфликты имен, которые могут возникнуть, если в производном классе будет предпринята попытка определить новый атрибут или метод с тем же именем.
    1   ...   10   11   12   13   14   15   16   17   ...   82


    написать администратору сайта