Кодим на Python по-функциональному: Познаем силу функциональной парадигмы программирования. Мир Python: функционалим по-маленьку - Продвинутый Python - Hexlet. Питон в действии

Существует несколько парадигм в программировании, например, ООП, функциональная, императивная, логическая, да много их. Мы будем говорить про функциональное программирование.

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

Сегодня познакомимся с простыми элементами, а сложные конструкции будут в других уроках.

Теория в теории

Как и в разговоре об ООП, так и о функциональном программировании, мы стараемся избегать определений. Все-таки четкое определение дать тяжело, поэтому здесь четкого определения не будет. Однако! Хотелки для функционального языка выделим:

  • Функции высшего порядка
  • Чистые функции
  • Иммутабельные данные

Это не полный список, но даже этого хватает чтобы сделать сделать "красиво". Если читателю хочется больше, то вот расширенный список:

  • Функции высшего порядка
  • Чистые функции
  • Иммутабельные данные
  • Замыкания
  • Ленивость
  • Хвостовая рекурсия
  • Алгебраические типы данных
  • Pattern matching

Постепенно рассмотрим все эти моменты и как использовать в Python.

А сегодня кратко, что есть что в первом списке.

Чистые функции

Чистые функции не производят никаких наблюдаемых побочных эффектов, только возвращают результат. Не меняют глобальных переменных, ничего никуда не посылают и не печатают, не трогают объектов, и так далее. Принимают данные, что-то вычисляют, учитывая только аргументы, и возвращают новые данные.

  • Легче читать и понимать код
  • Легче тестировать (не надо создавать «условий»)
  • Надежнее, потому что не зависят от «погоды» и состояния окружения, только от аргументов
  • Можно запускать параллельно, можно кешировать результат

Иммутабельные данные

Иммутабельные структуры данных - это коллекции, которые нельзя изменить. Примерно как числа. Число просто есть, его нельзя поменять. Также и иммутабельный массив - он такой, каким его создали, и всегда таким будет. Если нужно добавить элемент - придется создать новый массив.

Преимущества неизменяемых структур:

  • Безопасно разделять ссылку между потоками
  • Легко тестировать
  • Легко отследить жизненный цикл (соответствует data flow)

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

Функцию, принимающую другую функцию в качестве аргумента и/или возвращающую другую функцию, называют функцией высшего порядка :

Def f(x): return x + 3 def g(function, x): return function(x) * function(x) print(g(f, 7))

Рассмотрели теорию, начнем переходить к практике, от простого к сложному.

Списковые включения или генератор списка

Рассмотрим одну конструкцию языка, которая поможет сократить количество строк кода. Не редко уровень программиста на Python можно определить с помощью этой конструкции.

Пример кода:

For x in xrange(5, 10): if x % 2 == 0: x =* 2 else: x += 1

Цикл с условием, подобные встречаются не редко. А теперь попробуем эти 5 строк превратить в одну:

>>>

Недурно, 5 строк или 1. Причем выразительность повысилась и такой код проще понимать - один комментарий можно на всякий случай добавить.

В общем виде эта конструкция такова:

Стоит понимать, что если код совсем не читаем, то лучше отказаться от такой конструкции.

Анонимные функции или lambda

Продолжаем сокращать количества кода.

Def calc(x, y): return x**2 + y**2

Функция короткая, а как минимум 2 строки потратили. Можно ли сократить такие маленькие функции? А может не оформлять в виде функций? Ведь, не всегда хочется плодить лишние функции в модуле. А если функция занимает одну строчку, то и подавно. Поэтому в языках программирования встречаются анонимные функции, которые не имеют названия.

Анонимные функции в Python реализуются с помощью лямбда-исчисления и выглядят как лямбда-выражения:

>>> lambda x, y: x**2 + y**2 at 0x7fb6e34ce5f0>

Для программиста это такие же функции и с ними можно также работать.

