Питон - это объектно-ориентированный язык. Среди всего прочего, это означает: всё есть объект.
В C++, на примере которого (к сожалению) обычно обучают объектно-ориентированному программированию, объектами являются только экземпляры классов. Числа, например, объектами не являются.
В Java значения атомарных типов тоже являются объектами, но несколько искуственно: для них создаются так называемые boxed значения, то есть для каждого (например) числа создается экземпляр спецциального класса, содержащий это число.
В Питоне же всё является объектом. Например: экземпляры классов, собственно классы, типы, атомарные объекты (числа и строки), а также функци. Пример с числом:
>>> a = 1
>>> a.__str__() # этот метод дает строковое представление объекта
'1'
Функции принимают в качестве аргументов объекты и возвращают объекты. Например, функция может принимать функцию в качестве аргумента и возвращать класс, или принимать строку и возвращать функцию.
Рассмотрим помаленьку все традиционные принципы ООП.
4. Инкапсуляция.
Инкапсуляция подразумевает: алгоритмы работы с данными хранятся вместе с данными. Для атомарных значений мы это уже видели в предыдущем разделе (a.__str__()). Для экземпляров пользовательских классов это реализуется образом, очень похожим на C++ или Java:
class A(object): # object - класс, стоящий в вершине иерархии наследования
x = 0
y = 0
def move(self,x,y):
self.x = x
self.y = y
a = A()
a.move(2,3)
Здесь можно увидеть, что в Питоне указатель на экземпляр класса передается в методы явным образом, как первый аргумент (из стандарта С++ известно, что там это реализовано так же, только этот первый аргумент явно не выписывается). Здесь видно следование первому принципу философии Питона: явное лучше неявного.
Главное в инкапсуляции, как мы увидим позже - не надо путать инкапсуляцию и сокрытие данных.
5. Наследование.
Наследование подразумевает возможность создания классов объектов, ведущих себя почти также, как "родительский" класс, но "немного по-другому". В простейшем случае реализация наследования в Питоне похожа на C++:
class A(object):
x = 0
class B(A):
y = 1
Возможно и множественное наследование: class A(B,C,D):...
Типичная проблема, возникающая при проектировании в стиле ООП, состоит в следующем. Объект некоторого типа (например, Сотрудник) требуется передавать в качестве аргумента в различные функции. Разным функциям нужны разные свойства и методы этого объекта, причем набор свойств и методов объекта, которые нужны каждой функции, фиксирован. При этом хотелось бы сделать все эти функции полиморфными, то есть способными принимать объекты разных типов.
Эта проблема решается различными способами. В С++ для этого используется множественное наследование. В Java - интерфейсы. В Ruby - mix-ins (примеси).
В Питоне используется концепция, называемая Duck Typing: «Если ЭТО ходит, как утка, и крякает, как утка - значит, это утка». То есть, если у объекта есть все нужные функции свойства и методы, то он подходит в качестве аргумента. Например, в функцию
def f(x):
return x.get_value()
можно передавать объект любого типа, лишь бы у него был метод get_value().
Еще одна типичная проблема, возникающая в связи с множественным наследованием - не всегда очевидно, в каком порядке будут просматриваться родительские классы в поисках нужного свойства или метода. В Питоне для упрощения этой проблемы у каждого класса есть свойство __mro__ (method resolution order):
>>> class A(object): pass
...
>>> class B(object):
... x = 0
...
>>> class C(A,B):
... z = 3
...
>>> C.__mro__
(<class 'C'>, <class 'A'>, <class 'B'>, <type 'object'>)
6. Полиморфизм.
Полиморфизм - это способность функции работать с аргументами разных типов.
В C++ и Java полиморфизм тесно связан с наследованием. Например, в C++, если объявлено, что функция f принимает экземпляр класса A, то она может принимать экземпляр любого класса, унаследованного от A. В Java это поведение расширено: за счет интерфейсов (interfaces) есть возможность передавать в функцию экземпляры классов, не связанных "генетически" (но реализующих один интерфейс).
В Питоне полиморфизм реализован за счет Duck Typing: любая функция может принимать объекты любого типа, но если она попытается использовать свойства, которых у данного объекта нет, возникнет исключение (exception) (функция может перехватить его в конструкции try...except и сделать в этом случае что-то другое). За счет этого, например, функция, работающая с файлами, может принимать в качестве аргумента имя файла или дескриптор открытого файла - и действовать по обстоятельствам.
Таким образом, в Питоне (как и было задумано создателями парадигмы ООП) полиморфизм и наследование - совершенно ортогональные принципы.
7. Интроспекция.
Про этот принцип регулярно забывают, когда рассказывают об ООП на примере С++, а между тем это один из основополагающих принципов.
Заключается он в том, что каждый объект может (во время исполнения) получить информацию об интерфейсах и свойствах, предоставляемых любым другим объектом. Например, в разделе о типизации мы видели, как можно получать информацию о типе переменной во время исполнения.
У каждого объекта есть некоторое количество атрибутов. Атрибуты, имена которых, начинаются с подчеркивания, считаются приватными (private), хотя это и не влияет на область видимости - это только соглашение. "Более приватными" являются атрибуты, имена которых начинаются с двух подчеркиваний - снаружи они винды только как __имя-объекта__имя-атрибута__. Атрибуты, начинающиеся с двух подчеркиваний и заканчивающиеся двумя подчеркиваниями, имеют специальный смысл.
Список всех атрибутов любого объекта можно получить с помощью встроенной функции dir:
>>> a = 1
>>> dir(a)
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__str__', '__sub__', '__truediv__', '__xor__']
Объект, имеющий атрибут __call__, можно вызывать как функцию (собственно, функции в Питоне отличаются от остальных объектов только наличием этого атрибута). Для проверки, можно ли использовать объект как функцию, используется стандартная функция callable(f). Таким образом, методы объекта - это атрибуты, которые можно вызывать.
У функций и классов есть атрибут __doc__, содержащий так называемый docstring - строку документации. При описании функции она пишется на отдельной строке после def, при описании класса - после class. Стандартная функция help() выдает информацию о любом объекте.
Атрибут __name__ любого объекта содержит его имя. У экземпляров классов атрибут __class__ содержит ссылку на класс этого объекта.
Стандартная функция type() возвращает тип объекта (тип - это тоже объект).
С помощью функции isinstance(obj,cls) можно выяснить, является ли объект экземпляром данного класса (или одного из дочерних классов). А функция issubclass(cls1,cls2) выясняет, является ли cls1 потомком cls2.
Модуль inspect, входящий в стандартную поставку Питона, содержит некоторые дополнительные возможности интроспекции. В частности, функция inspect.getargspec(func) сообщает, сколько и каких аргументов ожидает получить функция.
8. Динамизм.
Этот принцип не был сформулирован как один из основных для ООП, однако референсная реализация ООП - Smalltalk - этим свойством обладает.
Речь идет о том, что свойства объекта (включая даже его тип) могут изменяться во время исполнения. Пример:
>>> class A(object):
... pass
>>> a = A()
>>> a.x = 25 # создаем новый атрибут объекта
>>> b = A() # другой экземпляр того же класса
>>> print b.x # вызовет исключение: у объекта b нет атрибута x
>>> b.y = 30 # создаем другой атрибут
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'x']
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'y']
Можно создавать "на ходу" даже методы класса:
>>> A.method = lambda self,s: "<%s %s>" % (s,self.x)
>>> c = A()
>>> c.x = 25
>>> c.method("Text")
'<Text 25>'
>>> C.__mro__
ОтветитьУдалить(, , , )
< и > "съелись"
Спасибо, поправил.
ОтветитьУдалить>> В Питоне полиморфизм реализован за счет Duck Typing
ОтветитьУдалитьв С++ это реализовано при помощи шаблонов
> то есть для каждого (например) числа создается экземпляр спецциального класса
ОтветитьУдалитьнеправда!
объекты-обёртки создаются лишь по требованию. например, когда число выступает ключём в map
Эта проблема решается различными способами. В С++ для этого используется множественное наследование. В Java - интерфейсы. В Ruby - mix-ins (примеси).
ОтветитьУдалитьВ Питоне используется концепция, называемая Duck Typing: «Если ЭТО ходит, как утка, и крякает, как утка - значит, это утка».
Странное сравнение. С помощью mix-ins в Ruby реализуется альтернатива множественного наследования. Python же множественное наследование поддерживает, т.е. похож в этом на С++. Duck Typing используют и Ruby, и Python.
@alexander
ОтветитьУдалитьА я что ли утверждал, что оно всегда создается? Покажите где, я поправлю.
@анонимный
множественное наследование часто используется с тем, чтобы объект создаваемого класса A можно было передать как в функцию, ожидающую B, так и в функцию, ожидающую C (более общО: чтобы у объекта были свойства класса A, а также классов B и C). В питоне для этого необязательно использовать множественное наследование, т.к. есть Duck Typing.
Вообще, тут большинство вопросов возникают из-за того, что это конспекты доклада "для себя": я-то знаю, что я имел ввиду... ;)
Инкапсуляция - это не только объединение кода и данных, но и сокрытие деталей реализации! (см. Гради Буч, "Объектно Ориентированный Анализ и Проектирование"). Отсутствие любой из составляющих нарушает следование ОО парадигме вцелом. К моему великому сожалению, в Питоне разработчик не имеет никакой возможности защитить свой код.
ОтветитьУдалить@kit
ОтветитьУдалить"Детали реализации" - это код методов. Он и так снаружи не виден. А чтобы классом правильно пользовались - пишите нормальную документацию. Кроме того, атрибуты можно сделать свойствами (properties), а "приватные" методы - реализовать с помощью декораторов, раз уж очень нужно.
@alex
А ссылку можно, где посмотреть как Duck Typing сделать на C++ ?
Для проверки, можно ли использовать объект как функцию, используется стандартная функция iscallable(f)
ОтветитьУдалитьнаверно таки callable(f) ?
@анонимный
ОтветитьУдалитьДа, эт очепятка.
>> А ссылку можно, где посмотреть как >> Duck Typing сделать на C++ ?
ОтветитьУдалитьпро шаблоны (template) прочитайте.
Инкапсуляция - это не только объединение кода и данных, но и сокрытие деталей реализации! (см. Гради Буч, "Объектно Ориентированный Анализ и Проектирование"). Отсутствие любой из составляющих нарушает следование ОО парадигме вцелом.
ОтветитьУдалитьСразу видно моск съеден явой =) Сокрытие деталей реализации вообще к ОО отношения не имеет, так же как и интроспекция. Да и полиморфизм вещь присущаяя не только ООП.
К моему великому сожалению, в Питоне разработчик не имеет никакой возможности защитить свой код.
Разработчик может написать __privvar, и если ты её юзаешь, то сам себе злобный буратина.
Защитить. Ммм. Вообще то даже в С++ можно получить доступ к приватным данным с помощью указателей. Имхо польза сокрытия реализации очень переоценена параноиками.
>> А ссылку можно, где посмотреть как >> Duck Typing сделать на C++ ?
ОтветитьУдалитьпро шаблоны (template) прочитайте.
То, что шаблоны в некоторых местах похожи на Duck Typing не делает их таковыми, как и С++ языком с поддержкой Duck Typing. Шаблоны это прежде всего compile-time generics. Вообщем не сравнивайте мягкое с тёплым =)
Если уж так хочется супер защиты данных, то почему бы не использовать замыкания? Не вижу каких-либо проблем в их использовании в Питоне. Если не прав -- поправьте.
ОтветитьУдалитьНе понятно как замыкания использовать для защиты данных? Странно по крайней мере.
ОтветитьУдалить