Введение в функциональное программирование на Python. Языки: Почти все

25.04.2019 Android

  • Перевод

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

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

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

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 принимает список ключей, которые надо извлечь из записей. Попробуйте её написать. Это буде функция высшего порядка.

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 ) ]

Существует большое количество публикаций, посвящённых реализациям концепций функционального программирования на языке Python, но большая часть этих материалов написана одним автором - Девидом Мертцом (David Mertz). Кроме того, многие из этих статей уже устарели и разнесены по различным сетевым ресурсам. В этой статье мы попробуем снова обратиться к этой теме, чтобы освежить и упорядочить доступную информацию, особенно учитывая большие различия, имеющиеся между версиями Python линии 2 и линии 3.

Функции в Python

Функции в Python определяются 2-мя способами: через определение def или через анонимное описание lambda . Оба этих способа определения доступны, в той или иной степени, и в некоторых других языках программирования. Особенностью Python является то, что функция является таким же именованным объектом, как и любой другой объект некоторого типа данных, скажем, как целочисленная переменная. В листинге 1 представлен простейший пример (файл func.py из архива python_functional.tgz

Листинг 1. Определения функций
#!/usr/bin/python # -*- coding: utf-8 -*- import sys def show(fun, arg): print("{} : {}".format(type(fun), fun)) print("arg={} => fun(arg)={}".format(arg, fun(arg))) if len(sys.argv) > 1: n = float(sys.argv[ 1 ]) else: n = float(input("число?: ")) def pow3(n): # 1-е определение функции return n * n * n show(pow3, n) pow3 = lambda n: n * n * n # 2-е определение функции с тем же именем show(pow3, n) show((lambda n: n * n * n), n) # 3-е, использование анонимного описание функции

При вызове всех трёх объектов-функций мы получим один и тот же результат:

$ python func.py 1.3 : arg=1.3 => fun(arg)=2.197 : at 0xb7662bc4> arg=1.3 => fun(arg)=2.197 : at 0xb7662844> arg=1.3 => fun(arg)=2.197

Ещё более отчётливо это проявляется в Python версии 3, в которой всё является классами (в том числе, и целочисленная переменная), а функции являются объектами программы, принадлежащими к классу function :

$ python3 func.py 1.3 : arg=1.3 => fun(arg)=2.1970000000000005 : at 0xb745432c> arg=1.3 => fun(arg)=2.1970000000000005 : at 0xb74542ec> arg=1.3 => fun(arg)=2.1970000000000005

Примечание . Существуют ещё 2 типа объектов, допускающих функциональный вызов - функциональный метод класса и функтор, о которых мы поговорим позже.

Если функциональные объекты Python являются такими же объектами, как и другие объекты данных, значит, с ними можно и делать всё то, что можно делать с любыми данными:

  • динамически изменять в ходе выполнения;
  • встраивать в более сложные структуры данных (коллекции);
  • передавать в качестве параметров и возвращаемых значений и т.д.

На этом (манипуляции с функциональными объектами как с объектами данных) и базируется функциональное программирование. Python, конечно, не является настоящим языком функционального программирования, так, для полностью функционального программирования существуют специальные языки: Lisp, Planner, а из более свежих: Scala, Haskell. Ocaml, ... Но в Python можно "встраивать" приёмы функционального программирования в общий поток императивного (командного) кода, например, использовать методы, заимствованные из полноценных функциональных языков. Т.е. "сворачивать" отдельные фрагменты императивного кода (иногда достаточно большого объёма) в функциональные выражения.

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

Достаточно часто при программировании на Python используют типичные конструкции из области функционального программирования, например:

print ([ (x,y) for x in (1, 2, 3, 4, 5) \ for y in (20, 15, 10) \ if x * y > 25 and x + y < 25 ])

В результате запуска получаем:

$ python funcp.py [(2,20), (2,15), (3,20), (3,15), (3,10), (4,20), (4,15), (4,10), (5,15), (5,10)]

Функции как объекты

Создавая объект функции оператором lambda , как было показано в листинге 1, можно привязать созданный функциональный объект к имени pow3 в точности так же, как можно было бы привязать к этому имени число 123 или строку "Hello!" . Этот пример подтверждает статус функций как объектов первого класса в Python. Функция в Python - это всего лишь ещё одно значение, с которым можно что-то сделать.

Наиболее частое действие, выполняемое с функциональными объектами первого класса, - это передача их во встроенные функции высшего порядка: map() , reduce() и filter() . Каждая из этих функций принимает объект функции в качестве своего первого аргумента.

  • map() применяет переданную функцию к каждому элементу в переданном списке (списках) и возвращает список результатов (той же размерности, что и входной);
  • reduce() применяет переданную функцию к каждому значению в списке и ко внутреннему накопителю результата, например, reduce(lambda n,m: n * m, range(1, 10)) означает 10! (факториал);
  • filter() применяет переданную функцию к каждому элементу списка и возвращает список тех элементов исходного списка, для которых переданная функция вернула значение истинности.

Комбинируя эти три функции, можно реализовать неожиданно широкий диапазон операций потока управления, не прибегая к императивным утверждениям, а используя лишь выражения в функциональном стиле, как показано в листинге 2 (файл funcH.py из архива python_functional.tgz

Листинг 2. Функции высших порядков Python
#!/usr/bin/python # -*- coding: utf-8 -*- import sys def input_arg(): global arg arg = (lambda: (len(sys.argv) > 1 and int(sys.argv[ 1 ])) or \ int(input("число?: ")))() return arg print("аргумент = {}".format(input_arg())) print(list(map(lambda x: x + 1, range(arg)))) print(list(filter(lambda x: x > 4, range(arg)))) import functools print("{}! = {}".format(arg, functools.reduce(lambda x, y: x * y, range(1, arg))))

Примечание. Этот код несколько усложнён по сравнению с предыдущим примером из-за следующих аспектов, связанных с совместимостью Python версий 2 и 3:

  • Функция reduce() , объявленная как встроенная в Python 2, в Python 3 была вынесена в модуль functools и её прямой вызов по имени вызовет исключение NameError , поэтому для корректной работы вызов должен быть оформлен как в примере или включать строку: from functools import *
  • Функции map() и filter() в Python 3 возвращают не список (что уже показывалось при обсуждении различий версий), а объекты-итераторы вида:

Для получения всего списка значений для них вызывается функция list() .

Поэтому такой код сможет работать в обеих версиях Python:

$ python3 funcH.py 7 аргумент = 7 7! = 720

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

Рекурсия

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

В некоторых обсуждениях по Python неоднократно приходилось встречаться с заявлениями, что в Python глубина рекурсии ограничена "аппаратно", и поэтому некоторые действия реализовать невозможно в принципе. В интерпретаторе Python действительно по умолчанию установлено ограничение глубины рекурсии, равным 1000, но это численный параметр, который всегда можно переустановить, как показано в листинге 3 (полный код примера можно найти в файле fact2.py из архива python_functional.tgz

Листинг 3. Вычисление факториала с произвольной глубиной рекурсии
#!/usr/bin/python # -*- coding: utf-8 -*- import sys arg = lambda: (len(sys.argv) > 1 and int(sys.argv[ 1 ])) or \ int(input("число?: ")) factorial = lambda x: ((x == 1) and 1) or x * factorial(x - 1) n = arg() m = sys.getrecursionlimit() if n >= m - 1: sys.setrecursionlimit(n + 2) print("глубина рекурсии превышает установленную в системе {}, переустановлено в {}".\ format(m, sys.getrecursionlimit())) print("n={} => n!={}".format(n, factorial(n))) if sys.getrecursionlimit() > m: print("глубина рекурсии восстановлена в {}".format(m)) sys.setrecursionlimit(m)

Вот как выглядит исполнение этого примера в Python 3 и в Python2 (правда на самом деле полученное число вряд ли поместится на один экран терминала консоли):

$ python3 fact2.py 1001 глубина рекурсии превышает установленную в системе 1000, переустановлено в 1003 n=1001 => n!=4027.................................................0000000000000 глубина рекурсии восстановлена в 1000

Несколько простейших примеров

Выполним несколько простейших трансформаций привычного императивного кода (командного, операторного) для превращения его отдельных фрагментов в функциональные. Сначала заменим операторы ветвления логическими условиями, которые за счёт "отложенных" (lazy, ленивых) вычислений позволяют управлять выполнением или невыполнением отдельных ветвей кода. Так, императивная конструкция:

if <условие>: <выражение 1> else: <выражение 2>

Полностью эквивалентна следующему функциональному фрагменту (за счёт "отложенных" возможностей логических операторов and и or ):

# функция без параметров: lambda: (<условие> and <выражение 1>) or (<выражение 2>)

В качестве примера снова используем вычисление факториала. В листинге 4 приведен функциональный код для вычисления факториала (файл fact1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 4. Операторное (императивное) определение факториала
#!/usr/bin/python # -*- coding: utf-8 -*- import sys def factorial(n): if n == 1: return 1 else: return n * factorial(n - 1) if len(sys.argv) > 1: n = int(sys.argv[ 1 ]) else: n = int(input("число?: ")) print("n={} => n!={}".format(n, factorial(n)))

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

  • определение функции факториала: factorial = lambda x: ((x == 1) and 1) or x * factorial(x - 1)
  • запрос на ввод значения аргумента с консоли терминала: arg = lambda: (len(sys.argv) > 1 and int(sys.argv[ 1 ])) or \ int(input("число?: ")) n = arg()

В файле fact3.py появляется ещё одно определение функции, сделанное через функцию высшего порядка reduсe() :

factorial = factorial = lambda z: reduce(lambda x, y: x * y, range(1, z + 1))

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

n = (lambda: (len(sys.argv) > 1 and int(sys.argv[ 1 ])) or \ int(input("число?: ")))()

Наконец, можно заметить, что присвоение значения переменной n требуется только для её использования в вызове print() для вывода этого значения. Если мы откажемся и от этого ограничения, то всё приложение выродится в один функциональный оператор (см. файл fact4.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

from sys import * from functools import reduce print("вычисленный факториал = {}".format(\ (lambda z: reduce(lambda x, y: x * y, range(1, z + 1))) \ ((lambda: (len(argv) > 1 and int(argv[ 1 ])) or \ int(input("число?: ")))())))

Этот единственный вызов внутри функции print() и представляет всё приложение в его функциональном варианте:

$ python3 fact4.py число?: 5 вычисленный факториал = 120

Читается ли этот код (файл fact4.py) лучше, чем императивная запись (файл fact1.py)? Скорее нет, чем да. В чём же тогда его достоинство? В том, что при любых изменениях окружающего его кода, нормальная работа этого фрагмента сохранится, так как отсутствует риск побочных эффектов из-за изменения значений используемых переменных.

Функции высших порядков

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

Замыкание

Одно из интересных понятий функционального программирования - это замыкания (closure). Эта идея оказалась настолько заманчивой для многих разработчиков, что была реализована даже в некоторых нефункциональных языках программирования (Perl). Девид Мертц приводит следующее определение замыкания: "Замыкание - это процедура вместе с привязанной к ней совокупностью данных" (в противовес объектам в объектном программировании, как: "данные вместе с привязанным к ним совокупностью процедур").

Смысл замыкания состоит в том, что определение функции "замораживает" окружающий её контекст на момент определения . Это может делаться различными способами, например, за счёт параметризации создания функции, как показано в листинге 5 (файл clos1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 5. Создание замыкания
# -*- coding: utf-8 -*- def multiplier(n): # multiplier возвращает функцию умножения на n def mul(k): return n * k return mul mul3 = multiplier(3) # mul3 - функция, умножающая на 3 print(mul3(3), mul3(5))

Вот как срабатывает такая динамически определённая функция:

$ python clos1.py (9, 15) $ python3 clos1.py 9 15

Другой способ создания замыкания - это использование значения параметра по умолчанию в точке определения функции, как показано в листинге 6 (файл clos3.py из архива python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 6. Другой способ создания замыкания
n = 3 def mult(k, mul = n): return mul * k n = 7 print(mult(3)) n = 13 print(mult(5)) n = 10 mult = lambda k, mul=n: mul * k print(mult(3))

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

$ python clos3.py 9 15 30

Частичное применение функции

Частичное применение функции предполагает на основе функции N переменных определение новой функции с меньшим числом переменных M < N , при этом остальные N - M переменных получают фиксированные "замороженные" значения (используется модуль functools ). Подобный пример будет рассмотрен ниже.

Функтор

Функтор - это не функция, а объект класса, в котором определён метод с именем __call__() . При этом, для экземпляра такого объекта может применяться вызов, точно так же, как это происходит для функций. В листинге 7 (файл part.py из архива python_functional.tgz в разделе "Материалы для скачивания") демонстрируется использование замыкания, частичного определения функции и функтора, приводящих к получению одного и того же результата.

Листинг 7. Сравнение замыкания, частичного определения и функтора
# -*- coding: utf-8 -*- def multiplier(n): # замыкания - closure def mul(k): return n * k return mul mul3 = multiplier(3) from functools import partial def mulPart(a, b): # частичное применение функции return a * b par3 = partial(mulPart, 3) class mulFunctor: # эквивалентный функтор def __init__(self, val1): self.val1 = val1 def __call__(self, val2): return self.val1 * val2 fun3 = mulFunctor(3) print("{} . {} . {}".format(mul3(5), par3(5), fun3(5)))

Вызов всех трёх конструкций для аргумента, равного 5, приведёт к получению одинакового результата, хотя при этом и будут использоваться абсолютно разные механизмы:

$ python part.py 15 . 15 . 15

Карринг

Карринг (или каррирование, curring) - преобразование функции от многих переменных в функцию, берущую свои аргументы по одному.

Примечание . Это преобразование было введено М. Шейнфинкелем и Г. Фреге и получило своё название в честь математика Хаскелла Карри, в честь которого также назван и язык программирования Haskell.

Карринг не относится к уникальным особенностям функционального программирования, так карринговое преобразование может быть записано, например, и на языках Perl или C++. Оператор каррирования даже встроен в некоторые языки программирования (ML, Haskell), что позволяет многоместные функции приводить к каррированному представлению. Но все языки, поддерживающие замыкания, позволяют записывать каррированные функции, и Python не является исключением в этом плане.

В листинге 8 представлен простейший пример с использованием карринга (файл curry1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 8. Карринг
# -*- coding: utf-8 -*- def spam(x, y): print("param1={}, param2={}".format(x, y)) spam1 = lambda x: lambda y: spam(x, y) def spam2(x) : def new_spam(y) : return spam(x, y) return new_spam spam1(2)(3) # карринг spam2(2)(3)

Вот как выглядят исполнение этих вызовов:

$ python curry1.py param1=2, param2=3 param1=2, param2=3

Заключение

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

В следующей статье мы обсудим вопросы организации параллельного исполнения кода в среде Python.

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

Какие парадигмы?! Давайте кодить!

Когда тебе надо написать что-то, то ты, наверное, меньше всего заморачиваешься относительно того, какую парадигму программирования выбрать. Скорее, ты либо выбираешь наиболее подходящий язык, либо сразу начинаешь кодить на своем любимом, предпочитаемом и годами проверенном. Оно и верно, пусть об идеологии думают идеологи, наше дело – программить:). И все-таки, программируя, ты обязательно следуешь какой-либо парадигме. Рассмотрим пример. Попробуем написать что-нибудь простое… ну, например, посчитаем площадь круга.

Можно написать так:

Площадь круга (вариант первый)

double area_of_circle(double r) {
return M_PI*pow(r,2);
}
int main() {
double r = 5;
cout << "Площадь: "<< area_of_circle(r)<< endl;
}

А можно и так:

Площадь круга (вариант второй)

class Circle {
double r;
public:
Circle(double r) { this->r = r; }
double area() { return M_PI*pow(this->r,2); }
void print_area() {
cout << "Площадь: "<< this->area() << endl;
}
};
int main() {(new Circle(5))->print_area();}

Можно и по-другому… но только как не старайся, код будет или императивным (как в первом случае), или объектно-ориентированным (как во втором).
Это происходит не из-за отсутствия воображения, а просто потому, что C++ «заточен» под эти парадигмы.

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

Парадигмы

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

Императивное программирование

«Сначала делаем это, потом это, затем вот это»

Языки: Почти все

Абсолютно понятная любому программисту парадигма: «Человек дает набор инструкций машине».
С императивной парадигмы все начинают учить/понимать программирование.

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

«Считаем выражение и используем результат для чего-нибудь еще».

Языки: Haskell, Erlang, F#

Абсолютно непонятная начинающему программисту парадигма. Мы описываем не последовательность состояний (как в императивной парадигме), а последовательность действий.

Объектно-ориентированное программирование

«Обмениваемся сообщениями между объектами, моделируя взаимодействия в реальном мире».

Языки: Почти все

Объектно-ориентированная парадигма со своим появлением прочно вошла в нашу жизнь.
На ООП построены практически все современные бизнес-процессы.

Логическое программирование

«Отвечаем на вопрос поиском решения».

Языки: Prolog

Логическое программирование – довольно специфическая штука, но, в то же время, интересная и интуитивно понятная.
Достаточно простого примера:

{задаем правила}
witch(X) <= burns(X) and female(X).
burns(X) <= wooden(X).
wooden(X) <= floats(X).
floats(X) <= sameweight(duck, X).
{задаем наблюдения}
female(girl).
sameweight(duck,girl).
{задаем вопрос}
? witch(girl).

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

Функциональное программирование противопоставляют императивному.

Императивное программирование подразумевает последовательность изменений состояния программы, а переменные служат для хранения этого состояния.

Функциональное программирование, наоборот, предусматривает последовательность действий над данными. Это сродни математике – мы долго пишем на доске формулу f(x), а потом подставляем x и получаем результат.

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

Двуликий питон

Нет лучшей теории, чем практика, так что давай уже что-нибудь напишем. А еще лучше – напишем на питоне:).
Посчитаем сумму квадратов элементов массива «data» императивно и функционально:

Императивный Питон

data = [...]
sum = 0
for element in a:
sum += element ** 2
print sum

Функциональный Питон

data = [...]
sq = lambda x: x**2
sum = lambda x,y: x+y
print reduce(sum, map(sq, data))

Оба примера на питоне, хотя я и не включил его в список функциональных языков. Это не случайность, поскольку полностью функциональный язык – довольно специфичная и редко используемая штука. Первым функциональным языком был Lisp, но даже он не был полностью функциональным (ставит в тупик, не правда ли?). Полностью функциональные языки используются для всякого рода научных приложений и пока не получили большого распространения.

Но если сами «функционалы» и не получили широкого распространения, то отдельные идеи перекочевали из них в скриптинговые (и не только) языки программирования.
Оказалось, что совершенно необязательно писать полностью функциональный код, достаточно украсить императивный код элементами функционального.

Питон в действии

Оказывается, концепции ФП реализованы в Питоне более чем изящно. Ознакомимся с ними подробнее.

?-исчисления

Lambda исчисления – это математическая концепция, которая подразумевает, что функции могут принимать в качестве аргументов и возвращать другие функции.
Такие функции называются функциями высших порядков. ?-исчисления основываются на двух операциях: аппликация и абстракция.
Я уже привел пример аппликации в предыдущем листинге. Функции map, reduce – это и есть те самые функции высших порядков, которые «апплицируют», или применяют, переданную в качестве аргумента функцию к каждому элементу списка (для map) или каждой последовательной паре элементов списка (для reduce).

Что касается абстракции – здесь наоборот, функции создают новые функции на основе своих аргументов.

Lambda-абстракция

def add(n):
return lambda x: x + n

adds =

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

Замыкание – это определение функции, зависящей от внутреннего состояния другой функции. В нашем примере это lambda x. С помощью этого приема мы делаем что-то похожее на использование глобальных переменных, только на локальном уровне.

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

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

Чистые функции и ленивый компилятор

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

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

Как мы уже выяснили, в функциональной парадигме можно распоряжаться функциями как угодно. Но больше всего выгоды мы получаем, когда пишем «чистые функции». Чистая функция – это функция без побочных эффектов, а значит, она не зависит от своего окружения и не изменяет его состояния.

Применение чистых функций дает нам ряд преимуществ:

  • Во-первых, если функции не зависят от переменных окружения, то мы уменьшаем количество ошибок, связанных с нежелательными значениями этих самых переменных. Вместе с количеством ошибок мы уменьшаем и время отладки программы, да и дебагить такие функции гораздо проще.
  • Во-вторых, если функции независимы, то компилятору есть, где разгуляться. Если функция зависит только от аргументов, то ее можно посчитать только один раз. В следующие разы можно использовать кэшированное значение. Также, если функции не зависят друг от друга, их можно менять местами и даже автоматически распараллеливать.

Для увеличения производительности в ФП также используются ленивые вычисления. Яркий пример:

print length()

По идее, на выходе мы должны получить ошибку деления на ноль. Но ленивый компилятор питона просто не станет вычислять значения каждого элемента списка, так как его об этом не просили. Нужна длина списка – пожалуйста!
Те же принципы используются и для других языковых конструкций.

В результате несколько «степеней свободы» получает не только программист, но и компилятор.

Списочные выражения и условные операторы

Чтобы жизнь (и программирование) не казались тебе медом, разработчики питона придумали специальный «подслащающий» синтаксис, который буржуи так и называют – «syntactic sugar».
Он позволяет избавиться от условных операторов и циклов… ну, если не избавиться, то уж точно свести к минимуму.

В принципе, ты его уже видел в предыдущем примере – это adds = . Здесь мы сразу создаем и инициализируем список значениями функций. Удобно, правда?
Еще есть такая штука, как операторы and и or, которые позволяют обходиться без громоздких конструкций типа if-elif-else.

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

Императивный код

L =
for x in xrange(10):
if x % 2 == 0:
if x**2>=50:
L.append(x)
else:
L.append(-x)
print L

Функциональный код

print

Итоги

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

Что ж, ООП – это, фактически, надстройка над императивной парадигмой, и если ты перешел от ИП к ООП, то следующим шагом должно быть применение ФП в ООП. В заключение скажу пару слов об уровне абстракции. Так вот, чем он выше – тем лучше и именно сочетание ООП и ФП дает нам этот уровень.

CD

На диск я положил свежие дистрибутивы питона для виндусоидов. Линуксоидам помощь не нужна:).

WWW

Несколько хороших ресурсов для тех, кому хочется узнать больше:

INFO

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

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 могут быть разной длины и передаваться в каком угодно порядке. Лямбда-выражения удобны для определения не очень сложных функций, которые передаются затем другим функциям.