Чтобы обращаться к анонимным функциям несколько раз, присваиваем переменной и пользуемся на здоровье.

>>> (lambda x, y: x**2 + y**2)(1, 4) 17 >>> >>> func = lambda x, y: x**2 + y**2 >>> func(1, 4) 17

Лямбда-функции могут выступать в качестве аргумента. Даже для других лямбд:

Multiplier = lambda n: lambda k: n*k

Использование lambda

Функции без названия научились создавать, а где использовать сейчас узнаем. Стандартная библиотека предоставляет несколько функций, которые могут принимать в качестве аргумента функцию - map(), filter(), reduce(), apply().

map()

Функция map() обрабатывает одну или несколько последовательностей с помощью заданной функции.

>>> list1 = >>> list2 = [-1, 1, -5, 4, 6] >>> list(map(lambda x, y: x*y, list1, list2)) [-7, 2, -15, 40, 72]

Мы уже познакомились с генератором списков, давайте и воспользуемся если длина список одинаковая):

>>> [-7, 2, -15, 40, 72]

Итак, заметно, что использование списковых включений короче, но лямбды более гибкие. Пойдем дальше.

filter()

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

>>> numbers = >>> list(filter(lambda x: x < 5, numbers)) # В результат попадают только те элементы x, для которых x < 5 истинно

То же самое с помощью списковых выражений:

>>> numbers = >>>

reduce()

Для организации цепочечных вычислений в списке можно использовать функцию reduce(). Например, произведение элементов списка может быть вычислено так (Python 2):

>>> numbers = >>> reduce(lambda res, x: res*x, numbers, 1) 720

Вычисления происходят в следующем порядке:

((((1*2)*3)*4)*5)*6

Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):

>>> reduce(lambda res, x: res*x, , 1) 1

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

>>> reduce(lambda res, x: [x]+res, , )

Для наиболее распространенных операций в Python есть встроенные функции:

>>> numbers = >>> sum(numbers) 15 >>> list(reversed(numbers))

В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.

apply()

Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):

>>> def f(x, y, z, a=None, b=None): ... print x, y, z, a, b ... >>> apply(f, , {"a": 4, "b": 5}) 1 2 3 4 5

В Python 3 вместо функции apply() следует использовать специальный синтаксис:

>>> def f(x, y, z, a=None, b=None): ... print(x, y, z, a, b) ... >>> f(*, **{"a": 4, "b": 5}) 1 2 3 4 5

На этой встроенной функции закончим обзор стандартной библиотеки и перейдем к последнему на сегодня функциональному подходу.

Замыкания

Функции, определяемые внутри других функций, представляют собой замыкания. Зачем это нужно? Рассмотрим пример, который объяснит:

Код (вымышленный):

Def processing(element, type_filter, all_data_size): filters = Filter(all_data_size, type_filter).get_all() for filt in filters: element = filt.filter(element) def main(): data = DataStorage().get_all_data() for x in data: processing(x, "all", len(data))

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

В замыкании однажды инициализируются переменные, которые затем без накладных расходов можно использовать.

Научимся оформлять замыкания:

Def multiplier(n): "multiplier(n) возвращает функцию, умножающую на n" def mul(k): return n*k return mul # того же эффекта можно добиться выражением # multiplier = lambda n: lambda k: n*k mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например, mul2(5) == 10

Заключение

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

3.2.3 Dictionary Comprehensions

Say we have a dictionary the keys of which are characters and the values of which map to the number of times that character appears in some text. The dictionary currently distinguishes between upper and lower case characters.

We require a dictionary in which the occurrences of upper and lower case characters are combined:

dct = { "a" : 10 , "b" : 34 , "A" : 7 , "Z" : 3 }

frequency = { k . lower () : dct . get (k . lower () , 0 ) + dct . get (k . upper () , 0 )

for k in dct . keys () }

print frequency # {"a": 17, "z": 3, "b": 34}

