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

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

9. Функциональное программирование.

ФП не является "родной" парадигмой для Питона. В частности, в Питоне нет оптимизации хвостовой рекурсии (создатель языка недавно подтвердил, что и не будет), что делает практически невозможным программирование в характерном для ФП рекурсивном стиле.

Однако некоторые элементы ФП могут быть использованы в Питоне очень эффективно. В частности, выше мы видели примеры применения конструкции lambda. Эта конструкция создает анонимную функцию, а точнее - замыкание (то есть создаваемая функция "запоминает" значения внешних переменных в момент создания). Классический пример использования замыканий:


def add(x,y):
return x+y

inc = lambda x: add(x,1)

def incrementer(n):
return lambda x: add(x,n)

inc(3) # выдаст 4

f = incrementer(3)

f(5) # выдаст 8.

Другие типичные конструкции, позаимствованные из функциональных языков - это стандартные функции map,filter,zip и специальная форма, называемая списочным сокращением (list comprehension).

Функция map(f,list) возвращает список значений, полученный применением функции f к каждому элементу списка list. Пример:

>>> def sqr(x):
... return x*x
...
>>> map(sqr,[1,2,3])
[1, 4, 9]


Функция filter(p,list) возвращает список из только тех элементов списка list, для которых функция p возвращает истину. Пример:


>>> def even(n):
... return n%2 == 0
...
>>> filter(even,[1,2,3,4,5,6])
[2, 4, 6]

Функция zip принимает два (или больше) списка и возвращает список пар (кортежей), составленных из соответствующих элементов списков:

>>> zip([1,2,3,4],["A","B","C","D"],['x','y','z','t'])
[(1, 'A', 'x'), (2, 'B', 'y'), (3, 'C', 'z'), (4, 'D', 't')]

List comprehensions позволяют одной строкой создать списки по какому-нибудь правилу:

>>> [x*x for x in [1,2,3,4,5]]
[1, 4, 9, 16, 25]

Можно тут же отбирать нужные элементы списка:

>>> [x*x for x in [1,2,3,4,5] if even(x)]
[4, 16]

Можно перебирать одновременно несколько последовательностей:

>>> l = [1,2,3,4,5]
>>> [x*y for x in l for y in l]
[1, 2, 3, 4, 5, 2, 4, 6, 8, 10, 3, 6, 9, 12, 15, 4, 8, 12, 16, 20, 5, 10, 15, 20, 25]

10. Декораторы.

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

Применяются декораторы так:

@decor
def f(x,y):
...

и это эквивалентно такой записи:

def f(x,y):
...
f = decor(f)

Классический пример использования декораторов - это отслеживание вызовов функции:
def trace(func):
name = func.func_name # имя переданной функции
def wrapper(*args,**kwargs): # создаем новую функцию
print "Function called: %s(%s,%s)" % (name,args,kwargs)
result = func(*args,**kwargs) # вызываем исходную функцию
print "Function %s returned %s" % (name,result)
return result
return wrapper # возвращаем созданную функцию

@trace
def f(x,y):
return x*y+2

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

Таким образом, декораторы в Питоне обеспечивают возможность аспектно-ориентированного программирования.

Другой пример - декоратор может вызывать исходную функцию только в том случае, если включен режим отладки. Или в режиме выполнения юнит-тестов вызывать функцию-заглушку.

Собственно функция-декоратор должна принимать только один аргумент - исходную функцию. Но можно писать функции, которые возвращают декораторы:

def print_on_call(text):
def decorator(func):
def wrapper(*args,**kwargs):
print ">> "+text
return func(*args,**kwargs)
return wrapper
return decorator

@print_on_call("F called!")
def f(x):
...

Другой пример использования декораторов - реализация делегатов (методов объекта, которые вызывают метод другого класса с тем же именем):

def delegate(cls):
def decorator(meth):
name = meth.func_name
def wrapper(*args,**kwargs):
m = object.__getattribute__(cls,name)
result = m(*args,**kwargs)
return result
return wrapper
return decorator

class A(object):
x = 0
def two(self,z):
r = self.x*z
self.x = z
return r

class B(object):
x = 3

@delegate(A)
def two(self,z):
pass

b = B()
print b.two(5) # Выводит 15

11. Дескрипторы

Дескриптор — это класс с определенными методами __get__() и __set__(). Предполагается, что __get__() возвращает значение экземпляра, а __set__() — соответственно, устанавлнивает.

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

В Питоне есть стандартный класс-дескриптор property, конструирующий свойство объекта из getter-а и setter-а. Типичный пример использования property:


class A(object):
def getx(self):
print "Getter called."
return self._x

def setx(self,value):
print "Setting .x to %s" % value
self._x = value

x = property(getx,setx)
Пользовательские классы-дескрипторы могут выполнять более сложную работу: например, можно хранить количество присваиваний (или более сложное состояние) внутри класса-дескриптора.

