Объектно-ориентированное программирование на Питоне

Материал из свободной русской энциклопедии «Традиция»
Перейти к: навигация, поиск

С самого начала Питон проектировался как объектно-ориентированный язык программирования [1]. На Питоне писать программы в объектно-ориентированном стиле просто и приятно.

Введение[править]

Принципы ООП[править]

Согласно Алану Кэю - автору языка программирования Smalltalk - объектно-ориентированным может называться язык, построенный с учетом следующих принципов[2]:

  • Все данные представляются объектами
  • Программа является набором взаимодействующих объектов, посылающих друг другу сообщения
  • Каждый объект имеет собственную часть памяти и может иметь в составе другие объекты
  • Каждый объект имеет тип
  • Объекты одного типа могут принимать одни и те же сообщения (и выполнять одни и те же действия)

Как будет показано в данной статье, язык программирования Питон соответствует этим принципам.

Основные концепции ООП в Питон[править]

Объекты, типы и классы[править]

Определение класса[править]

Для ясности последующиего изложения рассмотрим определение класса с точки зрения синтаксиса. Для определения класса используется оператор class:

   class имя_класса(надкласс1, надкласс2, ...):
       # определения атрибутов и методов класса

У класса могут быть базовые (родительские) классы (надклассы), которые (если они есть) указываются в скобках после имени определяемого класса.

Минимально возможное определение класса выглядит так:

   class A:
       pass

В терминологии Питона члены класса называются атрибутами, функции класса - методами, а поля класса - свойствами (или просто атрибутами).

Определения методов аналогичны определениям функций, но (за некоторыми исключениями, о которых ниже) методы всегда имеют первый аргумент, называемый по широко принятому соглашению self:

   class A:
       def m1(self, x):
           # блок кода метода

Определения атрибутов - обычные операторы присваивания, которые ссвязывают некоторые значения с именами атрибутов.

   class A:
       attr1 = 2 * 2

В Питон класс не является чем-то статическим после определения, поэтому добавить атрибуты можно и после:

   class A:
       pass
    
   def myMethod(self, x):
       return x * x
    
   A.m1 = myMethod
   A.attr1 = 2 * 2

Инстанциирование класса[править]

Для инстанциирования класса, то есть, создания экземпляра класса, достаточно вызвать класс по имени и задать параметры конструктора:

   class Point:
        def __init__(self, x, y, z):
            self.coord = (x, y, z)
        def __repr__(self):
            return "Point(%s, %s, %s)" % self.coord
   >>> p = Point(0.0, 1.0, 0.0)
   >>> p
   Point(0.0, 1.0, 0.0)

Переопределив классовый метод __new__, можно контролировать процесс создания экземпляра класса. Этот метод вызывается до метода __init__ и должен вернуть новый экземпляр либо None (в последнем случае будет вызван __new__ родительского класса). Метод __new__ используется для управлением создания неизменчивых (immutable) объектов, контроля создания объектов в случаях, когда __init__ не вызывается, например, при десериализации (unpickle). Следующий код демонстрирует один из вариантов реализации паттерна синглтон:

   >>> class Singleton(object):
           obj = None                           # Атрибут для хранения единственного экземпляра
           def __new__(cls,*dt,**mp):           # класса Singleton.
              if cls.obj is None:               # Если он еще не создан, то
                 cls.obj = object.__new__(cls,*dt,**mp) # вызовем __new__ родительского класса
              return cls.obj                    # вернем синглтон
   ...
   >>> obj = Singleton()
   >>> obj.attr = 12
   >>> new_obj = Singleton()
   >>> new_obj.attr                       
   12
   >>> new_obj is obj                     # new_obj и obj - это один и тот же объект
   True

Конструктор и деструктор[править]

Специальные методы вызываются при создании экземпляра класса (конструктор) и при удалении класса (деструктор). Питон имеет автоматическое управление памятью, поэтому деструктор нужен достаточно редко, для ресурсов, требующих явного освобождения.

Следующий класс имеет конструктор и деструктор:

   class Line:
       def __init__(self, p1, p2):
           self.line = (p1, p2)
       def __del__(self):
           print "Удаляется линия %s - %s" % self.line
   >>> l = Line((0.0, 1.0), (0.0, 2.0))
   >>> del l
   Удаляется линия (0.0, 1.0) - (0.0, 2.0)
   >>>

Следует заметить, что в момент вызова деструктора (например, по завершении программы) среда исполнения может быть уже достаточно "истощенной", поэтому в деструкторе следует делать только самое необходимое. Кроме того, необработанные в деструкторе исключения игнорируются.