Python supports the creation of anonymous functions (i.e. functions that are not bound to a name) at runtime, using a construct called “lambda”. This is not exactly the same as lambda in functional programming languages, but it is a very powerful concept that’s well integrated into Python and is often used in conjunction with typical functional concepts like filter() , map() and reduce() .

Anonymous functions in the form of an expression can be created using the lambda
statement:

args is a comma-separated list of arguments, and expression is an expression involving those arguments. This piece of code shows the difference between a normal function definition and a lambda function:

def function (x ) :

return x * x

print function (2 ) # 4

#-----------------------#

function = lambda x : x * x

print function (2 ) # 4

As you can see, both function() do exactly the same and can be used in the same ways. Note that the lambda definition does not include a “return” statement - it always contains an expression which is returned. Also note that you can put a lambda definition anywhere a function is expected, and you don’t have to assign it to a variable at all.

The following code fragments demonstrate the use of lambda functions.

def increment (n ) :

return lambda x : x + n

print increment (2 ) # at 0x022B9530>

print increment (2 ) (20 ) # 22

The above code defines a function increment that creates an anonymous function on the fly and returns it. The returned function increments its argument by the value that was specified when it was created.

You can now create multiple different increment functions and assign them to variables, then use them independent from each other. As the last statement demonstrates, you don’t even have to assign the function anywhere - you can just use it instantly and forget it when it’s not needed anymore.

Q3. What is lambda good for?
Ans.
The answer is:

  • We don’t need lambda, we could get along all right without it. But…
  • there are certain situations where it is convenient - it makes writing code a bit easier, and the written code a bit cleaner.

Q4. What kind of situations?

Well, situations in which we need a simple one-off function: a function that is going to be used only once.

Normally, functions are created for one of two purposes: (a) to reduce code duplication, or (b) to modularize code.

  • If your application contains duplicate chunks of code in various places, then you can put one copy of that code into a function, give the function a name, and then - using that function name - call it from various places in your code.
  • If you have a chunk of code that performs one well-defined operation - but is really long and gnarly and interrupts the otherwise readable flow of your program - then you can pull that long gnarly code out and put it into a function all by itself.

But suppose you need to create a function that is going to be used only once - called from only one place in your application. Well, first of all, you don’t need to give the function a name. It can be “anonymous”. And you can just define it right in the place where you want to use it. That’s where lambda is useful.

Typically, lambda is used in the context of some other operation, such as sorting or a data reduction:

names = [ "David Beazley" , "Brian Jones" , "Raymond Hettinger" , "Ned Batchelder" ]

print sorted (names , key = lambda name : name . split () [ - 1 ] . lower () )

# ["Ned Batchelder", "David Beazley", "Raymond Hettinger", "Brian Jones"]

Although lambda allows you to define a simple function, its use is highly restricted. In
particular, only a single expression can be specified, the result of which is the return
value. This means that no other language features, including multiple statements, conditionals, iteration, and exception handling, can be included.
You can quite happily write a lot of Python code without ever using lambda. However,
you’ll occasionally encounter it in programs where someone is writing a lot of tiny
functions that evaluate various expressions, or in programs that require users to supply
callback functions.

You’ve defined an anonymous function using lambda, but you also need to capture the
values of certain variables at the time of definition.

>>> x = 10

>>> a = lambda y : x + y

>>> x = 20

>>> b = lambda y : x + y

Now ask yourself a question. What are the values of a(10) and b(10)? If you think the
results might be 20 and 30, you would be wrong:

The problem here is that the value of x used in the lambda expression is a free variable
that gets bound at runtime, not definition time. Thus, the value of x in the lambda
expressions is whatever the value of the x variable happens to be at the time of execution.
For example:

If you want an anonymous function to capture a value at the point of definition and
keep it, include the value as a default value, like this:

