«Простое» программирование на python

Автор: admin от 9-01-2018, 22:00, посмотрело: 31

«Простое» программирование на python

functools (это такая свалка для всяких ненужных мне вещей :-).

— Гвидо ван Россум

Может показаться, что статья о ФП, но я не собираюсь обсуждать парадигму. Речь пойдет о переиспользовании и упрощении кода — я попытаюсь доказать, что вы пишете слишком много кода, поэтому он сложный и тяжело тестируется, но самое главное: его долго читать и менять.



В статье заимствуются примеры и/или концепции из библиотеки funcy. Во-первых, она клевая, во-вторых, вы сразу же сможете начать ее использовать. И да, нам понадобится ФП.

Чистые функции зависят только от своих параметров и возвращают только свой результат. Следующая функция вызванная несколько раз с одним и тем же аргументом выдаст разный результат (хоть и один и тот же объект, в данном случае %).



Напишем функцию-фильтр, которая возвращает список элементов с тру-значениями.



pred = bool
result = []

def filter_bool(seq):
    for x in seq:
        if pred(x):
            result.append(x)
    return result


Сделаем ее чистой:



pred = bool

def filter_bool(seq):
    result = []
    for x in seq:
        if pred(x):
            result.append(x)
    return result


Теперь можно вызвать ее лярд раз под ряд и результат будет тот же.



Функции высшего порядка



Это такие функции, которые принимают в качестве аргументов другие функции или возвращают другую функцию в качестве результата.



def my_filter(pred, seq):
    result = []
    for x in seq:
        if pred(x):
            result.append(x)
    return result


Мне пришлось переименовать функцию, потому что она теперь куда полезнее:



above_zero = my_filter(bool, seq)
only_odd = my_filter(is_odd, seq)
only_even = my_filter(is_even, seq)


Заметьте, одна функция и делает уже много чего. Вообще-то, она должна быть ленивой, делаем:



def my_filter(pred, seq):
    for x in seq:
        if pred(x):
            yield x


Вы заметили, что мы удалили код, а стало только лучше? Это лишь начало, скоро мы будем писать функции только по праздникам. Вот смотрите:



my_filter = filter


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



Частичное применение



Это процесс фиксации части аргументов функции, который создает другую функцию, меньшей арности. В переводе на наш это functools.partial.



filter_bool = partial(filter, bool)
filter_odd = partial(filter, is_odd)
filter_even = partial(filter, is_even)


Я понимаю, что это все азы ФП, но хочу отметить, что мы не написали ничего нового: мы взяли уже готовые функции и сделали другие. Основа новых — очень маленькие, простые, легкотестируемые функции, мы можем без опаски использовать их для создания более сложных.



Композирование



Такой простой, крутой и нужной штуки в python нет. Ее можно написать самостоятельно, но хотелось бы вменяемой сишной имплементации :(



def compose(*fns):
    init, *rest = reversed(fns)
    return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))


Теперь мы можем делать всякие штуки (выполнение идет справа налево):



mapv = compose(list, map)
filterv = compose(list, filter)


Это прежние версии map и filter из второй версии python. Теперь, если вам понадобится неленивый map, вы можете вызвать mapv. Или по старинке писать чуть больше кода. Каждый раз.



Функции compose и partial прекрасны тем, что позволяют переиспользовать уже готовые, оттестированные функции. Но самое главное, если вы понимаете преимущество данного подхода, то со временем станете сразу писать их готовыми к композиции.



Это очень важный момент — функция должна решать одну простую задачу, тогда:




  • она будет маленькой

  • ее будет проще тестировать

  • легко композировать

  • просто читать и менять

  • тяжело сломать



Пример



Задача: дропнуть None из последовательности.

Решение по старинке (чаще всего даже не пишется в виде функции):



no_none = (x for x in seq if x is not None)


Обратите внимание: без разницы как называется переменная в выражении. Это настолько неважно, что большинство программистов тупо пишут x, чтобы не заморачиваться. Все пишут этот бессмысленный код раз за разом. Каждый цензура раз: for, in, if и несколько раз x — потому что для компрехеншена нужен scope и у него есть свой синтаксис. Мы пишем: на каждую итерацию цикла присвоить переменной значение. И оно присваивается, и проверяется условие.



Мы каждый раз пишем этот бойлерплейт и пишем тесты на этот бойлерплейт. Зачем?



Давайте перепишем:



from operator import is_
from itertools import filterfalse
from functools import partial

is_none = partial(is_, None)
filter_none = partial(filterfalse, is_none) 

# Использование
no_none = filter_none(seq)

# Переиспользование
all_none = compose(all, partial(map, is_none))