Таким образом, дескрипторы - это значительное обобщение свойств (properties), имеющихся в C#.

12. Метаклассы

В Питоне всё есть объект. В том числе, классы - это тоже объекты.
Самые часто используемые объекты — это экземпляры классов. А классы, в свою очередь, являются экземплярами метаклассов.

Можно взглянуть на это несколько с другой стороны: класс — это шаблон для создания экземпляров класса. А метакласс — это шаблон для создания классов.

По умолчанию используется стандартный метакласс type. Однако, наследуясь от него, можно создавать свои метаклассы.

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

>>> class Meta(type):
... def whoami(cls):
... print "I'm a", cls.__name__
...
>>> class A(object):
... __metaclass__ = Meta # Указываем используемый метакласс
... def do_something(self):
... print "Doing something."
...
>>> a = A()
>>> A.whoami()
I'm a A
>>> a.do_something()
Doing something.

Также можно переопределять процесс создания классов:

>>> class Meta2(type):
... def __new__(cls,name,bases,dct): #Конструктор
... print "Creating class",name
... return type.__new__(cls,name,bases,dct)
... def __init__(cls,name,bases,dct): #Инициализатор
... print "Init'ing class", name
... super(Meta2,cls).__init__(name,bases,dct)
... cls.x = 25 # Добавляем атрибуты к классу
... cls.y = 30
...
>>> class B(object):
... __metaclass__ = Meta2
... def f(self):
... print "Calling F."
...
Creating class B
Init'ing class B
>>> b = B()
>>> b.x
25
>>> b.f()
Calling F.

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

Доклад по 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>'

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

1. Что такое?..

Python - это интерпретируемый алгоритмический объектно-ориентированный язык со строгой динамической типизацией, полиморфизм в нем реализован в виде Duck Typing.

Трансляция питона организована очень схожим с Java образом. Именно, исходник компилируется в байт-код, а затем этот байт-код исполняется. Это сходство настолько велико, что существует реализация Питона (Jython), генерирующая Java байт-код для исполнения виртуальной машиной Java. Различие состоит в политике, когда записывать байт-код на диск. Напомню, для Java традиционный способ запустить только что написанную программу такой: запускаем компилятор, подсовывая ему исходник - он генерирует байт-код и записывает его в файл. Затем запускаем виртуальную машину, подсовывая ей байт-код - и она его исполняет.
Питон же обычно не записывает байт-код на диск. В простейшем случае запуск программы происходит так: мы "скармливаем" исходник интерпретатору; он генерирует байт-код, но оставляет его в памяти, а затем передает виртуальной машине (являющейся частью интерпретатора). Это ускоряет запуск программы за счет отсутствия необходимости записывать байт-код на диск.
Однако, при загрузке (импорте) модулей Питон пытается сохранить байт-код, чтобы в следующий раз загрузка модуля происходила быстрее. Есть возможность и саму программу записать в виде байт-кода. Интерпретатору Питона можно подсовывать не только исходники, но и байт-код - он загрузится быстрее.
За счет такого сходства в устройстве с Java имеем и большое сходство в производительности (она примерно одинаковая, только Питон чуть быстрее загружает программы за счет того, что не пишет байт-код на диск).
И именно из-за этого сходства Питон обычно сравнивают именно с Явой.

Еще одно сходство с Явой (и многими другими интерпретируемыми и даже некоторыми компилируемыми языками) - это автоматическое управление памятью. В Питоне нет new[] и delete[], память отводится и освобождается автоматически. Алгоритм сборки мусора как бы "двухслойный": во-первых, сам интерпретатор реализует reference counting (удаляя объекты, на которые никто не ссылается), и во-вторых, есть время от времени запускаемый garbage collector, работающий по более замысловатым, но более быстрым и надежным алгоритмам (например, reference counting не удалит два объекта, ссылающихся друг на друга, даже если на них больше никто не ссылается).

Удобным свойством интерпретатора Питона является наличие REPL (read-eval-print loop), то есть возможности вводить языковые конструкции с консоли и тут же получать результат. Это часто используется для проверки каких-нибудь идей или для отладки.

1.1. Синтаксис.

В самых общих чертах синтаксис Питона напоминает С или Паскаль. Операторы записываются обычно по одному на строку. Присваивание записывается как в С, знаком =. Но при этом присваивание не возвращает значения, поэтому его нельзя использовать в условии. Для сравнений используются обычные для С знаки ==, !=, >,<.>=,<=. Небольшое отличие состоит в том, что Питон понимает "двойные" сравнения в "математическом" смысле, т.е. 0 < x < 10 понимается как 0 < x and x < 10. Основные конструкции проще показать на примерах:

if x < 0:
print "Yes"
elif x==0:
print "X is 0"
else:
print "No"

for i in [1,2,3]:
print i
else:
print "Loop was not break'ed"

while x>0:
print x
x -= 1

try:
z = x/y
except ZeroDivisionError,e:
print e
else:
print "Divided sucsessfully."
finally:
print "Some cleanup."

def fun(x,y):
return x+y

class Child(Parent):
x = 0
def method(self,x,y):
return self.x - x + y

f = open("abc.txt")
for line in f:
print "<%s>" % line
f.close()

В последнем примере показана замена функции sprintf: "строка" % (данные). В проектируемой сейчас версии Python3000 этот способ исчезнет, а вместо него появится другой: "Value {0}, Value {1}".format("one", 2).


1.2. Типы данных.

В Питоне выделяют атомарные и структурные (или ссылочные) типы данных. К атомарным типам относятся числа и строки. Структурные типы - это списки, кортежи (tuples), словари, функции, классы и экземпляры классов.

Данные некоторых типов (а именно - кортежи и строки) являются неизменяемыми.

Списки записываются так: [1, "one", 25]. Список может содержать любое количество объектов любого типа. Обращение к элементу списка - по индексу: a[2] (элементы нумеруются с нуля). Добавление элементов в список можно делать так:

a = a + [25]

или так (предпочтительней, т.к. быстрее):

a.append(25).

Кортежи записываются как значения через запятую: a,b,c. Часто для ясности кортежи приходится записывать в скобках: (a,b,c). Кортежи, как и списки, могут содержать значения любых типов, но, в отличие от списков, являются неизменяемыми.

Для строк, списков и кортежей есть специальный синтаксис для обращения к части данных, называемый срезами (впервые такой синтаксис появился еще в Фортране):

>>> a = "abcde"
>>> a[3:5]
'de'
>>> a[:2]
'ab'
>>> a[4:]
'e'
>>> a[:-2]
'abc'
>>> a[::-1]
'edcba'
>>> a[5:2:-1]
'ed'
>>> a[:]
'abcde'
>>> a[::2]
'ace'

Словари в Питоне - это структура, соответствующая хэшам в Перле, массивам в PHP и std::map в C++. Записываются так:

{'a': 1, 'b': 2, 'c': 3}

или так:

dict(a=1,b=2,c=3).

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

print d['key']

Метод keys() возвращает список из ключей словаря, а values() - список из значений. Порядок следования ключей не определен:

>>> d = dict(a=1,b=2,c=3)
>>> d.keys()
['a', 'c', 'b']
>>> d.values()
[1, 3, 2]

Функции создаются так:

def f(x):
"Documentation string (docstring)"
return x*x

или так:

f = lambda x: x*x

О классах и их экземплярах будем подробно говорить дальше.

2. Типизация.

Главное отличие Питона от Явы (если брать только чисто "лингвистические" свойства) - это типизация. Напомню, в Яве используется строгая статическая типизация с явным объявлением типов переменных. Это означает, что типы всех переменных известны в момент компиляции, и тогда же происходит проверка типов. Это дает преимущество в том плане, что значительная часть ошибок отлавливается в момент компиляции. Зато это замедляет компиляцию. В Питоне используется строгая динамическая типизация. "Динамическая" означает, что типы переменных становятся известными только во время выполнения, и тогда же выполняются проверки типов. Это дает больше возможностей написать неработающий код, но ускоряет компиляцию и дает значительную гибкость. Пример:

>> a = 20 # теперь a - это число, тип int
>> a = "a string" # а теперь - строка, тип str.

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

>>> x = [1,2,3] # x - это список
>>> y = x # y указывает туда же, куда x (копируется указатель).
>>> y[1] = 5 # изменяем элемент из y
>>> x # он изменился и в x.
[1, 5, 3]

Таким образом, в терминах C++, имя переменной - это ссылка (указатель), (void*). Исключением являются атомарные типы данных - числа и строки. Пример:

>>> a = 1
>>> b = a # здесь копируется уже не ссылка, а собственно значение
>>> b = 3 # присваиваем другое значение...
>>> a # на переменную a это не повлияло.
1

По той же схеме передаются аргументы в функцию.

За счет того, что информация о типе хранится вместе со значением, она доступна во время исполнения. Пример:

>>> a = 1
>>> type(a)
<type 'int'>
>>> a = "a string"
>>> type(a)
<type 'str'>

Таким образом, имеет смысл запись вроде if type(a) == str:...

Доклад по Python: About

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

Объем - около 3-х часов.
Что это такое. Особенности, отличия от других языков.
Плюсы и минусы. ООП в питоне. Сравнение реализации ООП в питоне и Ц++/Жаба.
Характерные приемы программирования и дизайна.

Главная задача - даже не "агитировать за питон", а просто расшевелить мозги.


В реальности у меня было сильно меньше трех часов (даже чуть меньше двух), проектора не было, так что пришлось доклад сокращать, примеры кода я показал только самые небольшие (на доску код писать долго и неудобно). Но тем не менее.