The problem addressed here is something that tends to come up in code that
tries to be just a little bit too clever with the use of lambda functions. For example,
creating a list of lambda expressions using a list comprehension or in a loop of some kind and expecting the lambda functions to remember the iteration variable at the time of definition. For example:

>>> funcs = [ lambda x : x + n for n in range (5 ) ]

  • Перевод

Рассуждая о функциональном программировании, люди часто начинают выдавать кучу «функциональных» характеристик. Неизменяемые данные, функции первого класса и оптимизация хвостовой рекурсии. Это свойства языка, помогающие писать функциональные программы. Они упоминают мапирование, каррирование и использование функций высшего порядка. Это приёмы программирования, использующиеся для написания функционального кода. Они упоминают распараллеливание, ленивые вычисления и детерменизм. Это преимущества функциональных программ.

Забейте. Функциональный код отличается одним свойством: отсутствием побочных эффектов. Он не полагается на данные вне текущей функции, и не меняет данные, находящиеся вне функции. Все остальные «свойства» можно вывести из этого.

Нефункциональная функция:

A = 0 def increment1(): global a a += 1

Функциональная функция:

Def increment2(a): return a + 1

Вместо проходов по списку используйте map и reduce

Map

Принимает функцию и набор данных. Создаёт новую коллекцию, выполняет функцию на каждой позиции данных и добавляет возвращаемое значение в новую коллекцию. Возвращает новую коллекцию.

Простой map, принимающий список имён и возвращающий список длин:

Name_lengths = map(len, ["Маша", "Петя", "Вася"]) print name_lengths # =>

Этот map возводит в квадрат каждый элемент:

Squares = map(lambda x: x * x, ) print squares # =>

Он не принимает именованную функцию, а берёт анонимную, определённую через lambda. Параметры lambda определены слева от двоеточия. Тело функции – справа. Результат возвращается неявным образом.

Нефункциональный код в следующем примере принимает список имён и заменяет их случайными прозвищами.

Import random names = ["Маша", "Петя", "Вася"] code_names = ["Шпунтик", "Винтик", "Фунтик"] for i in range(len(names)): names[i] = random.choice(code_names) print names # => ["Шпунтик", "Винтик", "Шпунтик"]

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

Перепишем это через map:

Import random names = ["Маша", "Петя", "Вася"] secret_names = map(lambda x: random.choice(["Шпунтик", "Винтик", "Фунтик"]), names)

Упражнение 1 . Попробуйте переписать следующий код через map. Он принимает список реальных имён и заменяет их прозвищами, используя более надёжный метод.

Names = ["Маша", "Петя", "Вася"] for i in range(len(names)): names[i] = hash(names[i]) print names # =>

Моё решение:

names = ["Маша", "Петя", "Вася"] secret_names = map(hash, names)

Reduce

Reduce принимает функцию и набор пунктов. Возвращает значение, получаемое комбинированием всех пунктов.

Пример простого reduce. Возвращает сумму всех пунктов в наборе:

Sum = reduce(lambda a, x: a + x, ) print sum # => 10

X – текущий пункт, а – аккумулятор. Это значение, которое возвращает выполнение lambda на предыдущем пункте. reduce() перебирает все значения, и запускает для каждого lambda на текущих значениях а и х, и возвращает результат в а для следующей итерации.

А чему равно а в первой итерации? Оно равно первому элементу коллекции, и reduce() начинает работать со второго элемента. То есть, первый х будет равен второму предмету набора.

Следующий пример считает, как часто слово «капитан» встречается в списке строк:

Sentences = ["капитан джек воробей", "капитан дальнего плавания", "ваша лодка готова, капитан"] cap_count = 0 for sentence in sentences: cap_count += sentence.count("капитан") print cap_count # => 3

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

Sentences = ["капитан джек воробей", "капитан дальнего плавания", "ваша лодка готова, капитан"] cap_count = reduce(lambda a, x: a + x.count("капитан"), sentences, 0)