Время жизни объекта[править]

Инкапсуляция и доступ к свойствам[править]

Инкапсуляция является одним из ключевых понятий ООП. В языке Питон сокрытие информации о внутреннем устройстве объекта выполняется на уровне соглашения между программистами о том, какие атрибуты относятся к общедоступному интерфейсу класса, а какие относятся к внутренней реализации. Одиночное подчеркивание в начале имени атрибута говорит о том, что метод не предназначен для использования вне методов класса (или вне функций и классов модуля), однако, атрибут все-таки доступен по этому имени. Два подчеркивания в начале имени дают несколько большую защиту: атрибут перестает быть доступен по этому имени. Последнее используется достаточно редко. Есть существенное отличие между такими атрибутами и личными (private) членами класса в таких языках как C++ или Java: атрибут остается доступным, но под именем вида _ИмяКласса__ИмяАтрибута, а при каждом обращении Python будет модифицировать имя в зависимости от того, через экземпляр какого класса происходит обращение к атрибуту. Таким образом, родительский и дочерний классы могут иметь атрибут с именем, например, "__f", но не будут мешать друг другу.

>>> class parent(object):
      def __init__(self):
          self.__f = 2
      def get(self):return self.__f
....
>>> class child(parent):
      def __init__(self):
        self.__f = 1
        parent.__init__(self)
      def cget(self):return self.__f
....
>>> c = child()
>>> c.get()
2
>>> c.cget()
1
>>> c.__dict__
{'_child__f': 1, '_parent__f': 2} #на самом деле у объекта "с" два разных атрибута

Особым случаем является наличие двух подчеркиваний в начале и в конце имени - такие атрибуты используются для специальных свойств и функций класса(например для перегрузки операции). Такие атрибуты доступны по своему имени, но их использование зарезервировано для специальных атрибутов, изменяющих поведение объекта.

Доступ к атрибуту может быть как прямой:

 class A(object):
     def __init__(self, x):          # атрибут получает значение в конструкторе
         self.x = x
    
 a = A(5)
 print a.x
 a.x = 6

Так и с использованием свойств с заданными методами для получения, установки и удаления атрибута:

 class A(object):
     def __init__(self, x):
         self._x = x
     def getx(self):                 # метод для получения значения
         return self._x
     def setx(self, value):          # присваивания нового значения
         self._x = value
     def delx(self):                 # удаления атрибута
         del self._x                 
     x = property(getx, setx, delx, "Свойство x")    # определяем x как свойство
    
 a = A(5)      
 print a.x      # Синтаксис доступа к атрибуту при это прежний
 a.x = 6

Разумеется, первый способ хорошо только если значение атрибута является атомарной операцией по изменению состояния объекта. Если же это не так, то второй способ позволит выполнить все необходимые действия в соответствующих методах.

Есть два способа централизованно контролировать доступ к атрибутам. Первый основан на перегрузке методов __getattr__(), __setattr__(), __delattr__(), а второй - метода __getattribute__() . Второй метод позволяет управление чтением уже существующих атрибутов. Эти способы позволяют организовать полностью динамический доступ к атрибутам объекта или, что используется очень часто, имитации несуществующих атрибутов. По такому принципу функционируют, например, все системы RPC для Python, имитируя методы и свойства, реально существующие на удаленном сервере.

Полиморфизм[править]

В компилируемых языках программирования полиморфизм достигается за счет создания виртуальных методов, которые в отличие от невиртуальных можно перегрузить в потомке. В Питоне все методы являются виртуальными. Это является естественным следствием разрешения доступа на этапе исполнения (следует отметить, что создание невиртуальных методов в компилируемых языках связанно с меньшими накладными расходами на их поддержку и вызов).

>>> class parent(object):
        def isParOrPChild(self) : return True
        def who(self) : return 'parent'
>>> class child(parent):
        def who(self,val):return 'child'
>>> x = parent()
>>> x.meth(),x.isParOrPChild()
('parent',True)
>>> x = child()
>>> x.meth(),x.isParOrPChild()
('child',True)

Явно указав имя класса, можно обратиться к методу родителя (как впрочем и любого другого объекта).

>>> class child(parent):
        def __init__(self):
            parent.__init__(self)

Для общего случая получения класса-предка используется функция super.

class Child(Parent):
    def __init__(self):
        super(Child,self).__init__(self)

Используя исключения, можно имитировать чисто виртуальные методы:

>>> class Abstract(Exception):pass
>>> class abstobj(object):
        def abstmeth(self):
            raise Abstract('Method abstobj.abstmeth is pure virtual')
