среда, марта 19, 2008

Доклад по Python: часть II

3. Объекты.

Питон - это объектно-ориентированный язык. Среди всего прочего, это означает: всё есть объект.
В 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>'

15 комментариев:

  1. >> В Питоне полиморфизм реализован за счет Duck Typing

    в С++ это реализовано при помощи шаблонов

    ОтветитьУдалить
  2. > то есть для каждого (например) числа создается экземпляр спецциального класса

    неправда!
    объекты-обёртки создаются лишь по требованию. например, когда число выступает ключём в map

    ОтветитьУдалить
  3. Анонимный3/19/2008 3:29 ПП

    Эта проблема решается различными способами. В С++ для этого используется множественное наследование. В Java - интерфейсы. В Ruby - mix-ins (примеси).

    В Питоне используется концепция, называемая Duck Typing: «Если ЭТО ходит, как утка, и крякает, как утка - значит, это утка».


    Странное сравнение. С помощью mix-ins в Ruby реализуется альтернатива множественного наследования. Python же множественное наследование поддерживает, т.е. похож в этом на С++. Duck Typing используют и Ruby, и Python.

    ОтветитьУдалить
  4. @alexander
    А я что ли утверждал, что оно всегда создается? Покажите где, я поправлю.

    @анонимный
    множественное наследование часто используется с тем, чтобы объект создаваемого класса A можно было передать как в функцию, ожидающую B, так и в функцию, ожидающую C (более общО: чтобы у объекта были свойства класса A, а также классов B и C). В питоне для этого необязательно использовать множественное наследование, т.к. есть Duck Typing.

    Вообще, тут большинство вопросов возникают из-за того, что это конспекты доклада "для себя": я-то знаю, что я имел ввиду... ;)

    ОтветитьУдалить
  5. Инкапсуляция - это не только объединение кода и данных, но и сокрытие деталей реализации! (см. Гради Буч, "Объектно Ориентированный Анализ и Проектирование"). Отсутствие любой из составляющих нарушает следование ОО парадигме вцелом. К моему великому сожалению, в Питоне разработчик не имеет никакой возможности защитить свой код.

    ОтветитьУдалить
  6. @kit
    "Детали реализации" - это код методов. Он и так снаружи не виден. А чтобы классом правильно пользовались - пишите нормальную документацию. Кроме того, атрибуты можно сделать свойствами (properties), а "приватные" методы - реализовать с помощью декораторов, раз уж очень нужно.

    @alex
    А ссылку можно, где посмотреть как Duck Typing сделать на C++ ?

    ОтветитьУдалить
  7. Анонимный3/20/2008 3:48 ДП

    Для проверки, можно ли использовать объект как функцию, используется стандартная функция iscallable(f)

    наверно таки callable(f) ?

    ОтветитьУдалить
  8. @анонимный
    Да, эт очепятка.

    ОтветитьУдалить
  9. >> А ссылку можно, где посмотреть как >> Duck Typing сделать на C++ ?

    про шаблоны (template) прочитайте.

    ОтветитьУдалить
  10. Инкапсуляция - это не только объединение кода и данных, но и сокрытие деталей реализации! (см. Гради Буч, "Объектно Ориентированный Анализ и Проектирование"). Отсутствие любой из составляющих нарушает следование ОО парадигме вцелом.

    Сразу видно моск съеден явой =) Сокрытие деталей реализации вообще к ОО отношения не имеет, так же как и интроспекция. Да и полиморфизм вещь присущаяя не только ООП.

    К моему великому сожалению, в Питоне разработчик не имеет никакой возможности защитить свой код.

    Разработчик может написать __privvar, и если ты её юзаешь, то сам себе злобный буратина.

    Защитить. Ммм. Вообще то даже в С++ можно получить доступ к приватным данным с помощью указателей. Имхо польза сокрытия реализации очень переоценена параноиками.

    ОтветитьУдалить
  11. >> А ссылку можно, где посмотреть как >> Duck Typing сделать на C++ ?

    про шаблоны (template) прочитайте.


    То, что шаблоны в некоторых местах похожи на Duck Typing не делает их таковыми, как и С++ языком с поддержкой Duck Typing. Шаблоны это прежде всего compile-time generics. Вообщем не сравнивайте мягкое с тёплым =)

    ОтветитьУдалить
  12. Если уж так хочется супер защиты данных, то почему бы не использовать замыкания? Не вижу каких-либо проблем в их использовании в Питоне. Если не прав -- поправьте.

    ОтветитьУдалить
  13. Не понятно как замыкания использовать для защиты данных? Странно по крайней мере.

    ОтветитьУдалить