А откуда здесь берётся начальное значение а? Оно не может быть вычислено из количества повторений в первой строке. Поэтому оно задаётся как третий аргумент функции reduce().

Почему map и reduce лучше?

Во-первых, они обычно укладываются в одну строку.

Во-вторых, важные части итерации,– коллекция, операция и возвращаемое значение,– всегда находятся в одном месте map и reduce.

В-третьих, код в цикле может изменить значение ранее определённых переменных, или влиять на код, находящийся после него. По соглашению, map и reduce – функциональны.

В-четвёртых, map и reduce – элементарные операции. Вместо построчного чтения циклов читателю проще воспринимать map и reduce, встроенные в сложные алгоритмы.

В-пятых, у них есть много друзей, позволяющих полезное, слегка изменённое поведение этих функций. Например, filter, all, any и find.

Упражнение 2 : перепишите следующий код, используя map, reduce и filter. Filter принимает функцию и коллекцию. Возвращает коллекцию тех вещей, для которых функция возвращает True.

People = [{"имя": "Маша", "рост": 160}, {" рост ": "Саша", " рост ": 80}, {"name": "Паша"}] height_total = 0 height_count = 0 for person in people: if "рост" in person: height_total += person[" рост "] height_count += 1 if height_count > 0: average_height = height_total / height_count print average_height # => 120

Моё решение:

people = [{"имя": "Маша", "рост": 160}, {" рост ": "Саша", " рост ": 80}, {"name": "Паша"}] heights = map(lambda x: x["рост"], filter(lambda x: "рост" in x, people)) if len(heights) > 0: from operator import add average_height = reduce(add, heights) / len(heights)

Пишите декларативно, а не императивно

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

Примеры вывода:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Текст программы:

From random import random time = 5 car_positions = while time: # decrease time time -= 1 print "" for i in range(len(car_positions)): # move car if random() > 0.3: car_positions[i] += 1 # draw car print "-" * car_positions[i]

Код императивен. Функциональная версия была бы декларативной – она бы описывала, что нужно сделать, а не то, как это надо сделать.

Используем функции

Декларативности можно достичь, вставляя код в функции:

From random import random def move_cars(): for i, _ in enumerate(car_positions): if random() > 0.3: car_positions[i] += 1 def draw_car(car_position): print "-" * car_position def run_step_of_race(): global time time -= 1 move_cars() def draw(): print "" for car_position in car_positions: draw_car(car_position) time = 5 car_positions = while time: run_step_of_race() draw()

Для понимания программы читатель просматривает основной цикл. «Если осталось время, пройдём один шаг гонки и выведем результат. Снова проверим время». Если читателю надо будет разобраться, как работает шаг гонки, он сможет прочесть его код отдельно.

Комментарии не нужны, код объясняет сам себя.

Разбиение кода на функции делает код более читаемым. Этот приём использует функции, но лишь как подпрограммы. Они упаковывают код, но не делают его функциональным. Функции влияют на окружающий их код и меняют глобальные переменные, а не возвращают значения. Если читатель встречает переменную, ему потребуется найти, откуда она взялась.

Вот функциональная версия этой программы:

From random import random def move_cars(car_positions): return map(lambda x: x + 1 if random() > 0.3 else x, car_positions) def output_car(car_position): return "-" * car_position def run_step_of_race(state): return {"time": state["time"] - 1, "car_positions": move_cars(state["car_positions"])} def draw(state): print "" print "\n".join(map(output_car, state["car_positions"])) def race(state): draw(state) if state["time"]: race(run_step_of_race(state)) race({"time": 5, "car_positions": })

Теперь код разбит на функциональные функции. Тому есть три признака. Первый – нет расшаренных переменных. time и car_positions передаются прямиком в race(). Второе – функции принимают параметры. Третье – переменные не меняются внутри функций, все значения возвращаются. Каждый раз, когда run_step_of_race() проделывает следующий шаг, он передаётся опять в следующий.

