- Объектно-ориентированное программирование на Питоне
-
С самого начала Питон проектировался как объектно-ориентированный язык программирования [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) >>>
Следует заметить, что в момент вызова деструктора (например, по завершении программы) среда исполнения может быть уже достаточно «истощенной», поэтому в деструкторе следует делать только самое необходимое. Кроме того, необработанные в деструкторе исключения игнорируются.
Время жизни объекта
Без применения каких-либо особых средств время жизни объекта, определённого в программе на Python, не выходит за рамки времени выполнения процесса этой программы.
Для преодоления этого ограничения существуют различные возможности: от хранения объектов в простейшей базе данных (shelve), применения ORM до использования специализированных баз данных с развитыми возможностями (например, ZODB, ZEO). Все эти средства позволяют делать объекты устойчивыми (persistent). Как правило, при записи объекта производится его сериализация, а при чтении — десериализация.
>>> import shelve >>> s = shelve.open("somefile.db") >>> s['myobject'] = [1, 2, 3, 4, 'свечка'] >>> s.close() >>> import shelve >>> s = shelve.open("somefile.db") >>> print s['myobject'] [1, 2, 3, 4, '\xd1\x81\xd0\xb2\xd0\xb5\xd1\x87\xd0\xba\xd0\xb0']
Инкапсуляция и доступ к свойствам
Инкапсуляция является одним из ключевых понятий ООП. Все значения в Питон являются объектами, инкапсулирующими код (методы) и данные и предоставляющими пользователям общедоступный интерфейс. Методы и данные объекта доступны через его атрибуты.
Сокрытие информации о внутреннем устройстве объекта выполняется в Питон на уровне соглашения между программистами о том, какие атрибуты относятся к общедоступному интерфейсу класса, а какие — к его внутренней реализации. Одиночное подчеркивание в начале имени атрибута говорит о том, что метод не предназначен для использования вне методов класса (или вне функций и классов модуля), однако, атрибут все-таки доступен по этому имени. Два подчеркивания в начале имени дают несколько большую защиту: атрибут перестает быть доступен по этому имени. Последнее используется достаточно редко. Есть существенное отличие между такими атрибутами и личными (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 = 5
Так и с использованием свойств с заданными методами для получения, установки и удаления атрибута:
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 = 5
Разумеется, первый способ хорош только если значение атрибута является атомарной операцией по изменению состояния объекта. Если же это не так, то второй способ позволит выполнить все необходимые действия в соответствующих методах.
Существуют два способа централизованно контролировать доступ к атрибутам. Первый основан на перегрузке методов
__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): return 'child' >>> x = Parent() >>> x.who(), x.isParOrPChild() ('parent', True) >>> x = Child() >>> x.who(), 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)
Используя специально предусмотренное исключение
NotImplementedError
, можно имитировать чисто виртуальные методы:>>> class abstobj(object): def abstmeth(self): raise NotImplementedError('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 NotImplementedError: Method abstobj.abstmeth is pure virtual
Или, с использованием декоратора, так:
>>> def abstract(func): def closure(*dt, **mp): raise NotImplementedError("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
Однако, в этом случае никакие преобразования типов не делаются, поэтому забота о согласованности данных всецело лежит на программисте. Кроме того, присваивание атрибуту
__class__
не должно применяться по поводу и без. Прежде чем решиться на его использование, необходимо рассмотреть менее радикальные варианты реализации изменения объекта, то есть по сути шаблона проектирования State.Более того, полиморфизм в Питоне вообще не связан с наследованием, поэтому его можно считать сигнатурно-ориентированным полиморфизмом (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
) и память, но приводит к невозможности изменения методов у встроенных классов.Отношения между классами
Наследование и множественное наследование
При описании предметной области классы могут образовывать иерархию, в корне которой стоит базовый класс, а нижележащие классы (подклассы) наследуют свои атрибуты, уточняя и расширяя поведение вышележащего класса (надкласса). Обычно принципом построения классификации является отношение «IS-A» («есть»).
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'
В Python (из-за «утиной типизации») отсутствие наследования ещё не означает, что объект не может предоставлять тот же самый интерфейс.
Множественное наследование в Python применяется в основном для добавления примесей (mixins) — специальных классов, вносящих некоторую черту поведения или набор свойств.
Порядок разрешения доступа к методам и полям
За достаточно простым в использовании механизмом доступа к атрибутам в Python кроется довольно сложный алгоритм. Далее будет приведена последовательность действий, производимых интерпретатором при разрешении запроса
object.field
(поиск прекращается после первого успешно завершённого шага, иначе происходит переход к следующему шагу).- Если у
object
есть метод__getattribute__
, то будет вызван он с параметром'field'
(либо__setattr__
или__delattr__
в зависимости от действия над атрибутом) - Если у
object
есть поле__dict__
, то ищетсяobject.__dict__['field']
- Если у
object.__class__
есть поле__slots__
, то'field'
ищется вobject.__class__.__slots__
- Проверяется
object.__class__.__dict__['fields']
- Производится рекурсивный поиск по
__dict__
всех родительских классов (при множественном наследовании поиск производится в режиме deep-first, в том порядке как базовые классы перечислены в определении класса-потомка). Алгоритм поиска разный для «классических» и «новых» классов. - Если у
object
есть метод__getattr__
, то вызывается он с параметром'field'
- Возбуждается исключение
AttributeError
.
Если поиск окончен успешно, то проверяется, является ли атрибут классом «нового стиля». Если является, то проверяется наличие у него метода
__get__
(либо__set__
или__delete__
, в зависимости от действия над атрибутом), если метод найден, то происходит следующий вызовobject.field.__get__(object)
и возвращается его результат (такие атрибуты называется в Python атрибутами со связанным поведением (binded behavior) и используются, например, для создания свойств [3]).Эта последовательность распространяется только на пользовательские атрибуты. Системные атрибуты, такие как
__dict__
,__len__
,__add__
и другие, имеющие специальные поля в С-структуре описания класса находятся сразу.«Новые» и «классические» классы
В версиях до 2.2 некоторые объектно-ориентированные возможности Python были заметно ограничены. Например, было невозможно наследовать встроенные классы и классы из модулей расширения. Свойства (property) не выделялись явно. Начиная с версии 2.2, объектная система Python была существенно переработана и дополнена. Однако для совместимости со старыми версиями Python было решено сделать две объектные модели: «классические» типы (полностью совместимые со старым кодом) и «новые» [4]. В версии 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
.Метаклассы
Обычных возможностей объектно-ориентированного программирования хватает далеко не всегда. В некоторых случаях требуется изменить сам характер системы классов: расширить язык новыми типами классов, изменить стиль взаимодействия между классами и окружением, добавить некоторые дополнительные аспекты, затрагивающие все используемые в приложении классы, и т. п.
При объявлении метакласса за основу можно взять класс
type
. Пример:# описание метакласса class myobject(type): # небольшое вмешательство в момент выделения памяти для класса def __new__(cls, name, bases, dict): print "NEW", cls.__name__, name, bases, dict return type.__new__(cls, name, bases, dict) # небольшое вмешательство в момент инициализации класса def __init__(cls, name, bases, dict): print "INIT", cls.__name__, name, bases, dict return super(myobject, cls).__init__(cls, name, bases, dict) # порождение класса на основе метакласса (заменяет оператор class) MyObject = myobject("MyObject", (), {}) # обычное наследование другого класса из только что порожденного class MySubObject(MyObject): def __init__(self, param): print param # получение экземпляра класса myobj = MySubObject("parameter")
Разумеется, вместо оператора
print
код метакласса может выполнять более полезные функции: регистрировать класс, передавать действия с классами на удаленную систему, использовать классы для других целей (например, как декларации или ограничения) и т. п.Методы
Метод
Синтаксис описания метода ничем не отличается от описания функции, разве что его положением внутри класса и характерным первым формальным параметром
self
, с помощью которого внутри метода можно ссылаться на сам экземпляр класса (название self является соглашением, которого придерживаются программисты на Python):class MyClass(object): def mymethod(self, x): return x == self._x
Статический метод
Статические методы в 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).
Мультиметоды
Примером для иллюстрации сути мультиметода может служить функция
add()
из модуляoperator
:>>> import operator as op >>> print op.add(2, 2), op.add(2.0, 2), op.add(2, 2.0), op.add(2j, 2) 4 4.0 4.0 (2+2j)
В языке Python достаточно легко реализовать и определённые пользователем мультиметоды [5]. Здесь можно найти пример.
Устойчивость объектов
Объекты всегда имеют своё представление в памяти компьютера и их время жизни не больше времени работы программы. Однако зачастую необходимо сохранять данные между запусками приложения и/или передавать их на другие компьютеры. Одним из решений этой проблемы является Устойчивость объектов (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.Ссылки
Примечания
Категории:- Объектно-ориентированное программирование
- Python
Wikimedia Foundation. 2010.