>>> abstobj().abstmeth()
 Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 2, in method
 __main__.Abstract: Method abstobj.abstmeth is pure virtual

Или лучше так:

>>> def abstract(func):
        def closure(*dt,**mp):
            raise Abstract("Method %s is pure virtual" % func.__name__)
        return closure
>>> class abstobj(object):
        @abstract
        def abstmeth(self):pass

Изменяя атрибут __class__, можно перемещать объект вверх или вниз по иерархии наследования (впрочем как и к любому другому типу)

>>> c = child()
>>> c.val = 10
>>> c.who()
'child'
>>> c.__class__ = parent
>>> c.who()
'parent'
>>> c.val
10

Однако в этом случае никакие преобразования типов не делаются, поэтому согласованность данных всецело лежит на программисте.

Более того, полиморфизм в Питоне вообще не связан с наследованием, поэтому его можно считать сигнатурно-ориентированным полиморфизмом (signature-oriented polymorfism). Например, чтобы экземпляру класса "прикинуться" файловым объектом, ему достаточно реализовать методы, относящиеся к файлам (обычно .read(), .readlines(), .close() и т.п.).

Имитация встроенных типов[править]

Встроенные типы и их методы имеют синтаксическую поддержку в языке Питон или другие особые "привилегии". Конечно, любая операция может быть представлена синтаксисом вызова функции, однако, для частого применения это неудобно.

Воспользоваться точно такой же синтаксической поддержкой может и любой определенный пользователем класс. Для этого нужно лишь реализовать методы со специальными именами. Самый простой пример - имитировать функцию:

>>> class Add:
...      def __call__(self, x, y):    # определение метода, 
...          return x + y             # который отвечает за операцию вызова функции
...
>>> add = Add()
>>> add(3, 4)                         # это эквивалентно add.__call__(3, 4)
7

Аналогично поддаются имитации все операции встроенных типов. Еще один пример связан с вычислением длины объекта с помощью функции len(). Оказывается, эта встроенная функция вызывает специальным метод:

>>> class wrongList(list):     # определяем собственный класс для списка
...     def __len__(self):     # который всегда считает, что имеет нулевую длину
...         return 0
...
>>> w = wrongList([1,2,3])
>>> len(w)                     # это эквивалентно w.__len__()
0

Методы __getitem__,__setitem__,__delitem__,__contains__ позволяют создать интерфейс для словаря или списка(dict). Достаточно просто имитировать и числовые типы. Скажем, следующий класс использует инфиксную операцию *:

class Multiplyable:
    def __init__(self, value): self.value = value
    def __mul__(self, y):      return self.value * y
    def __rmul__(self, x):     return x * self.value
    def __imul__(self, y):     return Multiplyable(self.value * y)
    def __str__(self):         return "Multiplyable(%s)" % self.value
>>> m = Multiplyable(1)
>>> print m
Multiplyable(1)
>>> m *= 3
>>> print m
Multiplyable(3)

Последний из методов - .__str__() - отвечает за представление экземпляра класса при печати оператором print и в других подобных случаях.

Аналогичные методы имеются и у соответствующих встроенных типов

>>> int.__add__
<slot wrapper '__add__' of 'int' objects>
>>> [].__getitem__
<built-in method __getitem__ of list object at 0x00DA3D28>
>>> class a(object):pass
>>> a.__call__
<method-wrapper '__call__' of type object at 0x00DDC318>

Не все из них существуют на самом деле, большая часть имитируется интерпретатором Python для удобства программиста. Такое поведение позволяет экономить время при наиболее важных операциях, например сложение целых не приводит к поиску и вызову метода __add__ у класса int и память, но приводит к невозможности изменения методов у встроенных классов, что является причиной критики Python со стороны программистов Ruby.

Отношения между классами[править]

Наследование и множественное наследование[править]

Python поддерживает наиболее полный вариант наследования - множественное, позволяющее классу наследовать любое количество базовых.

>>> class Par1(object):                # наследуем один базовый класс - object
        def name1(self):return 'Par1'
>>> class Par2(object):
        def name2(self):return 'Par2'
>>> class Child(Par1,Par2):            # создадим класс, наследующий Par1,Par2(и, косвенно, object)
        pass
>>> x = Child()
>>> x.name1(),x.name2()                # экземпляру Child доступны методы и Par1 и Par2
'Par1','Par2'

В большинстве языков программирования наследование исполняет две основные функции:

  1. Расширение функциональности класса за счет добавления новых атрибутов/методов
  2. Гарантирование предоставления объектом определенной функциональности