Все. Никакого лишнего кода. Мне приятно такое читать, потому что этот код (no_none = filter_none(seq)) очень простой. То, как работает это функция, мне нужно прочитать ровно один раз за все время в проекте. Компрехеншен вам придется читать каждый раз, чтобы точно понять что оно делает. Ну или засуньте ее в функцию, без разницы, но не забудьте про тесты.



Пример 2



Довольно частая задача получить значения по ключу из массива словарей.



names = (x['name'] for x in users)


Кстати, работает очень быстро, но мы снова написали кучу ненужной фигни. Перепишем, чтобы работало еще быстрее:



from operator import itemgetter

def pluck(key, seq):
    return map(itemgetter(key), seq)

# Использование
names = pluck('name', users)


А как часто мы это будем делать?



get_names = partial(pluck, 'name')
get_ages = partial(pluck, 'age')

# Сложнее
get_names_ages = partial(pluck, ('name', 'age'))
users_by_age = compose(dict, get_names_ages)

ages = users_by_ages(users)  # {x['name']: x['age'] for x in users}


А если у нас объекты? Пф, параметризируй это:



from operator import itemgetter, attrgetter

def plucker(getter, key, seq):
    return map(getter(key), seq)

pluck = partial(plucker, itemgetter)
apluck = partial(plucker, attrgetter)

# Использование
names = pluck('name', users)  # (x['name'] for x in users)
object_names = apluck('name', users)  # (x.name for x in users)

# Геттеры умеют сразу таплы данных
object_data = apluck(('name', 'age', 'gender'), users)  # ((x.name, x.age, x.gender) for x in users)


Пример 3



Представим себе простой генератор:



def dumb_gen(seq):
    result = []
    for x in seq:
        # здесь что-то проиcходит
        result.append(x)
    return result


Тут полно бойлерплейта: мы создаем пустой список, затем пишем цикл, добавляем элемент в список, отдаем его. Кажется, я буквально перечислил все тело функции :(



Правильным решением будут использование filter(pred, seq) или map(func, seq), но иногда нужно сделать что-то сложнее, т.е. генератор написать действительно нужно. А если результат всегда нужен в виде списка или тапла? Да легко:



@post_processing(list)
def dumb_gen(seq):
    for x in seq:
        ...
        yield x


Это параметрический декоратор, работает он так:



result = post_processing(list)(dumb_gen)(seq)


Т.е. результатом первого вызова будет новая функция, которая примет функцию в качестве аргумента и вернет другую функцию. Звучит сложнее, чем есть:



def post_processing(post):
    return lambda func: compose(post, func)


Обратите внимание, я использовал уже существующую compose. Результат — новая функция, которую никто не писал.

А теперь стихи:



post_list = post_processing(list)
post_tuple = post_processing(tuple)
post_set = post_processing(set)
post_dict = post_processing(dict)
join_comma = post_processing(', '.join)

@post_list
def dumb_gen(pred, seq):
    for x in seq:
        ...
        yield x


Куча новых функций по цене одной! И я убрал бойлерплейт, функция стала меньше и намного симпатичнее.



Итог



Перебирая данные железобетонными функциями (чистыми, высшими), мы сохраняем простоту реализации и обеспечиваем стабильность программы, которую проще тестировать:




  • пишите чистые функции, они обеспечат стабильность программы

  • пишите функции высшего порядка, код станет намного компактнее и надежнее

  • композируйте, декорируйте, частично применяйте, переиспользуйте код

  • используйте сишные либы, они дадут скорости вашему софту



Как только вы напишете свой набор инструментов, новый код будет создаваться со знанием того, что у вас есть штука, которая может решить часть задачи. А значит софт будет меньше и проще.



С чего начать?




  • обязательно ознакомьтесь с itertools, functools, operator, collections, в особенности с примерами в конце

  • загляните в документацию funcy или другой фпшной либы, почитайте исходный код

  • напишите свой funcy, весь он сразу вам не нужен, но опыт очень пригодится



Credits



В моем случае, использование ФП началось со знакомства с clojure — это штука капитально выворачивает мозги, настоятельно рекомендую посмотреть хотя бы видосы на ютубе.



Clojure как-то так устроен, что вам приходится писать проще, без привычных нам вещей: без переменных, без любимого стиля "романа", где сначала мы раскрываем личность героя, потом пускаемся в его сердечные проблемы. В clojure вам приходится думать %) В нем только базовые типы данных и "отсутствие синтаксиса" (с). И эту "простую" концепцию, оказывается, можно портировать в python.



Источник: Хабрахабр

Категория: Админитстрирование » Системное администрирование

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *