pyton – пробуем функциональный стиль
DESCRIPTION
Pyton – пробуем функциональный стиль Автор: Андрей Жлобич (Wargaming)TRANSCRIPT
Pyton – пробуем функциональный
стиль
Жлобич АндрейWargaming.net
Python Developer
Minsk Python Meetup
Принципы ФП
✔ Чистота – нет побочных эффектов.✔ Функции – сущности 1го рода.✔ Функции высших порядков.✔ Замыкания.✔ Рекурсия.✔ Неизменяемые структуры.✔ Ленивые вычисления.
Versus
➔ Императивный стиль
➔ Функциональный Декларативный стиль
grep 'search-for' inputfile.txt > s1.txt sort s1.txt > s2.txtuniq s2.txt > result.txtrm s1.txt s2.txt
cat inputfile.txt | grep 'search-for' | sort | uniq | cat > result.txt
Чистые функции
def p_fact(n): # pure return 1 if n < 2 else n * p_fact(n - 1)
def c_fact(n): # referentially transparent try: return c_fact._cache[n] except KeyError: x = c_fact._cache[n] = p_fact(n) return x c_fact._cache = {}
def o_fact(n): # io + logic - bad f = p_fact(n) print("{}! = {}".format(n, f) return f
calculated_factorials = {} def g_fact(n): # side effents f = fact(n) calculated_factorials[n] = f return f
Побочные эффекты – плохо?
● Могут приводить к гейзенбагам.
● Без побочных эффектов никак не обойтись.
● Даже в pure-functional языках.
● Но их можно локализовать.
● Система типов может в этом помогать.
Ошибки начинающих
a = [[]] * 3
a[0].append(1)
print(a)
# [[1], [1], [1]] - WTF?
class Foo:
g = {}
def __init__(self, x):
self.g[x] = x
f1, f2 = Foo(5), Foo(6)
print(f2.x)
# {5:5, 6:6} - WTF
f = [lambda: i for i in [1, 2]]
for x in f:
print(x(), end="")
# 22 – WTF?
def f(x, a=[]):
a.append(x)
print(a)
print(f(1))
print(f(2))
# [1, 2] – WTF?`
Мутабельный объект
class Point(object):
def __init__(self, x, y):
self.x, self.y = x, y
def move(self, dx, dy):
self.x += dx
self.y += dy
def distance(self, other):
return ((self.x - other.x) ** 2 +
(self.y - other.y) ** 2) ** 0.5
Немутабельный объект
class Point(object):
def __init__(self, x, y):
self.x, self.y = x, y
def move(self, dx, dy):
return Point(self.x + dx, self.y + dy)
def distance(self, other):
return ((self.x - other.x) ** 2 +
(self.y - other.y) ** 2) ** 0.5
А зачем тогда объект?
Point = namedtuple('Point', 'x, y')
def move_point(point, dx, dy):
return Point(point.x + dx, point.y + dy)
def distance(point1, point2):
return ((point1.x - point2.x) ** 2 +
(point1.y - point2.y) ** 2) ** 0.5
“Перестаньте писать классы”
class Greeting(object):
def __init__(self, word):
self.word = word
def greet(self, name):
return "{}, {}!".format(self.word, name)
greet_hello = Greeting('Hello').greet
# -- VERSUS --
def greet(word, name):
return "{}, {}!".format(word, name)
greet_hello = functools.partial(greet, 'Hello')
Высокоуровневые функции ввода-вывода
def process_line(line): a, b = map(int, line.split()) return "out: " + str(a + b)
def interact(f): for line in sys.stdin: print(f(line))
def run(): interact(process_line)
Какой вариант лучше?
def print_result(val): print("out:" + str(val))
def run(): for line in sys.stdin: a, b = map(int, line.split()) print_result(a + b)
PEP 443 – singledispatch – in stdlib since 3.4
from singledispatch import singledispatch
@singledispatch
def move_point(point, x, y): ...
@move_point.register(tuple)
def _(point, x, y): ...
@move_point.register(Point)
def _(point, x, y): ...
Функции – сущности 1го рода
def singledispatch(func): registry = {} def dispatch(cls): try: return registry[cls] except KeyError: return _find_impl(cls, registry) def register(cls, func=None): if func is None: return lambda f: register(cls, f) registry[cls] = func return func def wrapper(*args, **kwargs): return dispatch(args[0].__class__)(*args, **kwargs) registry[object] = func wrapper.register = register wrapper.dispatch = dispatch return wrapper
singledispatch – тест
CPython PyPy0
10
20
30
40
50
60
70
80
90
class singledispatch
Origins of lambda
def genfunc(args, expr):
exec("def f(" + args + "): return " + expr)
return eval('f')
vals = [1, 2, 3]
newvals = map(genfunc('x', 'x * 2'), vals)
1993 – Python version < 1.0 – нет lambda
print map(lambda x: x * 2, [1, 2, 3])
# внутри другой функции - ERROR!
y = 2
print map(lambda x: x * y, [1, 2, 3])
# workaround
print map(lambda x, y=y: x * y, [1, 2, 3])
January 1994 – Python 1.0
Origins of lambda
Origins of lambda
April 2001 – Python 2.1 – замыкания
December 2008 – Python 3.0
● Больше итераторов (map, filter, dict values/keys).
● Хотели убрать lambda – оставили!
● Добавили nonlocal – мутабельные замыкания.
● Убрали reduce из builtins.
Sugared lambda
Добавляем “новый” синтаксис
from underscore import _
print map(_ + 1, [1, 2, 3])
assert (_)(1) == 1
assert (_ + _)(1, 2) == 3
assert ((_ * _) + _)(2, 3, 4) == 10
Sugared lambda – реализация
● Прототип underscore <90 строк.
● Реализации на pypi – fn, whatever.
● Нету замыканий, оверхед на создание.
class Underscore(object):
__slots__ = ['_arity', '_call']
def __init__(self, arity, call): self._arity = arity self._call = call
def __call__(self, *args): return self._call(*args)
def __add__(self, value): ... ...
Sugared lambda – скорость
CPython PyPy0
5
10
15
20
25
30
35
40
45
50
lambda underscore whatever fn
X 4
Функции высших порядков
Функции принимают другие функции в качестве аргументов.
map, filter, timeit, iter...
Все декораторы!
trace, memoize, locking, transaction...
Пример
def process_file(filename):
with open(filename) as fp:
lines = iter(fp.readline, "")
ints = map(int, lines)
print map(memoize(func), ints)
timed(process_file)(filename)
Функции – тоже данные
● Можно хранить в структурах данных.
● Список или словарь функций – хорошо!
● Не забываем про замыкания.
● Функции – атомарные значения.
Функции везде
map("%s:%s".__mod__, zip("abc", [1, 2, 3])) # ["a1", "b2", "c3"]
filter(set([1, 3]).__contains__, [2, 3, 4, 5]) # [3]
filter(bool, [1, 0, "", None, 3]) # [1, 3]
reduce(operator.mul, range(1, 5)) # 24
(F(f, a, b) << g << F(p, c))(x) ~~ f(a, b, g(p(c, x)))
from fn import F, _
F(add, 1)(10) # 11
f = F(add, 1) << F(mul, 100)
list(map(f, [0, 1, 2]))
# [1, 101, 201]
list(map(F() << str << (_ ** 2), range(1, 5)))
# ["1", "4", "9", "16"]
Readability counts
ss = ["list", "of", "words"]
# 1 – imperative tlen = 0 for s in ss: tlen += len(s)
# 2 – so ugly reduce(lambda l,r: l+r, map(lambda s: len(s), ss))
# 3 – not bad reduce(add, map(len, ss))
# 4 – good sum(map(len, ss))
Рекурсия
● Во многих функциональных языках – единственная операция для огранизации цикла.
● Многие алгоритмы проще выражаются в рамках рекурсии.
● Не типична для Python программ.
Хвостовая рекурсия
def factorial(n):
if n:
return n * factorial(n - 1)
else:
return 1
def factorial(n, acc=1):
if n:
return factorial(n – 1, n * acc)
else:
return acc
TCO в Python
● Мешает красивым стектрейсам.
● Это оптимизация – деталь реализации, а не элемент языка.
● Рекурсия – не базовая операция в программировании.
● Мешает динамичная сущность Python'а.
Trampoline
def trampoline(f):
def wrapper(*args, **kwargs):
ff = f(*args, **kwargs)
while callable(ff): ff = ff()
return ff
return wrapper
def factorial(n, acc=0):
if n:
return lambda: factorial(n - 1, n * acc)
else:
return acc
print(trampoline(factorial)(10))
Эмулируем хвостовую рекурсию
Нельзя использовать как декоратор!
Trampoline - варианты
def recur(*args, **kwargs): ...
@trampoline
def factorial(n, acc=0):
if n:
return recur(
n - 1, n * acc)
else:
return acc
@trampoline
def factorial(n, acc=0):
if n:
yield factorial(
n - 1, n * acc)
else:
return acc
Py3k only
Сами реализуем TCO
➔ Модификация байткода.➔ Модификация исходного кода (препроцессинг).➔ Анализ стека (sys._getframe()).➔ Хранение локального состояния (threading.local).
Оптимизируем хвостовой вызов без модификации самой функции
Используем threading.local
_FunCall = namedtuple( '_FunCall', 'func, args, kwargs') def tco(f): tl = threading.local() tl.trampolined = False def func(*args, **kwargs): if not tl.trampolined: try: tl.trampolined = True while 1: res = f(*args, **kwargs) if isinstance(res, _FunCall) and \ res.func is f: args = res.args kwargs = res.kwargs else: return res finally: tl.trampolined = False else: return _FunCall(f, args, kwargs) return func
Проще – быстрее?
Не обрабатывает ситауцию
f → k → f
Используем sys._getframe()
TailRecurseCall = collections.namedtuple( 'TailRecurseCall', 'args, kwargs') def tco(f): def wrapper(*args, **kwargs): fr = sys._getframe() b = (fr.f_back and fr.f_back.f_back and fr.f_back.f_back.f_code == fr.f_code) del fr if b: return TailRecurseCall(args, kwargs) else: while 1: r = f(*args, **kwargs) if isinstance(r, TailRecurseCall): args = r.args kwargs = r.kwargs else: return r return wrapper
trampoline на стероидах
“правильная” реализация
Trampoline/TCO bench
CPython PyPy0
5
10
15
20
25
30
35
40
loop recursive trampoline tco-simple tco-getframe
X 5
Итераторы
● В Python они везде!● Но в Python3K их еще больше.● Простая универсальная абстракция.● Ленивые вычисления.● Простота композиции.● Запись в “итеративном” стиле
(генераторы)
Itertools
things = [('2009-08-01', 11), ('2009-08-23', 3), ('2009-09-03', 10),
('2009-09-03', 4), ('2009-09-05', 22), ('2009-09-09', 33), ...]
get_date = itemgetter(0)
get_value = itemgetter(1)
filtered1 = dropwhile(lambda x: get_date(x) < '2009-09-01', things)
filtered2 = takewhile(lambda x: get_date(x) < '2009-10-01', things)
grouped_by_date = groupby(filtered2, get_date)
get_total_value = lambda (dt, items): \
(dt, reduce(add, map(get_value, items)))
result = sorted(map(get_total_value, grouped_by_date), key=get_value)
print(list(result))
# [('2009-09-03', 36), ('2009-09-06', 33)]
Funcy
from funcy import *
walk(reversed, {'a': 1, 'b': 2}) # {1: 'a', 2: 'b'}
walk_keys(double, {'a': 1, 'b': 2}) # {'aa': 1, 'bb': 2}
walk_values(inc, {'a': 1, 'b': 2}) # {'a': 2, 'b': 3}
select(even, {1,2,3,10,20}) # {2,10,20}
select(r'^a', ('a','b','ab','ba')) # ('a','ab')
some(even, [1, 2, 5]) # 2
take(4, iterate(double, 1)) # [1, 2, 4, 8]
first(drop(3, count(10))) # 13
split(odd, range(5)) # [[1, 3], [0, 2, 4]]
chunks(2, range(5)) # [[0, 1], [2, 3], [4]]
Итераторы не pure
odd = lambda x: bool(x % 2)
odds = ifilter(odd, count())
print(list(islice(odds, 4)))
# [1, 3, 5, 7] – ok
print(list(islice(odds, 4)))
# [9, 11, 13, 15] – WTF
Итераторы работают с побочными эффктамиМожно использовать (пройтись) только один раз.
Lazycol
class lazycol(object):
__slots__ = 'iterator'
def __new__(cls, iterable):
if isinstance(iterable, (tuple, frozenset, lazycol)):
return iterable
return object.__new__(cls)
def __init__(self, iterable):
self.iterator = iter(iterable)
def __iter__(self):
self.iterator, result = itertools.tee(self.iterator)
return result
Fn - Streams
s = Stream() << [1,2,3,4,5]
assert list(s) == [1,2,3,4,5]
assert s[1] == 2
assert list(s[0:2]) == [1,2]
s = Stream() << range(6) << [6,7]
assert list(s) == [0,1,2,3,4,5,6,7]
def gen():
yield 1; yield 2; yield 3
s = Stream() << gen << (4,5)
assert list(s) == [1,2,3,4,5]
● Элементы вычисляются лениво (“по требованию”).
● Элементы кешируются.
● Коллекция, не итератор.
● Мутабельная.
Fn - Streams
from fn import Stream
from fn.iters import take, drop, map
from operator import add
f = Stream()
fib = f << [0, 1] << map(add, f, drop(1, f))
assert list(take(10, fib)) == [0,1,1,2,3,5,8,13,21,34]
assert fib[20] == 6765
assert fib[20] == 6765
assert list(fib[30:34]) == [832040,1346269,2178309,3524578]
Ленивая бесконечная последовательность чисел фиббоначи
Иммутабельные структуры
● Нельзя случайно изменить – меньше ошибок и времени в дебаггере.
● Могут быть оптимизированы для многопоточных програм – не в Python.
● Могут разделять общую структуру.
● Встроенные: str, tuple, frozenset.
frozenlist, frozendict
def _build_fmethod(parent_method):
def method(self, *args, **kwargs):
if self._frozen: raise TypeError("frozen list")
return parent_method(self, *args, **kwargs)
return method
class frozenlist(list):
def __init__(self, iterable=None):
list.__init__(self, iterable)
self._frozen = False
if __debug__:
for mn in ['append', 'extend', …, '__delitem__']:
locals()[mn] = _build_fmethod(getattr(list, mn))
def freeze(self):
self._frozen = True
Иммутабельные структуры – реализации
● immutablepy
● dictproxyhack
● fronzendict
● changeless
● werkzeug.datastructures
● sqlalchemy.util
Персистентные структуры
● Хрянят “историю”
● Разделяют общую структуру
● Используются в Clojure, Git, CouchDB
Funktown
v = ImmutableVector([1, 2, 3])
v2 = v.conj(9)
print(v2)
# [1, 2, 3, 9]
v = v.assoc(1, 999)
print(v, v[1])
# 999
v = v.conj([])
v[3].append('BAD')
print(v)
# [1, 2, 3, ['BAD']]
d = ImmutableDict({'a': 1})
d = d.assoc('b', 2)
print(d['b'], d.get('b'))
# 2 2
print(d.assoc('c', 9))
# {'a': 1, 'c': 9, 'b': 2}
print(d.remove('d'))
# {'a': 1, 'b': 2}
print(d.items())
# [('a', 1), ('b', 2)]
I have never considered Python to be heavily influenced by functional languages, no matter what people say or think. I was much more familiar with imperative languages such as C and Algol 68 and although I had made functions first-class objects, I didn't view Python as a functional programming language.
Мнение автора языка
Guido van Rossum
В завершение
● Python – не функциональный язык.● Но в нем есть функциональные элементы.● Избегайте побочных эффектов.● Не стоит увлекаться классами.● Плохой код можно написать в любом стиле.● В любом случае весьма полезно познакомится
с Clojure, Haskell, Erlang и другими.
Minsk Python Meetup
Всем большоеспасибо за внимание
вопросы?
[email protected]@jabber.ru
anjensan at github, habrahabr etc