Для Python в следствии "утиной типизации" второй пункт имеет ограниченное применение - он гарантирует наличие интерфейсов, но не наоборот (отсутствие наследования не гарантирует их отсутствия).

Порядок разрешения доступа к методам и полям[править]

За достаточно простым в использовании механизмом доступа к атрибутам в Питон кроется довольно сложный алгоритм. Далее будет приведена последовательность действий для определения производимая интерпретатором притона при разрешении запроса object.field ( если какой-либо шаг завершается успешно то поиск прекращается, иначе происходит переход к следующему шагу).

  1. Если у object есть метод __getattribute__, то будет вызван он с параметром 'field' (либо __setattr__ или __delattr__ в зависимости от действия над атрибутом)
  2. Если у object есть поле __dict__, то ищется object.__dict__['field']
  3. Если у object.__class__ есть поле __slots__, то 'field' ищется в object.__class__.__slots__
  4. Проверяется object.__class__.__dict__['fields']
  5. Производится рекурсивный поиск по __dict__ всех родительских классов (при множественном наследовании поиск производится в режиме deep-first, в том порядке как базовые классы перечислены в определении класса-потомка). Алгоритм поиска разный для "классических" и "новых" классов.
  6. Если у object есть метод __getattr__, то вызывается он с параметром 'field'
  7. Возбуждается исключение AttributeError .

Если поиск окончен успешно, то проверяется, является ли атрибут классом "нового стиля". Если является, то проверяется наличие у него метода __get__ (либо __set__ или __delete__, в зависимости от действия над атрибутом), если метод найден, то происходит следующий вызов object.field.__get__(object) и возвращается его результат (такие атрибуты называется в Python атрибутами со связанным поведением (binded behavior) и используются, например, для создания свойств).

Эта последовательность распространяется только на пользовательские атрибуты. Системные атрибуты, такие как __dict__, __len__, __add__ и другие, имеющие специальные поля в С-структуре описания класса находятся сразу.

"Новые" и "классические" классы[править]

В версиях до 2.2 некоторые объектно-ориентированные возможности Python были заметно ограничены. Например, было невозможно наследовать встроенные классы и классы из модулей расширения. Свойства (property) не выделялись явно. Начиная с версии 2.2, объектная система Python была существенно переработана и дополнена. Однако для совместимости со старыми версиями Python было решено сделать две объектные модели "классические" типы (полностью совместимые со старым кодом) и "новые". В версии Python3000 поддержка "старых" классов будет удалена.
Для построения "нового" класса достаточно унаследовать его от другого "нового". Если нужно создать "чистый" класс, то можно унаследоваться от object - родительского типа для всех "новых" классов.

class OldStyleClass:pass          # класс "старого" типа
class NewStyleClass(object):pass  # и "нового"

Все стандартные классы - классы "нового" типа.

Агрегация. Контейнеры. Итераторы[править]

Агрегация, когда один объект входит в состав другого, или отношение (HAS-A) "имеет", реализуется в Питоне с помощью ссылок. Питон имеет несколько встроенных типов контейнеров: список, словарь, множество. Можно определить собственные классы контейнеров со своей логикой доступа к хранимым объектам. (Следует заметить, что в Питон агрегацию можно считать разновидностью ассоциации, так реально объекты не вложены друг в друга в памяти и, более того, время жизни элемента может не зависеть от времени жизни контейнера.)

Следующий класс из модуля utils.py среды web.py является примером контейнера-словаря, дополненного возможностью доступа ко значениям при помощи синтаксиса доступа к атрибутам:

class Storage(dict):
   def __getattr__(self, key):
       try:
           return self[key]
       except KeyError, k:
           raise AttributeError, k
    
   def __setattr__(self, key, value):
       self[key] = value
    
   def __delattr__(self, key):
       try:
           del self[key]
       except KeyError, k:
           raise AttributeError, k
    
   def __repr__(self):
     return '<Storage ' + dict.__repr__(self) + '>'

Вот как он работает:

       >>> v = Storage(a=5)
       >>> v.a
       5
       >>> v['a']
       5
       >>> v.a = 12
       >>> v['a']
       12
       >>> del v.a

Для доступа к контейнерам очень удобно использовать итераторы:

>>> cont = dict(a=1, b=2, c=3)
>>> for k in cont:
...     print k, cont[k]
...
a 1
c 3
b 2

Ассоциация и слабые ссылки[править]