Вот вам две функции zero() и one():

Def zero(s): if s == "0": return s def one(s): if s == "1": return s

Zero() принимает строку s. Если первый символ – 0, то возвращает остаток строки. Если нет – тогда None. one() делает то же самое, если первый символ – 1.

Представим функцию rule_sequence(). Она принимает строку и список из функций-правил, состоящий из функций zero и one. Она вызывает первое правило, передавая ему строку. Если не возвращено None, то берёт возвращённое значение и вызывает следующее правило. И так далее. Если возвращается None, rule_sequence() останавливается и возвращает None. Иначе – значение последнего правила.

Примеры входных и выходных данных:

Print rule_sequence("0101", ) # => 1 print rule_sequence("0101", ) # => None

Императивная версия rule_sequence():

Def rule_sequence(s, rules): for rule in rules: s = rule(s) if s == None: break return s

Упражнение 3 . Этот код использует цикл. Перепишите его в декларативном виде с использованием рекурсии.

Моё решение:

def rule_sequence(s, rules): if s == None or not rules: return s else: return rule_sequence(rules(s), rules)

Используйте конвейеры (pipelines)

Теперь перепишем другой вид циклов при помощи приёма под названием конвейер.

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

Bands = [{"name": "sunset rubdown", "country": "UK", "active": False}, {"name": "women", "country": "Germany", "active": False}, {"name": "a silver mt. zion", "country": "Spain", "active": True}] def format_bands(bands): for band in bands: band["country"] = "Canada" band["name"] = band["name"].replace(".", "") band["name"] = band["name"].title() format_bands(bands) print bands # => [{"name": "Sunset Rubdown", "active": False, "country": "Canada"}, # {"name": "Women", "active": False, "country": "Canada" }, # {"name": "A Silver Mt Zion", "active": True, "country": "Canada"}]

Название функции «format» слишком общее. И вообще, код вызывает некоторое беспокойство. В одном цикле происходят три разные вещи. Значение ключа "country" меняется на "Canada". Убираются точки и первая буква имени меняется на заглавную. Сложно понять, что код должен делать, и сложно сказать, делает ли он это. Его тяжело использовать, тестировать и распараллеливать.

Сравните:

Print pipeline_each(bands, )

Всё просто. Вспомогательные функции выглядят функциональными, потому что они связаны в цепочку. Выход предыдущей – вход следующей. Их просто проверить, использовать повторно, проверять и распараллеливать.

Pipeline_each() перебирает группы по одной, и передаёт их функциям преобразования, вроде set_canada_as_country(). После применения функции ко всем группам, pipeline_each() делает из них список и передаёт следующей.

Посмотрим на функции преобразования.

Def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d = value return d def set_canada_as_country(band): return assoc(band, "country", "Canada") def strip_punctuation_from_name(band): return assoc(band, "name", band["name"].replace(".", "")) def capitalize_names(band): return assoc(band, "name", band["name"].title())

Каждая связывает ключ группы с новым значением. Без изменения оригинальных данных это тяжело сделать, поэтому мы решаем это с помощью assoc(). Она использует deepcopy() для создания копии переданного словаря. Каждая функция преобразовывает копию и возвращает эту копию.

Всё вроде как нормально. Оригиналы данных защищены от изменений. Но в коде есть два потенциальных места для изменений данных. В strip_punctuation_from_name() создаётся имя без точек через вызов calling replace() с оригинальным именем. В capitalize_names() создаётся имя с первой прописной буквой на основе title() и оригинального имени. Если replace и time не функциональны, то и strip_punctuation_from_name() с capitalize_names() не функциональны.

К счастью, они функциональны. В Python строки неизменяемы. Эти функции работают с копиями строк. Уфф, слава богу.