Отношение использования (USE-A) экземпляров одного класса другими является достаточно общим отношением. При использовании один класс обычно зависит от интерфейса другого класса (хотя эта зависимость может быть и взаимной). Если один объект использует другой, он обязательно содержит ссылку на него. Объекты могут ссылаться и друг на друга. В этом случае возникают циклические ссылки. Если ссылающиеся друг на друга объекты удалить, то они уже не могут быть удалены интерпретатором Питон с помощью механизма подсчета ссылок. Удалением таких объектов занимается сборщик мусора.

Ассоциацию объектов без присущих ссылкам проблем можно осуществить с помощью слабых ссылок. Слабые ссылки не препятствуют удалению объекта.

Для работы со слабыми ссылками применяется модуль weakref.

Метаклассы[править]

Методы[править]

Статический метод[править]

Статические методы в Python являются синтаксическими аналогами статических функций в основных языках программирования. Они не получают ни экземпляр объекта (self) ни класс (cls) первым параметром. Для создания статического метода используется декоратор

staticmethod
>>> class D(object):  # только "новые" классы могут иметь статические методы
      @staticmethod
      def test(x):
          return x == 0
...
>>> D.test(1)    # доступ к статическому методу можно получать и через класс
False
>>> f = D()
>>> f.test(0)    # и через экземпляр класса
True

Статические методы реализованы с помощью свойств (property).

Метод класса[править]

Классовые методы в Python занимают промежуточное положение между статическими и обычными. В то время как обычные методы получают первым параметром экземпляр класса, а статические не получают ничего, в классовые методы передается класс. Возможность создания классовых методов является одним из следствий того, что в Python классы также являются объектами. Для создания классового метода можно использовать декоратор classmethod

>>> class A(object):  # только "новые" классы могут иметь классовые методы
      def __init__(self,int_val):
          self.val = int_val + 1
      @classmethod
      def fromString(cls,val):   # вместо self принято использовать cls
          return cls(int(val))
...
>>> class B(A):pass
...
>>> x = A.fromString("1")
>>> print x.__class__.__name__
A
>>> x = B.fromString("1")
>>> print x.__class__.__name__
B

Классовые методы достаточно часто используются для перегрузки конструктора. Классовые методы, как и статические, реализуются через свойства (property).

Мультиметоды[править]

Устойчивость объектов[править]

Объекты всегда имеют свое представление в памяти компьютера и их время жизни не больше времени работы программы. Однако зачастую необходимо сохранять данные между запусками приложения и/или передавать их на другие компьютеры. .Одним из решений этой проблемы является Устойчивость объектов (object persistence) которая достигается с помощью хранения представлений объектов (сериализацией) в виде байтовых последовательностей и их последующего восстановления (десериализация).

Модуль pickle является наиболее простым способом "консервирования" объектов в Питон.

Следующий пример показывает как работает сериализация-десериализация:

# сериализация
>>> import pickle
>>> p = set([1, 2, 3, 5, 8])
>>> pickle.dumps(p)
'c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.'
# де-сериализация
>>> import pickle
>>> p = pickle.loads('c__builtin__\nset\np0\n((lp1\nI8\naI1\naI2\naI3\naI5\natp2\nRp3\n.')
>>> print p
set([8, 1, 2, 3, 5])

Получаемая при сериализации строка может быть передана по сети, записана в файл или специальное хранилище объектов, а позже - прочитана. Сериализации поддаются не все объекты. Некоторые объекты (например, классы и функции) представляются своими именами, поэтому для десериализации требуется наличие тех же самых классов. Нужно отметить что нельзя десериализовать данные из непроверенных источников с помощью модуля pickle, так как при этом возможны практически любые действия на локальной системе. При необходимости обмениваться данными по незащищенным каналам или с ненадежными источниками можно воспользоваться другими модулями для сериализации.

В основе сериализации объекта стоит представление его состояния. По умолчанию состояние объекта - это все, что записано в его полях. Пользовательские классы могут управлять сериализацией, предоставляя состояние объекта явным образом (методы __getstate__, __setstate__ и др.).

На стандартном для Питона механизме сериализации построена работа модуля shelve ( shelve(англ. глаг.) - ставить на полку; сдавать в архив). Модуль предоставляет ф-цию open. Объект, который она возврящяет, работает аналогично словарю, но объекты сериализуются и сохраняются в файле:

>>> import shelve
>>> s = shelve.open("myshelve.bin")
>>> s['abc'] = [1, 2, 3]
>>> s.close()
# .....
>>> s = shelve.open("myshelve.bin")
>>> s['abc']
[1, 2, 3]

Сериализация pickle - не единственная возможная, и подходит не всегда. Для сериализации, независимой от языка программирования, можно использовать, например, XML.

Ссылки[править]

Примечания[править]