Такой контраст между строками и словарями (их изменяемостью) в Python демонстрирует преимущества языков типа Clojure. Там программисту не надо думать, не изменит ли он данные. Не изменит.

Упражнение 4 . Попробуйте сделать функцию pipeline_each. Задумайтесь над последовательностью операций. Группы – в массиве, передаются по одной для первой функции преобразования. Затем полученный массив передаётся по одной штучке для второй функции, и так далее.

Моё решение:

def pipeline_each(data, fns): return reduce(lambda a, x: map(x, a), fns, data)

Все три функции преобразования в результате меняют конкретное поле у группы. call() можно использовать, чтобы создать абстракцию для этого. Она принимает функцию и ключ, к которому она будет применена.

Set_canada_as_country = call(lambda x: "Canada", "country") strip_punctuation_from_name = call(lambda x: x.replace(".", ""), "name") capitalize_names = call(str.title, "name") print pipeline_each(bands, )

Или, жертвуя читаемостью:

Print pipeline_each(bands, )

Код для call():

Def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d = value return d def call(fn, key): def apply_fn(record): return assoc(record, key, fn(record.get(key))) return apply_fn

Что тут у нас происходит.

Один. call – функция высшего порядка, т.к. принимает другую функцию как аргумент и возвращает функцию.

Два. apply_fn() похожа на функции преобразования. Получает запись (группу). Ищет значение record. Вызывает fn. Присваивает результат в копию записи и возвращает её.

Три. call сам ничего не делает. Всю работу делает apply_fn(). В примере использования pipeline_each(), один экземпляр apply_fn() задаёт "country" значение "Canada". Другой – делает первую букву прописной.

Четыре. При выполнении экземпляра apply_fn() функции fn и key не будут доступны в области видимости. Это не аргументы apply_fn() и не локальные переменные. Но доступ к ним будет. При определении функции она сохраняет ссылки на переменные, которые она замыкает – те, что были определены снаружи функции, и используются внутри. При запуске функции переменные ищутся среди локальных, затем среди аргументов, а затем среди ссылок на замкнутые. Там и найдутся fn и key.

Пять. В call нет упоминания групп. Это оттого, что call можно использовать для создания любых конвейеров, независимо от их содержимого. Функциональное программирование, в частности, строит библиотеку общих функций, пригодных для композиций и для повторного использования.

Молодцом. Замыкания, функции высшего порядка и область видимости – всё в нескольких параграфах. Можно и чайку с печеньками выпить.

Остаётся ещё одна обработка данных групп. Убрать всё, кроме имени и страны. Функция extract_name_and_country():

Def extract_name_and_country(band): plucked_band = {} plucked_band["name"] = band["name"] plucked_band["country"] = band["country"] return plucked_band print pipeline_each(bands, ) # => [{"name": "Sunset Rubdown", "country": "Canada"}, # {"name": "Women", "country": "Canada"}, # {"name": "A Silver Mt Zion", "country": "Canada"}]

Extract_name_and_country() можно было бы написать в обобщённом виде под названием pluck(). Использовалась бы она так:

Print pipeline_each(bands, )])

Упражнение 5 . pluck принимает список ключей, которые надо извлечь из записей. Попробуйте её написать. Это буде функция высшего порядка.

2010-11-17 09:47

Функции map, zip и лямбда (кстати говоря называются "функции высшего порядка" или "first-class-functions") позволяют достаточно просто выполнять различные манипуляции с данными, для чего в "обычном" процедурном стиле приходится писать немного больше кода. Все ниженаписанное относится к так называемому функциональному программированию , луркайте подробности.

Функции map, zip и lambda в примерах.

Простая задача есть список a = и список b = одинаковой длины и нужно слить их парами. Проще простого - используя функцию zip :

a = [ 1 , 2 ] b = [ 3 , 4 ] print zip (a , b ) [(1 , 3 ), (2 , 4 )]

или тройками:

a = [ 1 , 2 ] b = [ 3 , 4 ] c = [ 5 , 6 ] print zip (a , b , c ) [(1 , 3 , 5 ), (2 , 4 , 6 )]

или в более общем виде

list = [ a , b , c ] print zip (* list ) [(1 , 3 , 5 ), (2 , 4 , 6 )]

Звездочка * перед list как-бы говорит что передается список аргументов, т.е. Действовать эквивалентно тому как если бы передали a, b, c т.е. Можно даже так print zip(*) результат не изменится.

def f (x ): return x * x nums = [ 1 , 2 , 3 ] for num in nums : print f (num )

Более опытный нуб изучивший list comprehensions:

def f (x ): return x * x print [ f (num ) for num in nums ]

Программист сделает проще:

def f (x ): return x * x print map (f , nums )

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

print map (lambda x : x * x , nums )

Последняя запись являет собой пример наиболее грамотного подхода. Дело в том, что когда человек пишет код как стихи, в порыве вдохновения (что другими словами можно назвать "в диком угаре"), крайне роляет скорость написания (отсюда растут корни трепетной любви многих девелоперов к простым текстовым редакторм vim, emacs, sublimetext), а сильная сторона питона как раз в размере генерируемого кода - он очень компактный. Написать одну строчку естественно быстрее чем 7, да и читать короткий код проще, однако написание подобного кода требует определенного навыка. Другая сторона медали – иногда в этом "диком угаре" пишут в одну строчку целые последовательности достаточно сложных действий, да так что очень трудно понять что там происходит и что получается в конечном итоге.

Из примера понятно, что map применяет какую-либо функцию к списку и возвращает результат опять же в виде списка. Вы можете передать несколько списков, тогда функция (идущая первым параметром) должна принимать несколько аргументов (по количеству списков переданных в map ).

def f (x , y ): return x * y a = [ 1 , 3 , 4 ] b = [ 3 , 4 , 5 ] print map (f , a , b ) [ 3 , 12 , 20 ]

Классно, правда?

Однако если списки разной длины, т.е. Один короче другого, то он будет дополнен значениями None до нужной длины. Если убрать из списка b последнее значение – пример не будет работать, т.к. В функции f произойдет попытка умножения числа на None, и питоне не позволяет это делать, что кстати выгодно отличает его от php, который в подобной ситуации работал бы дальше. Поэтому если функция f достаточно объемна, неплохо бы проверять передаваемые значения. Например;

Если же заместо функции стоит None – то map действует примерно так же как и zip , но если передаваемые списки разной длины в результат будет писаться None – что кстати очень уместно в некоторых моментах.

a = [ 1 , 3 , 4 ] b = [ 3 , 4 ] print map (None , a , b ) [(1 , 3 ), (3 , 4 ), (4 , None )]

Теперь про лямбда функции в python . Они используются когда вам необходимо определить функцию без исподьзования def func_name(): ..., ведь часто (как в предыдущих примерах) функция настолько мала, что определять её отдельно смыла нет (лишние строчки кода, что ухудшение читабельность). Поэтому функцию можно определить “на месте” f = lambda x: x*x как бы говорит нам – принимает x, возвращает x*x

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

def f (x , y ): if (y == None ): y = 1 return x * y

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

lambda x , y : x * (y if y is not None else 1 )

А теперь хорошо бы передавать списки отсортированные по длине – len(a) > (b) – проще простого - воспользуемся функцией sorted :

sorted ([ a , b ], key = lambda x : len (x ), reverse = True )

фунция sorted принимает список значений ( = [,]) и сортирует по ключу key – который у нас задан функцией len(x) - возвращающей длину списка, сортируем в порядке убывания (reverse=True)

В конечном итоге вся операция записывается таким образом:

map (lambda x , y : x * (y if y is not None else 1 ), * sorted ([ a , b ], key = lambda x : len (x ), reverse = True ))

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




Top