Текст
                    рттясл ।

функциональное программирование на РУТНОМ в примерах сообщества Маск ОусгНоуу Издательство АСТ г. Москва
УДК 004-43 ББК 32.973 М52 В издании использованы материалы из книги «РипсНопа! Рго§гатт1п§ /л РуПзоп», которая распространяется по лицензии СгеаСме Сот.топз АМлРиНоп-ЗИагеАНке 4.01п(етаРопа1 (к1ар://сгеаЦуесоттоп5.ог^/Псеп5е5/Ьу-5а/4.0/) и доступна по ссылке 1Шр5://агс11^е.ог§/деСаП5/]:ипсИопа1 рго§гаттт§ руИ1оп/тос1е/2ир/. Мерц, Дэвид. М52 Функциональное программирование на Ру1Ноп / Д. Мерц, Л. Клаймен. — Москва : Издательство АСТ, 2026. — 128 с ; ил. — (Программирование. Оффер на миллион). 18БЫ 978-5-17-178630-4. Перед вами практическое руководство, посвященное функциональному программированию' на Рутбоп — подходу, который помогает писать более пред- сказуемый, модульный и устойчивый к ошибкам код. Книга шаг за шагом зна- комит читателя с основными концепциями функционального стиля от базовых принципов и встроенных средств языка до продвинутых приемов, применяемых в современных проектах. Вы узнаете, как использовать функции высш'его порядка, замыкания, деко- раторы и ленивые вычисления для построения гибких программных решений Подробно рассматриваются механизмы итераторов и генераторов, принципы композиции функций, ооганизация пайплэйноь обработки данных и применение асинхронного функционального программирования. Особое внимание уделено темам мемоизации, конкурентности и интеграции с популярными инструментами, такими как кегГоо15, ТипсГоо1з, орегаСог, азупсю и Рубапбс Б книге подробно рассказывается, как сочетать лаконичность и выразитель- ность функционального стиля с возможностями РуГбоп, избегая излишней импе- ративности и улучшая читаемость кода. Приведенные примеры и практические рекомендации помогут не только глубже понять теорию функционального про- граммирования, но и сразу применить ее на практике — при проектировании, оптимизации и тестировании приложений. Издание предназначено для разработчиков, стремящихся выйти за рамки традиционного объектно-ориентированного подхода и освоить функциональную парадигму, чтобы писать более элегантный, надежный и современный Ру1Нсп-код. УДК 004.43 ЬБК 32.973 15ЧЫ 978-5-17-178630-4 © Оформление. ООО «Интеджерч, 2025 © ООО «Издательство Л СТ», 2026
Содержание Предисловие................................................... 5 Что такое функциональное программирование (Гипсйопа! ргойгаппп1п§, ГР)9.5 За пределами стандартной библиотеки («(апДатс! Шпату)........... 6 Стилевое примечание............................................ 7 (Избегая) управление потоком выполнения (Е1оиг Сотто!)..............8 Инкапсуляция (ЕпсарзЫайоп)...................................... 8 Вложения (сотртейепзюпз)........................................ 9 Рекурсия (Кесигаоп).............................................11 Вызываемые объекты (Са11аЫе$)......................................17 Именованные функции и лямбды....................................18 Замыкания и вызываемые экземпляры.............................. 19 Методы классов (МеШойз о! Оаззез).............................. 21 Множественная диспетчеризация...................................24 Ленивые вычисления (1аху еуа!иайоп)................................28 Протокол итератора (йетаГот ргогосо!)...........................29 Модуль йеПоок...................................................31 Функции высшего порядка (Н1§йег О г Дет Гипсйопз)..................34 Утилитарные функции высшего порядка (ПШйу Н^йет-Отйет Гппсйопз).35 Модуль орегаЮг (Тпе орегаГог МобШе).............................36 Модуль ГипсЮок (Тйе ГипсюоЕ Мойик)..............................37 Декораторы ШесогаГотз)..........................................37 Композиция функций и пайплайпы................................... 39 Основы композиции функций.......................................39 Функциональная реализация ртре-оператора...................... 41 Продвинутые техники композиции.............................. 43 Обработка ошибок в пайплайнах...................................49 Асинхронные пайплайны...........................................52 Асинхронное функциональное программирование........................55 Теоретические основы асинхронного функционального программирования......55 Основы азупс/гдсап в функциональном стиле................. 56 Параллельная обработка и конкурентность........................ 58 Композиция асинхронных функций................................ 61
Обработка потоков данных с азупсю............................65 Обработка ошибок в асинхронном контексте______________________71 Мемоизация и кеширование.................................... 75 Основы мемсизации и ее преимущества..........................75 Создание собственных кеширующих декораторов............ 81 Кеширование с учетом типов данных............................85 Оптимизация рекурсивных алгоритмов через мемоизацию..........87 Параллельное программирование в функциональном стиле............91 Основы функционального параллельного программирования........91 Модуль сопсштеШ.ГШигез и функциональные паттерны.............93 Мар-Кебисе операции в функциональном стиле................. 96 Валидация данных функциональными методами................... 102 Основы функциональной валидации.............................102 Композиция валидаторов......................................105 Цепочки проверок и монадическая композиция..................108 Интеграция с Рубапйс в функциональном стиле.................112 Функциональное тестирование....................................116 Основы функционального тестирования.........................116 РгореПу-Ьазеб тестирование с Нуротйезтз.....................118 Генерация тестовых данных функциональными методами..........121 Заключение............................................ 126
Предисловие Что такое функциональное программирование (Типс1юпа1 ргодгатгтпд, РР)? Начнем, пожалуй, с самого трудного вопроса: что же такое функциональное программирование (1’ипсбопа1рго§гаттт§, РР)? Один из ответов — это то, что вы делаете, когда программируете на таких языках, как Ызр, Зсйете, С1ощге, 8са1а, НазкеИ, МЬ, ОСат1, Ег1ап§ и некоторых других. Это верный ответ, но он мало что проясняет. К сожалению, трудно по- лучить единое мнение о том, что именно представляет собой функциональное программирование, даже ст самих функциональных программистов. Также можно противопоставить функциональное программирование императив- ному программированию (ппрега)ше рго§гапппт§) — тому, чем вы занимае- тесь на языках вроде С, Разса!, С++, (аса, Рег1, Лшк, ТСЬ и большинстве других. Функциональное программирование — это также не объектно-ориентирован- ное программирование (оР]ес1-опеШеб рго^гатпищ;, ООР), хотя некоторые язы- ки поддерживают оба подхода. И это не логическое программирование (Доцдс ргодгатттд), например Рго 1о§, но и здесь существуют мультипарадигмальные (тиШрагабщт) языки. Лично я в общих чертах охарактеризовал бы функциональное программиро- вание как обладающее по меньшей мере несколькими из следующих призна- ков. Языки, которые называют функциональными, упрощают реализацию этих идей, делая другие вещи сложными или вовсе недоступными: • Функции (Гппсйопз) — объекты первого класса (йгзс-скгзз оЬ]ес)з). То есть все, что вы можете делать с данными (с!а1а), можно делать и с самими функциями (например, передавать функцию в другую функцию). • Рекурсия (гесигыоп) используется в качестве основного управляющего ме- ханизма. В некоторых языках не существует никаких других конструкций «цикла». • Существует фокус на обработку списков (Из) ргосеззшд) — например, от- сюда происходит название Ызр. Списки (йз)з) часто используются вместе с рекурсией по подспискам как замена циклам. • «Чистые» (риге) функциональные языки избегают побочных эффектов (зНе ейесТз). Это исключает почти повсеместный в императивных языках ша- блон присваивания сначала одного, затем другого значения одной и той же переменной (уапаЫе) для отслеживания состояния программы. • Функциональное программирование либо нс поощряет, либо полностью запрещает инструкции (зЧПетешз), вместо этого опираясь на вычисление выражений (ехргеззюпз) — другими словами, на функции плюс аргумен- ты. В чистом случае одна программа — это одно выражение (плюс поддер- живающие определения).
• Функциональное программирование Иипсйопа! рго§га1пт1п§) заботится о том, что именно следует вычислить, а не о том, как это следует вычис- лить. • Большая часть функционального программирования использует функ- ции высшего порядка (ЫцИег-огс!ег йшсйопз) — то есть функции, которые оперируют функциями, оперирующими функциями. Сторонники функционального программирования утверждают, что все пе- речисленные свойства обеспечивают более быструю разработку, меньший объ- ем и меньшую склонность к ошибкам кода. Кроме того, теоретики высокого уровня в области информат ики, логики и математики считают гораздо более простым доказать формальные свойства функциональных языков и программ, чем императивных языков и программ. Одним из ключевых понятий в функци- ональном программировании является «чистая функция» — функция, которая всегда возвращает один и тот же результат при одних и тех же аргументах, — что гораздо ближе к значению функции в математике, чем в императивном программировании. Руйгоп определенно не является чистым функциональным языком програм- мирования (риге Гипсйопа! рго^гашпйг.^ 1ап§иаде) — побочные эффекты широ- ко распространены в большинстве программ на РугЬоп. То есть переменные ча- сто переназначаются, изменяемые коллекции данных (шиГаЫе бага соПссйопз) нередко меняют свое содержимое, а ввод-вывод (1/0) свободно перемежает- ся с вычислениями. Он также не является языком функционального програм мирования (Еипсйопа! рго§гаштт§ (ап^оаде) в более общем смысле. Однако Руйюп — мультипарадигмальный язык (тиШрагасйдш (аг^иа^е), который дела- ет оункциональное программирование простым, когда это требуется, и позво- ляет легко смешивать его с другими стилями программирования. За пределами стандартной библиотеки (з1апс1агс1 НЬгагу) Хотя в рамках данного краткого отчета они не будут обсуждаться, существу- ет большое число полезных сторонних библиотек Русйоп для функционального программирования. Единственным исключением здесь будет обсуждение би- блиотеки МайЬем Коскйп под названием ти11:1р1еб1зра1с(1 как лучшей на те- кущий момент реализации своей концепции. Большинство сторонних библиотек вокруг функционального программи- рования представляют собой коллекции функций высшего порядка ((п^йег- огбег Гипсйопз) и иногда расширения инструментов для «ленивой» работы с итераторами (йега(огз), содержащихся в ИегСооТз. Некоторые заметные примеры включают следующее, но этот список не следует считать исчерпы- вающим: • ругзЕзСегц содержит ряд неизменяемых коллекций (йппийаЫе соИесйопз). Все методы структуры данных, которые обычно изменяют ее (тише), вместо этого возвращают новую копию структуры с требуемы- ми обновлениями. Исходная структура остается нетронутой. • Соо1г предоставляет набор утилитарных функций (иШйу 1'ипсйопз) для итераторов (пегагогз), фтакций (йтейопз) и словарей (ейейопапез). Эти функции тесно взаимосвязаны и образуют строительные блоки типич- ных операций анализа данных. Они расширяют стандартные библиотеки
11еггоо1.8 и ТипсЮо! з и во многом опираются на стандартные библиоте- ки современных функциональных языков • Ьуро1Кез1з — это библиотека для создания модульных тестов (ипИ ТезГз) с целью поиска ошибок в вашем коде, о которых вы бы никогда не по- думали. Она генерирует случайные данные, соответствующие вашей спецификации, и проверяет, что ваша гарантия все еще выполняется в этих случаях. Это часто называют тестированием на основе свойств (ртореПу-Ьазеа гезйщ»). популяризированным библиотекой Назке11 фшскСЪеск. • 1поге_1теЩоо1з пытается собрать полезные композиции итераторов, которые не покрываются ни ИегбссРз, ни рецептами, включенными в ею документацию. Эти композиции сложно реализовать корректно, и данная хорошо продуманная библиотека помогает пользователям из- бежать подводных камней при самостоятельной реализации. Стилевое примечание Как и в большинстве книг по программированию, для встроенных и блочных примеров кода будет использоваться моноширинный шрифт, включая простые имена команд или функций. Внутри блоков кода условный фрагмент псевдоко- да (рзеибо сойе) обозначается словом в угловых скобках, то есть это невалидный для Руйтоп фрагмент, например <сойе-Ыоск> . В иных случаях синтаксически валидные, но не определенные функции используются с описательными имена- ми. такими как деб_1:Ье_йа1:а().
(Избегая) управление потоком выполнения (Нош Сопгго!) В типичных императивных программах на РуГРюп, включая те, что ис- пользуют классы (с1а85ез) и методы (теШосЗз) для размещения императивного кода, блок кода обычно состоит из внешних циклов Носрз) Тог или иЫ1е, при- сваивания переменных состояния (ыасе уапаЫез) внутри этих циклов, модифи кации структур данных (с!аСа зтгисШгез), таких как сНсС, 1181 и зеС (или различ- ных иных структур из стандартной библиотеки или сторонних пакетов), а также операторов ветвления 1Г/е11Г/е1зе или Сгу/ехсерТ/ГГпаНу. Все это понятно и поначалу кажется простым для применения. Однако проблемы часто возника- ют именно из-за тех побочных эффектов, которые связаны с переменными состо- яния и изменяемыми структурами данных (тгНаЫе бай зГгисСигез) — они неред- ко хорошо моделируют наши представления из физического мира контейнеров, но при этом сложно рассуждать о том, в каком состоянии находятся данные в конкретный момент выполнения программы. Одно из решений — сосредоточиться не на конструировании коллекции дан- ных, а на описании того, что именно составляет эту коллекцию данных. Когда вы просто думаете: <'Вот есть некоторые данные, что мне нужно с ними сделать?» — а не о механизме построения данных, рассуждать часто оказывается проще. Им- перативное управление потоком, описанное в предыдущем абзаце, гораздо боль- ше про «как», чем про «что», и часто мы можем сместить фокус своего внимания. Инкапсуляция (Епсар8и1а(юп) Очевидный способ сфокусироваться на «что», а не на «как», — просто отре- факторить код и поместить конструирование данных в более изолированное ме- сто, то есть в функцию или метод. Например, рассмотрим существующий фраг- мент императивного кода, который выглядит так’ # настроить исходные данные соПесПое = де1_1п111а1_8ГаГе() зТаТе.уаг = Иопе Гог баГит ю баСа_зеГ: 1Г сопб1Псп(зСа1е_’/аг): з1:а1:е_7аг = са1ои1а1е_Ггот(ба1:ит) пей = тобСГу(бгСит, 8Са1е_уаг) со11ссМоп.абб_Со(пей) е1зе: пел = тоб1(:у_б1ГГегепС1у( баСит) со11есС1оп. гбб_Со(пе«) # теперь действительно работаем с данными Гог С1т1пд 1п соНесПоп: ргосе88(СЫпд)
Мы можем просто убрать «как» построения данных из текущей области ви- димости и спрятать его в функцию, о которой можно размышлять изолированно (или вовсе не думать, когда абстракция достаточно хороша). Например. # спрятать построение данных СеГ таке_сс11ес(:1оп(бэ'1а_зе1): соПесбГоп = деГ_1П1Г1а1_зГа1е() зГаГе_уаг = Ыопе тог барит 1п бага_зеГ: 1Г сопбИ1оп(з1а1.е_^аг): зРат.е_уаг = са1си1ате.Гго.'п(аа1ет, зтате.уэг) пей = тоб1Гу(баГьт, зГате.^аг) со11ес1л оп,абб_1о(пей) е1зе. пей = тоб1Гу_б1ГГегеп11у(ба1ит) соПесНоп.абб_Го(пей) гегигп соПесПоп # теперь действительно работаем с данными Гог 1Ыпд 1п паке_со11есГ1оп(ба1а_зеГ): ргосезз(1Н1пд) Мы не изменили логику программирования и даже не затронули строки кода, но все же сместили фокус с «Как мы конструируем со11есГ гоп?» на «Что создает таке_со11еоГ1оп()?». Вложения (сотргеНепзюпг) использование вложений списков и прочих сотргейепзюп-форм одновре- менно делает код более компактным и смещает фокус с «как» на «что». Вклю- чаемое выражение (сотргейепыоп) — это выражение, которое использует те же ключевые слова, что и циклы и условные блоки (солсНПопаН), но инвертирует их порядок, чтобы сосредоточиться на данных, а не на процедуре. Простое измене- ние формы выражения нередко существенно влияет на то, как мы рассуждаем с коде и насколько легко его понимать. Тернарный оператор (гегпагу орегаГог) также выполняет похожую перестройку фокуса, используя те же ключевые сло- ва в ином порядке. Например, если исходный код был таким: соПесГюп = 11з1() Гог баГит 1п баТа_зеГ: 1Г сопб1Г1оп(баГит) : соПесНоп ,аррепб( баГит) е!зе пел = тоб1Гу(ба!иР1) соПесГюп. аррепб (пей) . то более компактно можно написать так: соНесГюп = [б 1Г согб1Т1оп(б) е1зе тоб1Гу(б) Гог б 1п оаГа_зеТ] Гораздо важнее, чем просто сэкономить несколько символов и строк — это ментальный сдвиг, достигаемый размышлением о том, что такое зоПесГбоп,
и избеганием необходимости отвечать на вопрос «Каково состояние соПесТэоп в этой точке цикла?». Включения списков дольше всего присутствуют в Руйюп и в некотором смыс- ле являются самыми простыми. Сейчас в синтаксисе Руйюп также доступны включения выражений, включения множеств (зес сошрге1эеп81оп8) и включения словарей (сШД сотргейепзюпз). Однако оговоримся, хотя вы можете вкладывать выражения друг в друга на произвольную глубину, после довольно простого уровня они перестают прояснять и начинают «затуманивать» текущую ситуа- цию. Для действительно сложного построения коллекции данных рефакторинг в функции остается более читаемым вариантом. Генераторы (ОепегаЫгз) Генераторные выражения (§епегаюг сошргеГюпзюпз) имеют тот же синтак- сис, что и генераторы списков (Пзг сошргейепзюпз) — за исключением того, что вокруг них нет квадратных скобок (но круглые скобки синтаксически тре- буются в некоторых контекстах вместо скобок списка). При этом они «ленивые». То есть это лишь описание того, как получить данные, которое не материализу ется, пока его явно не попросят — либо вызовом метода пехГ () у объекта, либо проходом по нему в цикле. Это часто экономит память для больших последова- тельностей и откладывает вычисления до момента, когда они действительно по- надобятся. Например: 1од_11пез = (Ипе Гог ]зпе 1п геаб_Ипе(Ниде_1од_1:11е) 1Г сотр1ех_соп011:1оп(Ипе)) В типичных случаях поведение будет таким же, как если бы вы построили список, но поведение во время выполнения будет более «приятным». Очевидно, у этого генераторного выражения есть и императивные версии йтрегайче чег- зюпз), например: беГ деС_1од_Ипез(1од..Г11е): Ипе = геаб..Ипе(1од_ГИе) ипИе Тгие- Угу: 1Г сошр1ех_сопбИ1оп(Ипе): у!е1б Ипе Ипе = геас|_Ипе(1од_Г11е) ехсерТ ЗТорИегатГоп: гаГзе 1од_11пез = деГ_1од_11пез(Ниде_1од_ГИе) Да, императивную версию можно было бы тоже упростить, но показанный вариант предназначен для иллюстрации скрытого «как» цикла (ог по итериру- емому объекту (ЛегаЫе) — дополнительных подробностей, от которых мы так- же хотим абстрагироваться в своем мышлении. По сути, даже использование у 1 е 1 б—это своего рода абстракция поверх базового протокола итератора (йегаЮг рготосо]) Мы могли бы сделать то же самое с классом, у которого есть методы . гехб() и . Нет(). Например: сГазз СеП.од( 1пез(оЬ]ес1:) : беГ___1п1г__(зеИ, 1од_Г11е):
5е1Т.1од_Г11е = 1од_Г11е зеХТ.Ипе = ^1опе РеЕ___Пег___(зеХГ): геХигп зеХТ бе!___пех!__(зеХГ): 1Т зеХТ.ХХпе 13 Мопе: зеХТ.ХХпе = геаО.Х] пе(Ход_Т1Хе) лЬПе г.о1 сол1р1ех_сог.с111:±оп(8еХ'(:.ХХпе): зеХГ.ХХпе = геаО_11пе(зе1!.Ход_'(:1Хе) геХигп зеХТ Х1пе Ход_Х1пез = 6е'С1_од1_1пез(11аде_Ход_Г1Хе) Отвлекаясь от протокола итератора и «ленивосги» в целом, читатель должен увидеть, что генераторные выражения лучше фокусируют внимание на «что», тогда как императивная версия — хотя, возможно, и удачная как рефакторинг — сохраняет фокус на «как». Словари и множества (Билз ап(1 Хесз) Аналогично тому, как списки (ИзТз) метут создаваться с помощью вложенных списков, а не путем создания пустого списка, обхода в цикле и многократных вы- зовов . аррспб (), словари ((ИсНопапез) и множества (зеТз) можно создавать «сра зу целиком», а не путем многократных вызовов . ирба1е() или . ас!а () в цикле. Например: »> {1:сНг(б5+1) -Гог 1 ап гаг.де(б)} {0: ‘Д’, 1 В 2: 'С', 3: 0' 4: 'Е', 5: 'Е' } >» {сНг (65+1) Тог 1 1п гапде(б)} {'А', 'В', 'С', 'б , 'Е', 'Р'} Императивные версии этих генераторов выглядят очень похоже на приме- ры, показанные ранее для других встроенных типов данных (ЬшХМп ЛаТаТурез). Рекурсия (Ресигзюп) Функциональные программисты часто делают упор на выражении управле- ния потоком выполнения (Логу сошго!) через рекурсию (гесигзюп+), а не через ци- клы (1оорз). В таком подходе можно избежать изменения состояния каких-ли- бо переменных или структур данных в алгоритме и, что важнее, приблизиться к «что», а не к «как» вычисления. Однако, рассматривая возможность использо вания рекурсивных стилей, следует различать случаи, когда рекурсия — это про сто «итерация другим способом», и случаи, когда задачу легко разделить на бо- лее мелкие подзадачи и подойти к каждой из них сходным образом. Есть две причины делать упомяну] ое различие. С одной стороны, использо- вание рекурсии по сути как способа прохода по последовательности элементов хотя и возможно, на самом деле не является «питоничным». Это вполне соот- ветствует стилю других языков, таких как Ызр, но в Ругйоп часто выглядит не- естественно. С друтой стороны, Ру Итон сравнительно медленный на рекурсии и имеет ограниченную глубину стека. Да, вы можете изменить ее с помощью зуз.зетгесиг51опХ1п11т() на значение больше стандартного 1000; но если вы ловите себя на том, что делаете это, вероятно, это ошибка. В Руйюп отсутствует
внутренняя особенность под названием «оптимизация хвостовых вызовов (гаП са11 еИгтпаноп)», делающая глубокую рекурсию вычислительно эффективной в некоторых языках. Рассмотрим тривиальный пример, где рекурсия — по сути разновидность итерации: беТ гипп1пд_зит(питоегз, зТаг1:=0): 1Т 1еп(питЬегз) == 0: рг1пТ() геТигп Го1:а1 = питЬегз[0] + збагб рг1П1(10Ха1, егс!=" ') гипп1пд_8ит(питЬегз[1:] Тоса1) Однако здесь мало что можно рекомендовать: итерация, которая просто многократно изменяет переменную состояния ТоТа1, была бы более читаемой, и к тому же эту функцию вполне разумно вызывать для последовательностей намного большей длины, чем 1000. В других случаях рекурсивный стиль, даже поверх последовательных опера- ций, все же выражает алгоритмы более интуитивно и в форме, о которой лег- че рассуждать. Чуть менее тривиальный пример — факториал в рекурсивном и итеративном стилях: с1еТ Тас1:ог1а].Р(М): ''Ресызтме Тасбогта! ТипсСтсп" аззегб 1з1пз1:апсе(И, 1пТ) апй И >= 1 гебигп 1 1Г М <= 1 е1зе 1\ * Тас1:ог1а1Р(М-1) беТ ^асбог1а11(М): "ИегаНуе ТасТог1а1 РипсПоп" аззегб 1з]пзТапсе(М, 1п1) апб Н >= 1 ргобос! = 1 иИПе М >= 1 : ргобисТ *= М -= 1 геТигп ргос!ис1: Хотя этот алгоритм можно легко выразить с помошью переменной «текуще- го произведения», рекурсивное выражение все же ближе к «что», чем к «как» ал- горитма. Подробности многократного изменения значений ргобосб иМ в итера тивной версии ощущаются как «бухгалтерия», а не сущность самого вычисления (хотя итеративная версия, вероятно, быстрее, и лимит рекурсии легко достигает- ся, если его не коррекгировать). В качестве примечания: самая быстрая версия РасСогтаЦ ), которую я знаю в РугКоп, выполнена в стиле функционального программирования и также хоро- шо выражает «что» алгоритма, если вам знакомы некоторые функции высшего порядка (Ыцйег-огбег Гипспопз): Тгош ГипсЬоо1з ттрог! гебисе Тгот орегаТог ттрогр та1 беТ Гасбог1а1Н0Е(п): геТити гес!исе(ти1, гагде(1, п+1), 1)
Там, где рекурсия действительно убедительна, а иногда и единственно оче- видный способ выразить решение — эго когда задача поддается подходу «разде- ляй и властвуй», то есть если можно выполнить похожее вычисление над двумя половинами (или, во всяком случае, несколькими сходного размера частями) более крупной коллекции. В таком случае глубина рекурсии составляет всего 0(1од И) от размера коллекции, что вряд ли будет чрезмерно глубоким. На- пример, алгоритм быстрой сортировки (цшскзоП) весьма изящно выражается без каких-либо переменных состояния или циклов, а целиком через рекурсию: СеТ аи1скзсгс(1з1:) : "ОштскзогХ оуег а ИзС-Ике зериепсе" 1Т 1еп(1зг) == 0: геТигп 1зТ ртуоС = 1з1:[0| ртуоСз = [х Тог х 1п 1зТ 1Т х == ргуоТ] 8ша11 = ри1ск8огт([х Тог х 1п 1з1 1Т х < ргуот]) 1агде = ри1скзогТ([х Тог х 1п 1з1 1Т х > рйуой]) гетит зтаП + рзуоСз + 1агде В теле функции используются некоторые имена для хранения удобных зна- чений, но они никогда не мутируют. Это было бы не столь читаемо, но при жела- нии определение можно записать одним выражением. Фактически превратить эго в стабильную итеративную версию довольно трудно и, несомненно, менее интуитивно. В качестве общего совета: хорошей практикой является поиск возможно- стей рекурсивного выражения — и особенно версий, избегающих необходимо- сти в переменных состояния или изменяемых коллекциях данных — всякий раз, когда задача выглядит делимой на подзадачи. В Руйюп обычно не лучшая идея использовать рекурсию лишь как «итерацию другими средствами». Устранение циклов (Е11тта(т§ Ьоорз) Просто ради удовольствия быстро посмотрим, как можно убрать все ци- клы из любой программы на Руйюп. В большинстве случаев это плохая идея — как для читаемости, так и для производительности, — но стоит увиде ть, насколь- ко просто это сделать систематически, чтобы получить тему для размышления о случаях, когда это действительно хорошая идея. Если мы просто вызываем функцию внутри цикла То г, нам на помощь прихо- дит встроенная функция высшего порядка тар (): Тог е 1п п: # цикл на операторах (згатетегй Ьазеб 1оор) Еипс(е) Следующий код полностью эквивалентен функциональной версии, за исклю- чением того, что здесь нет многократного переназначения переменной е, а зна- чит, нет состояния; тао(Гипс, 11) # "Зооо1 на оснозе тар() Похожий прием доступен и для функционального подхода к последователь- ному^ потеку выполнения. Большая часть императивного программирования состоит из операторов, которые сводятся к «сделай это, затем то, затем еще вот
это». Если эти отдельные действия обернуты в функции, шар() позволяет сде- лать следующее: # пусть Г1, Т2, ЕЗ (и т. д.) - функции, выполняющие действия # вспомогательная функция для исполнения йо.11 = ДатЬба -Г, *агдз: Е(*агдз) # последовательность действий на основе тар() тар(с!о_11, [Е1, Е2, ЕЗ]) Мы можем комбинировать последовательный вызов функций с передачей аргументов из итерируемых объектов (йегаЫез): >» Ье11о = 1атЬйа Е1гз1, 1аз1: рг!п1("Не11о", Е1гз1, 1аз1) >» Ьуе = Датоба Е1гз1, Даз! рг1п!( Буе1 ЕДгзЕ, 1аз1) >>> _ = 11з1(|тар(с1о_11, [НеДДо, Ьуе], ['йауДсГ ,'Дапе'], [ Мег1г ,'Бое'])) НеДДо БауДД Мег1/ Вуе Дапе Дое Конечно, глядя на пример можно предположить, что на самом деле нам нуж- но передать все аргументы каждой из функций, а не по одному аргументу из каж- дого списка каждой функции. Выразить это трудно без использования вложен- ного списка, но с ним достаточно легко' >>> По_а11_Еипсз = ДатЬДа Епз, *агдз: [ 1131(тар(Еп, *а-дз)) Еог Еп Дп Епз] »> _ = оо_а11_Еипсз([Ке11о, Ьуе], ['БауДсГ ,’Дапе'], [ Мег1г ,’Бое']) Не11о Юам1с1 мег12 Не11о Дапе Бое Вуе БауДс Мег12 Вуе Дапе йое В общем случае весь основной модуль программы мог бы, в принципе, быть выражением шар() с итерируемым набором функций, которые нужно выпол- нить для завершения программы. Перевод и!1 Не немного сложнее, но его можно сделать напрямую с использо- ванием рекурсии: # цикл лЫ1е на операторах (з1а1етеп1 Ьэзеб -лЫ1е 1оср) щЫ1е <сопс1>: <рге-зо11е-> ДЕ <Ьгеак_сопс1111сп> • Ьгеак е!зе: <зи!1е> # рекурсивный йИ11е в функциональном стиле (ЕР-з1у1е гесигзДуе «ч/ННе 1оор) беЕ йпНе_Ыоск(): <рге-зиНе> 1Е <Ьгеак_сопс1111сп>:
геТигп 1 е!зе: <зи1(:е> гетит 3 «Н11е_ЕР = 1атЬс1а: (<сопб> аг.с! «ИПе_Ыоск()) ог «Ы1е_ЕР() МтПе_ЕР() Наш перевод у/КПе все еще требует функции «К11е_Ыоск(), которая сама может содержать операторы, а не только выражения. Мы могли бы продвинуть- ся дальше и превратить «сниппеты» в последовательности функций с использо- ванием тар(), как показано выше. Если сделать это, мы могли бы вернуть одно единственное тернарное выражение. Подробности этого дальнейшего чисто функционального рефакторинга оставлены читателю в качестве упражнения (подсказка: получится некрасиво — забавно для экспериментов, но не годится для продакшек-кода). Сложно сделать полезным <сопб> с обычными проверками вида «КПе шууаг==7, поскольку тело цикла (по задумке) не может изменять значения ка- ких-либо переменных. Один из способов добавить более полезное условие — по- зволить «Ы1е_Ыоск() возвращать более интересное значение и сравнивать это возвращаемое значение для условия завершения. Вот конкретный пример устранения операторов: # императивная версия (нтрегэТтхе) 'есбоО'1 бе'*2 есбо_1МР() : «Ы1е 1 ’ х = 1приТ:("1МР -- ") 1Т х == 'ци11:' : Ьгеак е1зе: рГТ.П1(X) ес(ю_1МР() Теперь уберем цикл ухЬИе в функциональной версии: # функциональная версия (ЕР) ес(ю() беЕ 1беп1:11:у_рг1п1:(х) : # "хбепТЩу «11:1т зхбе-еНес!:1 ргхп1:(х) геТигп х ес1тэ_ЕР = 1ашЬс1а. хоеп1:х1:у_рг1п1:(хпри1:(’ЕР -- "))=='циН' ог ес1то_ЕР() ес!то_ЕР() Чего мы добились? Нам удалось выразить небольшую программу, которая включает ввод/вывсд (1/0), пикт и условные операторы, как чистое выраже- ние с рекурсией (а именно как объект функции, который при желании мож- но передать куда-то еше). Мы все еще используем вспомогательную оункпию хбеп1’хту_ог1п1:(), но эта функция полностью общая и может переисполь- зоваться в любом функциональном выражении, которое мы создадим позже (это «одноразовая» стоимость). Заметьте, что любое выражение, содержащее 1с1еп1:11:у_рг1п1:(х), вычисляется в то же самое значение, как если бы оно про сто содержало х; она вызывается только ради побочного эффекта ввсда/выЕода.
Устранение рекурсии (ЕИтта1т§ Яесигзюп) Как и в случае с простым примером факториала, иногда мы можем выпол- нить «рекурсию без рекурсии», используя Топс1юс15 гебосе() или другие операции свертки (ГоЙз) — другие ГоШ отсутствуют в стандартной библиотеке Руйюп, ио их легко построить и/или найти в сторонних библиотеках. Часто ре- курсия — это просто способ комбинирования чего то простого с накапливаемым промежуточным результатом, и именно это в основе делает гебисе(). Чуть бо- лее развернутое обсуждение ‘ГипсТооРз. тесисе () приводится в главе о функци- ях высшего порядка.
Вызываемые объекты (СаНаЫез) Акцент в функциональном программировании — несколько тавтологиче- ски — делается на вызове функций. Руйюп на самом деле предоставляет нам несколько разных способов создания функций - или, по крайней мере, нечто очень похожее на функции, то есть то. что можно вызывать (саИаЫе). К таким способам относятся; • Обычные функции, создаваемые с помощью ОеТ и получающие имя в мо- мент определения. • Анонимные функции (апопупюш Гипсйопз), создаваемые с помощью 1атоба. • Экземпляры классов (шзТапсез), которые определяют метод с а 11 (). • Замыкания (йозитез), возвращаемые «фабриками» функций (йшейоп Гас- (опез). • Статические методы (згайс тейюйз) у экземпляров — либо через декора- тор (бесогаЩг) ©зТаЩетеббоб, либо через класс 61с1:. • Функции-генераторы. Этот список, вероятно, не исчерпывающий, но он дает представление о мно- гочисленных слегка различающихся способах создать нечто вызываемое. Конеч- но, обычный метод экземпляра класса тоже является вызываемым объектом, но чаще всего их используют там, где акцент делается па доступе к изменяемому состоянию и его модификации. Руйюп — мультипарадигмальный язык, но он делает упор на объектно-ори- ентированные стили. Когда определяют класс, обычно делают это ради создания экземпляров, которые служат контейнерами для данных, меняющихся по мере вызова методов класса. Такой стиль в каком-то смысле противоположен подходу функционального программирования, который подчеркивает неизменяемость и чистые функции. Любой метод, который обращается к состоянию экземпляра - - в любой степе- ни, — чтобы определить, какой результат вернуть, не является чистой функци- ей. Разумеется, и все прочие типы вызываемых объектов, о которых мы говорим, гоже допускают опору на состояние различными способами. Долго решалось, можно ли применить некоторую «темную магию» Руйюп, чтобы явно объя- вить функцию чистой — скажем, задекорировав ее гипотетическим декорато- ром ©ригеТипсИоп, который вызывал бы исключение, если функция порожда- ет побочные эффекты, — но общее мнение таково, что невозможно защититься от каждого пограничного случая во внутренней машине Руйюп. Преимущество чистой функции и кода без побочных эффектов в том, что их, как правило, легче отлаживать и тестировать. Вызываемые объекты, которые свободно перемежают «состояние» со своими возвращаемыми результатами, нельзя исследовать независимо от контекста их выполнения, чтобы понять их поведение — по крайней мере не полностью. Например, модульный тест цтй ТезО с использованием босгезс или игигХезТ либо сторонних фреймвор- ков, таких как руДезР или позе, может пройти в одном контексте и «упасть»
при идентичных вызовах внутри исполняющейся стабильной программы. Разумеется, любая программа, которая делает что-то полезное, должна иметь какой-то вывод — в консоль, файл, базу данных, по сети и т. п., — поэтому побоч- ные эффекты нельзя полностью исключить, их можно лишь в некоторой степе- ни изолировать, когда мы мыслим категориями функционального программи- рования. Именованные функции и лямбды Самые очевидные способы создать вызываемые объекты в Руйгоп — в поряд- ке очевидности — это именованные функции (пашеб Гипспопз) и лямбды (1ашЬ- йаз). Единственное принципиальное отличие между ними — наличие атрибута . диа1пате, поскольку обе могут быть привязаны (Ьоипё) к одному или несколь- ким именам. В большинстве случаев выражения 1апЬба используют в Руйзоп для коллбэков (саВЬаскз) и прочих сценариев, где простое действие «встраи- вается» в вызов функции. Но. как мы уже показали, управление потоком (Пом? сопТго!) в целом можно включать в однострочные лямбды, если очень захотеть. Определим простой пример для демонстрирования: >» беб Ке11о1(пате): рг1п1( 'Не11о , пате) >>> Не11о2 = 1атЬба папе: рг1пс("Не11о", пате) > >> Ке11о1 (’ йаVъс!') Не11о Йау1б > >> Не11о2( йаухб') Не11о Йаугб > >> Не11о1._циа1пате___ 'Не11о1' > >> Не11о2__циа1пате___ '<1атЬба>' »> Не11оЗ = Пе11о2 »> ИеНоЗ_____риаЗпате___ '<1атЬба>' > >> ЬеПоЗ ._циа1пате___= ВеПоЗ' »> Не11оЗ____циа1пате__ ' Не11оЗ' # можно привязывать функцию к другим именам Одна из причин, почему функции полезны — они лексически изолируют со- стояние и предотвращают «загрязнение» внешних пространств имен. Это огра- ниченная форма неизменяемости (поптШаЫПгу) в том смысле, что по умолча- нию ничто из того, что вы делаете внутри функции, не привяжет переменные состояния (з1аСе уапаЫез) снаружи функции. Конечно, эта гарантия весьма огра- ничена, поскольку операторы д!оЬа1 и поп1оса1 явно позволяют состоянию «утекать» из функции. Более того, многие типы данных сами по себе изменяе- мы, поэтому, если передать их в функцию, та может изменить их содержимое. Кроме того, выполнение операций ввода-вывода (1/0) тоже может изменить «со- стояние мира» и, следовательно, повлиять на результаты функций (например,
путем изменения содержимого файла или базы данных, которые читаются где-то еще). Несмотря на все оговорки и ограничения, упомянутые выше, программист, желающий сфокусироваться на стиле функционального программирования, мо жет сознательно писать многие функции как чистые, чтобы к ним можно было применять математические и формальные рассуждения В большинстве случа- ев состояние «утекает» только намеренно, а создание некоторого подмножества всей вашей функциональности в виде чистых функций ведет к более чистому коду. Возможно, такие функции стоит выделить в чистые модули либо анноти- ровать в именах функций или их босзДггпд-ах. Замыкания и вызываемые экземпляры В информатике говорят, что класс — это «данные с прикрепленными опе- рациями», а замыкание (с1озиге) — «операции с прикрепленными данными». В некотором смысле они достигают одной цели — помевщют логику и данные в один объект. Но есть и философская разница в подходах: классы подчеркива- ют изменяемое или переназначаемое состояние (тпТаЫе/геЫпбаЫе зГа(е), тогда как замыкания подчеркивают неизменяемость и чистые функции. Ни одна сто- рона этой «границы» не абсолютна — по крайней мере в Руйтоп, — но разные установки мотивируют использование каждого подхода. Построим учебный пример — что-то чуть сложнее Ке11э иог!б для разных стилей: # Класс, создающий вызываемые суммируемые экземпляры с!азз Аббег(оЬдесГ): беТ__1п11__(зе1Т, п) : зе1Т.п = п беТ__са11__(зе!Т, гп): геТогп зеИ.п + ш аб05_1 = Аббег(5) # "тпзТапсе' или 1трегаТ1уе" Мы создали нечто вызываемое (саПаЫе), что прибавляет 5 к переданному аргументу. Кажется достаточно простым и «математичным». Попробуем то же как замыкание (с!озиге): бе* паке_абаег(п): беТ аббег(т): гебигп т + п ге^игп аббег абб5_Т = таке_аббег(5) # ТипсПопа!" Пока что это выглядит практически одинаково, но изменяемое состояние (тшаЫе зГаге) в экземпляре — привлекательная «опасная приманка»: >» абб5_1(10) 15 >>> абб5_б(10) 15 »> абб5_1.п =10 # состояние легко меняется »> абб5_1(10) # результат зависит от прежнего потока 20 # только аргумент влияет на результат - для замыкания
Поведение суммируемого числа, созданного с помощью Ас1с1ег() или шаке_ас1аег(), вообще говоря, определяется только во время выполнения. Но как только объект создан, замыкание ведет себя как чистая функция, тогда как экземпляр класса остается зависящим от состояния. Можно, конечно, про- сто не менять это состояние — и это возможно (если никто другой с более сла- бым пониманием не импортирует и не использует ваш код), — но мы привычно меняем состояние у экземпляров, и стиль, который программно предотвращает злоупотребления, поощряет лучшие привычки. Есть небольшое неудобство в том, как Ругйоп привязывает переменные в замыканиях. Он делает это но имени, а не по значению, что может вызвать путаницу — но это имеет простое решение. Например, что если мы хотим «на- штамповать» несколько родственных замыканий, инкапсулирующих разные данные: # почти наверняка не то поведение, которое мы хотели! >>> аббегз = [] >>> Тог п тп гапде(5) : абс1ег5.аррепсЦЕатЬба ш: т+п) >>> [ас1аег(10) Тог асМег 1п аоаегз] [14, 14, 14, 14, 14] >>> п = 10 >>> [абс1ег(10) Тог аббег 1п асМогз] [20, 20, 20, 20, 20] К счастью, небольшое изменение обеспечивает поведение, которое, вероят- но, лучше соответствует цели: >>> асМегз = [ | >>> Тог п 1п гапде(5) : абс!егз.аррепб(1ашЬс1а ш, п=п: т+п) >>> [абс1ег(10) Тог эсМег 1п аббегз] [10, 11, 12, 13, 14] »> п = 10 >>> [ас!с!ег(10) Тог аббег 1п аббегз] [10, 11, 12, 13, 14] >>> ас!с14 = аббегз[4] >>> ас!с14(10, 100) # можно переопределить связанное значение 110 Обратите внимание: «трюк» со связыванием значения в область через имено- ванный аргумент (кеумгогй аг§итеп!) позволяет изменить замкнутое значение; но это несет куда меньший риск путаницы. чем в случае экземпляра класса. Пе- реопределяющее значение для именованной переменной должно быть явно пе- редано в самом вызове, а не «переприсвоено» где-то далеко по ходу программы. Да, имя ас!64 уже не совсем точно описывает «складывание любых двух чисел», но, ко крайней мере, изменение результата синтаксически локализовано
Методы классов (МегЬос18 о! С1а88ез) Все методы классов — вызываемые объекты (саПаЫез). Однако в больший стве случаев вызов метода экземпляра идет вразрез со стилями функционально- го программирования (йшейопа] рго^гатшт§). Обычно мы используем методы, потому что хотим обращаться к изменяемым данным, упакованным в атрибуты экземпляра, — следовательно, каждый вызов метода может давать разный ре- зультат, который меняется независимо от переданных ему аргументов. Аксессоры и операторы (Ассеззогх апс1 ОрегаСогз) Даже аксессоры (ассеззогз), созданные с декоратором (аесогаГог) (Эр^орсгТу или иными способами, технически являются вызываемыми объектами, хотя их применение с точки зрения функционального программирования ограничено гем, что геттеры (^ейегз) не принимают аргументов, а сеттеры (зейегз) не воз- вращают значения: с1азз Саг(оО]есб): бе! __1п1т__(зе1Г): зе1Т._зрееб = 100 (РргорегТу Реб зрееб(зе1Л): рг1пТ('Зрееб 1з", зеИ,_зрееб) геТигп зе!Т._зрееб ©зрееб.зеТТег бе! зрееб(зе1Г, уа1ие): рг!пТ("ЗеТТтпд Те", уа1ие) зеИ. _зрееб = еа1ое # » саг = Саг() # >>> саг.дрееб =80 # Необычный синтаксис для передачи одного аргумента # ЗеТТТпд Го 80 # >>> х = саг.зрееб # Зрееб 1з 80 В аксессоре мы приспосабливаем синтаксис присваивания Руйюл для переда- чи аргумента. Впрочем, это достаточно типично и для большого фрагмента син- таксиса Руйгол, например: >>> с1азз Те1каТ1уе1пТ(1пТ) : беТ __1зЫТт„_(зе1Т, отбег): рг!пТ("ЗбИТ , зс1Т, "Ьу" отбег) геТогп 1пТ.__1зб1Тт__(зе!Т, отбег) »> Т = 1а1каТ1уе1пТ(8) >>> Т « 3 3616т 8 Ьу 3 64 Любой оператор в Руйгол по сути является вызовом метода «под капотом». Но хотя иногда это создает более читаемый предметно-ориентированный язык (боташ зресйгс 1ап§иа§е, П31,), назначение «особых» вызываемых значений
для операторов не добавляет новых возможностей по сравнению с вызовами функций. Статические методы экземпляров (8гаИс Ме(коЛз о/Чпзшпсез) Один способ использования классов и их методов, более согласованный со стилем функционального программирования, — это использовать их просто как пространства имен (пагпезрасез) для набора родственных функций: 1трог1 шаСЬ сРазз К1д1тсТг1апд1е(о1)]есТ): "С1азз изеб зо1е1у аз патезрасе -Гэг геРаТеб ТипсПопз' ©зТаГРотеТбоб боТ Ьуро1:епи8е(а, Ь) . геТигп таПг.8дгР(а**2 + Ь**2) @зТаТ1стеГпоб беТ зРп(а, Ь): геТигп а / РРд1г1:ТгРапдРе.буроГ.епи8е(а1 Ь) ©зГаПстеТбоб беР соз(а, Ь): геТигп Ь / ГТ г 1апд 1е . буро1епизе(а, Ь) Наличие такой функциональности внутри класса позволяет не загрязнять глобальное (или модульное и т. и.) пространство имен и дает возможность ука- зывать либо класс, либо его экземпляр при вызове чистых функций. Например: >>> РРдИГТгРапдРе.ЬуроТепи8е(3,4) 5.0 »> г! = РхдЬТТгхапдТе() >>> г!.зРп(3,4) 0.6 >>> П.008(3,4) 0.8 Самый прямой способ определить статические методы — использовать одно- именный декоратор. Однако в Руйгоп 3.x можно «доставать» и функции, которые не были так задекорированы, то есть понятие несвязанного метода (гшЬоопб тейгоб) больше не нужно в современных версиях Руйгоп. »> гтоогС ТопсСооРз, орегаСог »> сРазз МаЦЬ(оЬ}есР) : сеТ ргооисС(*питз): геТигп ТипсТооРз . гебисе(орега1ог.тиР, питз) сеС ро1Л'ег_сНа1п(*пип18): геРигп ТипсРооРз.гебисе(орега(ог.ро™, питз) >>> МаСН ргобист(3,4,5) 60 »> Ма1±.роиег_сЬа1п(3,4, 5) 3486784^01 Однако без ФзРаМстеббоб это не будет работать на экземплярах (тзРалсез), потому что они все еще ожидают, что будет передан зеРТ:
>>> ш = Ма1:п() »> ш. ргобое 1:(3,4, 5)------------------------------------- ТуреЕггог ТгасеЬаск (позТ гесепт са11 Хаз!) <1ру1Ноп-1прЫ:-5-е1 бе62сГ88аГ> 1п <тоби1е>()-----> 1 т.ргсс!ис1:(3,4.5) <1руГИоп-1при1>2-535'!94',:57а64> 1п ргобист(*потз) 2 с1азз МаГНСоЬтес!:): 3 Се* ргоСисС(*питз) :----> 4 5 б геТигп Гипс1оо1з.гебисе(09егаюг .ти1, пита) Се"Р роиег_спа1п(*питз); геТигп ГипсГооХз. гебисе(орегаТог.рои/, питз) ТуреЕггог: ипзиррогТеб орегапб Туре(з) Гог *: МаТИ' апС '1пТ’ Если ваше пространство имен — зто «мешок» для чистых функций, нет при- чин не вызывать их через класс, а не через экземпляр. Но если вы хотите сме- шать некоторые чистые функции с другими методами, которые зависят от изме- няемого состояния экземпляра, используйте декоратор @зтаТ1спеГбоб. Функции-генераторы (Сепега1ог Гипсиопз) Особый вид функции в РуШоп—та, которая содержит оператор у 1е 1С, превра- щающий ее в генератор. Результатом вызова такой функции является не обыч- ное значение, а итератор, производящий последовательность значений по мере того, как вы вызываете пехГ () или проходите по нему в цикле. И хотя, как и в любом объекте Р^Гйоп, существует множество способов при- внести «состояние» в генератор, по принципу генератор может быть «чистым» в смысле чистой функции. Это просто чистая функция, производящая (возмож- но, бесконечную) последовательность значений, а не одно значение, но все еще только на основе переданных ей аргументов. Заметьте, что функции-генерато- ры обычно имеют значительное внутреннее состояние; именно на границе сиг- натуры вызова и возвращаемого значения они ведут себя как «черный ящик» без побочных эффектов. Простой пример: >>> беГ деГ_рг!тез() : 181тр1е 1агу Ззеуе оГ ЕгаТозГбепез сапоХбаСе = 2 Гоопб = [| м'ПНе Тгие : 1Г аХЦсагбббаТе % ргХте != 0 Гог ргХте 1п Гоипб): уге1б сапбХбаГе *оипб .аррепб(сапб1ба1:е) сагбгбаЕе += 1 >>> ргбтез = деС_рг1те.з() »> пех-ЦргХтез), пех-Цргхтез), пехТ(рг1г1ез) (2, 3, 5) »> Гог ргХте 1п 21р(гапде(10), ргГтез):
рг 1пI:(рггпе, епс1-" ") 7 11 13 17 19 23 29 31 37 41 Каждый раз, когда вы создаете новый объект с помощью дес_рг1теЕ(), ите- ратор будет той же самой бесконечной «ленивой» последовательностью — дру- гой пример мог бы принимать начальные значения, влияющие на результат, — но сам объект будет стабильным, сохраняющим свое состояние (ЫаГеГи!) по мере его поэтапного потребления. Множественная диспетчеризация Очень интересный подход к программированию с несколькими путями вы полнения — техника множественной диспетчеризации (пги!йр1е сбзраГсН), ино- гда называемая «мультиметоды» (тиШтеШойз). Идея заключается в том, чтобы объявить несколько сигнатур для одной и той же функции и вызывать ту реали- зацию, чьи типы или свойства аргументов совпадают с переданными. Эт а техни- ка часто позволяет избежать явною ветвления или сократить его, заменив более интуитивными «описаниями шаблонов» аргументов. Когда-то давно был написан модуль тиИттеТНосз, который был довольно гибок в вариантах разрешения «линеаризации диспетчеризации (сйзраЩН 1ш- еапхайоп)», но был настолько стар, что работал только с РуЙгоп 2.x, и даже был написан до появления в РуГбоп декораторов для более элегантного выражения концепции. Более современный тиШрБесНзраУсН Магйгеч/ Коскип — это совре- менный подход для новых версий Руйгоп, хотя он и лишен некоторых теоретиче- ских тонкостей. которые автор исследовал в «древнем» модуле. В идеале, по мне- нию автора, будущая версия Руг]топ включала бы стандартизованный синтаксис или АР1 для множественней диспетчеризации — но, вероятно, эта задача всегда будет в сфере сторонних библиотек. Чтобы показать, как множественная диспетчеризация делает код более чита- емым и менее склонным к ошибкам, реализуем игру «камень/бумага/ножницы» в трех стилях. Для всех вариантов создадим классы для игры: с!азз ТЫпд(оЬ]ес1:): разз с!азз Ксск(7Н1пд): оазз с!азз Рарсг(ТК1пд): разз с!азз 8с1ззог8(ТЫпд) : разз Много ветвлений (Мапу Вгапскез) Сначала рассмотрим чисто императивную версию. Здесь будет много повто ряющихся вложенных условных блоков, в которых легко ошибиться: ОеС Ьеабз(х, у): 17 1з1пзТапсе(х, Роок): 1Т 1з1пз1:апое(у, Воск): геТигп Ьюпе # Нет победителя е11Г 1з1пзТапсе(у, Рарег): геТигп у е11Г 131пз('апсе(у, Бсгззогз) :
гебигп х е!зе: гаьзе ТуреЕггог( "11пкпоу/п зесопа тЫпд") е116 1з1пзбапсе(х, Раэег): 16 1з1гзТапсе(у, Роек): гетигп х е116 181пзбапсе(у, Рэрег) : гебигп Иосе # Нет победителя е116 1з1пз1:апсе(у, Зсбззогз): гетигп у е1зе гатзе ТуреЕггог('Ипкполп зесопа тЫпд1) е116 1з1пзТапсе(х, Зсзззогз): 16 1з1пзбапсе(у, Роек): геТигп у е116 1з1пзбапсе(у, Рарег): гетигп х еИ6 151пзТапсе(у, Зстззогз): гстигп Йоге # Нет победителя е1зе: га!зе ТуреЕггог( 11пкполп зесопа Тббпд") е1зе: га!зе ТуреЕггог( "Нпкпо'лп 61 тэт Тб1пд") госк, рарег, зстззогз = Роск(), Рарег(), Зс1ззогз() # »> Ьеабз(рарег, госк) # <_ша1п___.Рарег а! 0x103Ь96Ь0Р> # >>> Ьеабз(рарег, 3) # ТуреЕггог: Впкпоип зесопб ТЫпд Делегирование объекту (1)е1е§а1т§ ю гЛс ОЬ]есг) Во второй попытке мы можем попытаться устранить часть хрупкого дубли- рования с помощью «утинотипизации» (Диск 1ур1п§) в Руйюп — то есть разные объекты могут разделять общий метод, который вызывается по необходимости: с!азз ОиекАоск(Роск): сеТ Ьеабз(зе16, отбег)- 1Т 1з1пзгапсе(огбег, Роек): гесигп Мопе # Нет победителя е116 151пзбапсе(сТбег, Рарег): гетигп отбег еИТ 1з1пзтапсе(оТбег, Зстззогз): геТигп зеИ е1зе: га!зе ТуреЕггог( Ипкпом/п зесопб Тб1пд ') с1азз РискРарег(Рарег): аеТ ЬеаТз(зе1Т, отбег): 16 131пзТапсе(оТбег, Роек): геТигп зе!6
еИТ 1з1пз1апсе(о1:11ег, Рарег) : геРигп боле # Нет победителя е11Т 1зтпз1:апсе(о1:Нег, 8с1ззогз): геГОгп оГбег е1зе: гатзе ТуреЕггог( "Опкгоип зесопб 1Ыпд' ) с!азз 0иск8с1ззогз(8с1ззогз) : с!еТ 1эеа1:з(зе1Т, оГбег): К 1з1пз1апсе(о1:Нег, Зоек) : гетигп отбег е1К 131пзТапсе(оТбег, Рарег): геТигп зе1Т еИГ 1з1пзТапсе(оТбег, 8с1ззогз): геТигп Нэпе # Нет победителя е1зе: га!зе ТуреЕггог("бпкпоип зесопб Тбтпд ') беТ ЬеаТз2(х, у) : 1Т НазаТТг(х, 'РеаТз'): гетигп х.Ьсатз(у) е1зе: га!зе ТуреЕггог( 'бпкпоип Т1гз1 ТН1пд ) госк, рарег, зехззогз = ОискРоск(), ОискРарег(), Риск8с1ззогз() # »> Ьеасз2(гсск, рарег) # <_та!п___.ЭискРарег аТ 0х1ЕЗЬ894а8> # >>> Ьеа1з2(3, госк) # ТуреЕггог: бпкпоип Е1гзТ ТНтпд Мы на самом деле не сократили объем кода, но эта версия несколько умень- шает сложность внутри каждого отдельно вызываемого объекта (саПаЫе) и снижает уровень вложенности условий на одну ступень. Большая часть ло- гики распределена по отдельным классам, а не спрятана в глубоких ветвлени- ях. В объектно-ориентированном программировании мы можем «делегировать диспетчеризацию объекту» — но только «управляющему» объекту. Сопоставление с шаблоном (раИегп та1сМп$) В качестве последней попытки мы можем выразить всю логику напрямую, используя множественную диспетчеризацию (шп1йр1е сНзраЕйт). Это должно быть более читаемо, хотя по-прежнему остается определенное число случаев, которые нужно описать: Тгот ти11:1р1еб1зра1:сй ттрогТ: б1зра+сб @б1зрэ1:сН(Роск, Коек) беТ Сеа1:83(х, у): геГОгп Мопе @б1зра1:сН(Роск, Рарег) беТ Ьеа1зЗ(х, у): ге1:игп у @б!зра1:сб(Роск, 8с1ззсгз) беТ ЬеаСзЗ(х, у): геГОгп х @б1зра1:сб(Рарег1 Роек) беТ Ьеа1зЗ(х, у): геГОгп х
@сКзра1:сН(Рарег1 Рарег) Ьеа1-зЗ(х, у): геТигп Мопе ©сИзра^сЬ(Рарег, 8с1ззогз) бе* Ьеа1зЗ(х, у): геТигп х @б1зраГсН(8с1зеогз, Коек) Йе* ЬеатзЗ(х, у): геТигп у @>с1 изранен(8с 1350гз, Рарег) беГ Ьеа1зЗ(х, у): геПигп х @с!ЗзраРсН(8с 1 ззогз, 8с1ззогз) Не4" Ьеат53(х, у): гетигп Мэпе @с! нзратсП (оа ] ес Ц оЬуес!) Не-? ЬеатзЗ(х, у): 1Т по! ]81пз1апсе(х, (Роек, Рарег, Зсэззогз)): гатзе ТуреЕггог( "Опкпо'лп НгзИ 1Н1пд") е1зе: га!зе ТуреЕггог("Кпкпожп зесопб 1111пд") # >>> ЬеаТзЗ(госк, рарег) # <_ша1п__.0искРарег а! 0х103ЬС94а8> # >>> Ьса1зЗ(госк, 3) # ТуреЕггог: Цпкпоип зесопб НН1пд Диспетчеризация на основе предикатов (ргесИсаСе-Ьазед сИзра^сЮ Действительно экзотический подход к выражению условной логики как решений диспетчеризации — включать предикаты (ргесйсаЗез) непо- средственно в сигнатуры функций (или, возможно, в декораторы на них, как в тиЗПрЗесНзраСсб). Я не знаю стабильно поддерживаемой библи- отеки РуТКоп, которая делает это, но давайте просто предположим суще- ствование гипотетической библиотеки, чтобы проиллюстрировать кон- цепцию. Эта воображаемая библиотека могла бы называться, например, ргес11саГ1уе_с11зра1сП: Тгош ргесНсагтуе сйзраДсН 1трог1: ргеЛсаге @ргеа1са1е(1ат1Ьба х: х < 0, 1атЬба у: Тгие) бе4' з!дп(х, у): рг1п1("х 1з педа11\/е; у 1з", у) @ргед1саНе(]атЬс1а х: х == 0, 1атЬба у: Тгие) з1дп(х, у): рг1РН("х 1з гего; у 1з", у) @ргеа1са1е( ТатЫа х: х > 0, 1атЬба у: Тгие) бе42 з!дп(х, у): рг1пН("х 1з розШуе; у 1$", у) Хотя этот пример явно не является полной спецификацией, читатель может увидеть, как мы могли бы перенести болыщтс часть условного ветвления непо- средственно в сигнатуры вызова функции, и это могло бы привести к меньшим, более легко понимаемым и отлаживаемым функциям.
Ленивые вычисления (1агу еуа1иаГюп) Мощным инструментом РуПюп является его итераторный протокол (йег- аЮг ргоГосо]), к которому мы вскоре перейдем. Эта возможность слабо связа на с функциональным программированием как таковым, поскольку РуГЬоп не вполне предлагает ленивые структуры данных (1аху баГа зГгисйпгез) в том смысле, как это делает язык НазкеИ. Однако использование итераторного про- токола и многочисленных встроенных или стандартных библиотечных итери- руемых объектов достигает во многом тою же эффекта, что и настоящая лени- вая структура данных. Поясним различие немного подробнее. В языке вроде НазкеИ, который по сво- ей природе исполвзует ленивые вычисления Оаху еуакшйоп), мы могли бы опре делить список всех простых чисел следующим образом: -- Определим список ВСЕХ простых чисел ргзтез = зге'уе [2 .. ] ибеге з!еуе (р:хз) = р : з1ехе [х | х <- хз, (х 'гет р)/=0] Здесь мы не пытаемся обучать вас НазкеИ, но вы можете увидеть в данном примере включение (сошргейепзюп), которое, по сути, стало моделью для введе- ния включений в РуШсп. Там также используется глубокая рекурсия (гесигзюп), которая в Рутйоп работать не будет. Помимо синтаксических различий, или даже способности рекурсиро вать на произвольную глубину, существенное отличие состоит в том, что вер- сия рпгпез на НазкеИ является фактической (бесконечной) последовательно- стью, а не просто объектом, способным последовательно производить элементы. В частности, в НазкеИ вы можете индексироваться к произвольному элементу бесконечного списка простых чисел, и промежуточные значения будут произво- диться внутренне по мере необходимости на основе синтаксической конструк- ции самого списка. Заметьте, это можно воспроизвести и в РуИэоп — просто это не заложено в синтаксис языка и требует «ручной» конструкции. Имея функцию-генера- тор де!_р гДтез () из предыдущего обсуждения, мы могли бы написать свой кон- тейнер, чтобы имитировать то же самое, например: Ггот соПесИопз.аЬс 1прогт Ведиепсе с1азз Ехрарб1пд8едиепсе(8едиепсе): беТ __1п1б__(зе1Т, 11): зе1Т.1б = 11 зе1Т._саобе = [] деТ___дс111еш_(зс1Т, 1пбех): йН11е 1еп(зе1Т._сэсЬе) <= 1пбех: зе11._сасбе. аррепб(пехс(зе1Т.11)) ге!игп зеИ._сасбе[ 1пбех] бе!___1еп__(зе1Т): ге1игп 1еп(зе1Г._сас11е)
Это г новый контейнер может быть и ленивым, и индексируемым: »> рггтез = Ехрапб1пд8едзепсе(деГ_рг1те5()) »> Гог р 1п 21р(гапде(1А), рг1те$): Р’"1пТ(р, епо=“ 1 ) 235711 13 17 19 23 29 > >> рг!тез[10] 31 > » рг1тез[5] 13 > >> 1еп(рг1тез) 11 >» рг1тез[18Э] 547 »> 1еп(рг1тез) 101 Разумеется, можно было бы добавить и другие пользовательские возможно- сти, поскольку ленивые структуры данных не являются неотъемлемой частью РуШоп. Возможно, нам хотелось бы уметь делать срезы (зйсез) этого специаль- ного последовательного объекта. Возможно, нам хотелось бы получить более красивое строковое представление объекта при печати. Возможно, нам следо- вало бы сообщать длину как 1пГ, если мы каким-то образом сигнализировали, что он задуман как бесконечный. Все это возможно, но для добавления каждого такого поведения нужен небольшой объем кода — вместо того, чтобы это явля- лось действием по умолчанию для структур данных Руйюп. Протокол итератора (йегаЮг ргоЮсо!) Самый простой способ создать итератор, то есть ленивую последователь- ность Паху зецпепсе), в Руйюп заключается в определении функции-генератора, как обсуждалось в главе «Вызываемые объекты (СаПаЫез)». Просто используй- те оператор у!е1б внутри тела функции, чтобы определить места (обычно в ци кле), где производятся значения. Или, технически, самый простой способ — использовать один из множе- ства итерируемых объектов, уже предоставляемых встроенными средствами или стандартной библиотекой, вместо написания своего собственного. Функ- ции-генераторы — это синтаксический сахар (зупгасйс зп§аг) для определения функции, которая возвращает итератор Многие объекты имеют метод .__Пег__(), который возвращает итератор при вызове — как правило, через встроенную функцию 11ег(), или еще чаще просто при итерации по объекту (например, Гог 11еш 1п соПесГюп: ...). Итератор — это объект, возвращаемый вызовом Пег(зопе11з1пд), который сам имеет метод ._Пег____(), просто возвращающий сам объект, а также дру гой метод под названием ._пехГ__(), Причина, по которой сам итерируемый объект все же имеет метод .__Пег___(), — сделать 1Гег() идемпотентной
(1с1етроГеп1). То есть эта идентичность всегда до.тжна выполняться (или генери- роватьТурсЕггэг(«оо]есТ 1з пот ИегаЫе»)): 11ег_яец = 11ег(зециепсе) 1Тег(П:ег_зея) == Пег_зец Вышеизложенное немного абстрактно, поэтому рассмотрим несколько кон- кретных примеров: >>> 1а/у = ореп( '06-1а71пезз.п1с1') # итерация по строкам файла >>> '__Пег___’ 1п сПг(1а?у) апб '____пех!___' 1п д1г(1а/у) Т гое >>> р1изI = тар(1ашЬс!а х: х+1, гапде(10)) >>> р1из1 # итерация по отложенным вычислениям «тар а! 0х103Ь002Ь0> >>> '__Пег___’ 1п б1г(р1из1) апо '____пехГ__’ 1п сНг(р1из1) Тгие >>> с1еТ То 10 () : Тог 1 1п гапде(10) : у!е1с1 1 >>> '__Пег___’ хи еПг(Го10) Еа1зе >>> '__Пег___' 1П Р1г(То10()) апс! '___пехЕ__' 1п с!1г(То'10()) Тгие »> 1 = [1,2,3] >>> '__Пег___’ 1п сП г(]) Тгие »> '___пехТ__' ю с!1г(1) Еа1зе »> 11 = Пег(1) >>> 11 <11з1_Пега1ог а! 0х103Ы1278> >>> 11 == Пег(11) Т гие # итерация по конкретной коллекции В стиле функционального программирования, или даже просто ради хоро- шей читаемости, писать пользовательские итераторы как функции-генерато- ры наиболее естественно. Однако мы также можем создавать пользовательские классы, соблюдающие протокол, — част о у них будут и другие поведения (т. е. ме- тоды), но большинство таких поведений неизбежно опираются на сохранение состояния и побочные эффекты., чюбы иметь смысл. Например Тгот соПесЫэпз аЬс хпрогс ПегаЫе с1азз Е1Ьопасс1(11:е''сЫе) : беТ___1пП____(зе1Т): зе1Т.а, ее!Т.Ь = 0, 1
зе1Т.Со1а1 = 0 сеТ___Пег____(зе1Т): гебигп зе1Т се"Г__пехг___(зе1Т): зеИ а, зе1Т Ь = зе1Т Ь, зеГТ.а + зе1Т Ь зе1Т.1о1а1 += зе1Т.а гебигп зеТГ.а сеТ гипп1пд_8ит(зе1Г): гетигп зеТТ.гога! # >>> Т1Ь = Е1Ьопасс1() # »> Т1Ь. гипп1пд_зи(Т1() # 0 # >>> Тог 1 1п 2]р(галде(10), Т1Ь): # ... ргптЦ!, епс!=‘ ") # ... # 1 1 2 3 5 8 13 21 34 55 # >>> Т1Ь.гипп1пд_зит() # 143 # >>> псхТ(Т1Ь) # 89 Этот пример, конечно, тривиален, но он показывает класс, который одновре- менно реализует протокол итератора и предоставляет дополнительный метод, возвращающий нечто зависящее от состояния экземпляра. Поскольку состоя- ние — это сфера объектно-ориентированного программирования, в стиле функ- ционального программирования мы, как правило, будем избегать подобных классов Модуль Иег1оо1з Модуль ПегГосТз — это набор очень мощных и тщательно спроектирован- ных функций для выполнения «алгебры итераторов (йегаЮг а!§еЬга)». То есть они позволяют комбинировать итераторы сложными способами, не материа лизуя ничего сверх того, что требуется прямо сейчас. Помимо базовых функций самого модуля документация модуля предоставляет ряд небольших, но легко реализуемых «рецептов (пестрей)» дополнительных функций, каждая из кото- рых использует две или три базовые функции в сочетании. Сторонний модуль гтюге_Иег1;эс1з предоставляет дополнительные функции, которые спроекти- рованы аналогично с учетом распространенных подводных камней и краевых случаев. Базовая цель использования строительных блоков из 1Сег1оо1з — избегать выполнения вычислений до того, как они действительно понадобятся, избе- гать затрат памяти на большие материализованные коллекции, избегать потен- циально медленного ввода-вывода (1/0) до тех пор, пока он строго не требует- ся. и так далее. Итераторы — это ленивые последовательности (1аху зедпепсез), а не материализованные коллекции, и при объединении с функциями или ре- цептами из гбегСооТз они сохраняют это свойство.
Вот пример комбинирования нескольких вещей. Вместо состояния внутри класса Е1Ьопасс1 для ведения текущей суммы мы можем просто создать один ленивый итератор, который генерирует и текущее число, и эту сумму: >>> беГ 'Г1Ьоласс1(): а, Ь = 1, 1 иКПе Тгие : у 1е1с1 а а, Ь = Ь, а+Ь >» Ггош 1тег'('со1з 1ггрогЩ Тее, асситиТаТе »> з, С = Сее(ЕтЬопасс1()) >>> ра1гз = 21р(С, ассати1а1:е(з)) >>> Тог (ТгЬ, 1о1а1) 1п х!р(гапде(7), ратгз): рг1пI(Г1Ь, Тога1) 1 1 1 2 2 4 3 7 5 12 8 20 13 33 Точное и оптимальное использование функций из 11:ег1оо1з часто требует тщательного обдумывания, ко после комбинирования получается удивительно мощный инструмент для работы с большими или даже бесконечными итерато- рами, что невозможно при работе с конкретными коллекциями Документация модуля 11ег1оо1з содержит подробности о его комбинатор- ных функциях, а также ряд коротких рецептов для их комбинирования. В дан- ной книге нет места для повторения этих описаний, поэтому достаточно при- вести некоторые из них выше. Заметьте, что для практических целей г1р(), шар(), ТПРег() и гапде() (который, в некотором смысле, просто завершающий- ся 1Рег1оо1з .соопт()) могли бы находиться в 1сег1оо1з, если бы они не были встроенными функциями. То есть все эти функции лениво генерируют после- довательные элементы — в основном на основе существующих итерируемых объектов, — не создавая конкретной последовательности. Встроенные функции а!1(), апу(), зит(), т!п(), тах() и ТипсРсо1з гебисе() также работают с ите- рируемыми объектами, но все они в обшем случае должны исчерпать итератор, а не оставаться ленивыми. Функция 11егРоо1з ргобисЦ) может выглядеть не на своем месте в этом модуле, так как она создает конкретные кешированные последовательности и не может работать с бесконечными итераторами. Объединение итерируемых объектов (СИа1пт§ ИегаЫез) Функции 1СегТоо1з.оНа1п() и 11егСос1з.сЛа1п.Тгот_1СегаЫе() объ- единяют несколько итерируемых объектов. Встроенные функции ?1р() и 1СегСоо1з. 21р_1опдсзт() тоже делают это, разумеется, но так, что позволяют пошагово продвигаться по итерируемым объектам. Следствие этого — хотя объ- единение бесконечных итерируемых объектов синтаксически и семантически
корректно, никакая реальная программа не исчерпает более ранний итерируе- мый объект. Например: Тгош г1егЮо18 1ггрог1: сНатп, ооипт Тбпсе-Ло^пТ = сМг 1п (соитР (), соигтЦ), соип1:()) Концептуально Сбг1се_(:о_1пТ будет считать до бесконечности три раза, но на практике одного раза всегда достаточно. Однако для просто больших ите- рируемых объектов — не для бесконечных — объединение (сЬашш#) может быть очень полезным и экономным по памяти: бе* Сгот_1одз(Тпатез): у!е1б Тгот (ореп(ТПе) Тог ТНе 1п Гпатез) Ипез = сба1п. Тгот_1ТегаЫе(Тгот_1сдз( [' Ниде. 1од ’, ' дТдапПс. 1од' ])) Заметьте, что в приведенном примере нам даже не нужно было передавать конкретный список файлов — сама последовательность имен файлов может быть ленивым итерируемым объектом согласно описанному АР1. Помимо объединения с помощью гГегГооГз стоит упомянуть соИесТТопз. С1за1пМар(). Словари — или вообще любые объекты, реализующие интерфейс соПесИопз аЬс . Маррьпд, — являются итерируемыми по своим ключам Точно гак же, как мы можем объединять несколько последовательных итерируемых объектов, мы иногда хотим объединить несколько отображений, не создавая один большой конкретный объект СЬа1пМар() удобен и не изменяет исходные отображения, из которых он состоит.
Функции высшего порядка (Н|дНег-Огс1ег Еипсйопз) Б предыдущей главе мы рассматривали «алгебру итераторов (йегайгг а]^е- Ьга)», основанную на модуле ИегЕооЕз. Р некотором смысле функции выс шего порядка (1зц>1тег огйег Гппсйоиз, НОЕз) предоставляют похожие строи тельные блоки для выражения сложных концепций путем комбинирования простых функций (ЕипсНопз) в новые функции. В общем случае функция выс- шего порядка — эго функция, которая принимает одну или несколько функ- ций в качестве аргументов и/или возвращает функцию в качестве результа- та. Здесь доступны многие интересные абстракции. Они позволяют связывать и комбинировать функции высшею порядка (Ы^йег-огйег ЕипсИопз) аналогич- но тому, как мы можем комбинировать функции из ИегТоо! з для получения новых итераторов. Несколько полезных функций высшего порядка содержатся в моду- ле Топсгоо1з. еще несколько — во встроенных средствах. Принято считать, что пар(), Е111ег() и ЕопсТооТз. гебосе() — базовые строительные блоки функций высшего порядка, и большинство функциональных языков програм- мирования используют их как примитивы (иногда под другими именами). Поч- ти столь же базовой, как таэ/Н Пег / гебисе, операцией является каррирование (спггуш§). В Руйюп каррирование называется раг11а1() и содержится в модуле ТипсТоэ1з — это функция, которая принимает другую функцию, а также ноль или более аргументов для предварительного заполнения, и возвращает функ- цию с меньшим числом аргументов, которая работает так же. как исходная функция при передаче ей этих аргументов. Встроенные функции тар() и Т1Иег() эквивалентны выражениям-генера- торам — особенно теперь, когда доступны генераторные выражения, — и боль- шинство программистов РуГйоп считают варианты с сопргебепзюпз более чи- табельными. Например, вот некоторые (почти) эквивалентные пары: # Классический "ЕР-з1:у1е ГгапзТогшес) = тар(С"ап(огтаГ1оп, ПегаСог) # Вариант с сотргебепзюп ТгапзТогтес! = (ТгапзЕсгтаТюг^х) Тог х 1п ИегаТог) # Классический 'ЕР-з1:у1е" ГтИегео = Е1Иег(рге<Пса1:е, ИегаТог) # Вариант с сотргебепзюп ЕШегес! = (х Тог х 1п ПегаТог 1Г ргес)1саСе(х)) 0>ункция ЕипсСосЕз. гебисе() довольно общая, очень мощная и очень тон кая в полном применении. Она берет последовательные элементы итерируе- мого объекта и как-то тех комбинирует. Наиболее распространенный случай ис- пользования гес!иое(), вероятно, покрывается встроенной функцией (ЬиПМп) зиш(), которая является более компактной записью следующего’
Тгош Гипс1со]з 1трог1 гебисе 101а! = гейисе(ореганог.аос1, 11, 0) # 1с1:а1 = зиш(1Т) Может быть очевидно, а может и нет, что тар() и +111ег() также являются частными случаями гебисе(). То есть. »> абб5 = 1атЬба п: п+5 > >> гес1исе(1атЬба 1, х: 1+1 асе! 5 (х) ], гапде(10), []) [5. 6, 7, 8, 9, 10, 11, 12, 13, 14] »> # проще: тар(аба5, гапде(10)) »> 1зСбб = 1ашЬба п: п%2 »> гебисе(1атбба 1, х: 1+[х] 1Т 1зОс1с1(х) е1зе 1, гапде(10), []) 11. 3, 5, 7, 9] > >> # проще: Т1Иег(1зОс1с1, гапде(10)) Эти выражения с гсаисе() довольно неуклюжи, но они также иллюстриру ют. насколько мощная данная функция в своей общности: все, что можно вычис- лить из последовательности последовательных элементов, можно (хоть и неу- клюже) выразить через свертку. Существует несколько распространенных функций высшего порядка, кото- рых нет среди «батареек в комплекте» Руйтоп, но которые очень летке создать как утилиты, и они включены во многие сторонние коллекции инструментов функционального программирования. Разные библиотеки — и другие язы- ки программирования — могут использовать разные имена для утилитарных функции, которые я описываю, но аналогичные возможности довольно широко распространены (как и выбранные мной названия). Утилитарные функции высшего порядка (1НШ1у Н(дНег-0гс1ег ЕипсИопз) Полезная утилита — сотрозе(). Эго функция, которая принимает последова- тельность функций и возвращает функцию представляющую применение ка- ждой из этих функций-аргументов к данным: бе! сотрозе(*Гипсз): ...РеТогп а пел ТопсИоп 5.1. сотрозе(Г,д,..,)(х) == Т(д(...(х)))11" беТ 1ппег(бата, Сипсз=Типсз): геза11: = баба Тог Г 1п ге1/егзеб(бипсз): гезиИ = Г(гези11:) гебигп гезиИ гетигп хппег # >>> С1тез2 = 1атЬба х: х*2 # >>> тхпизЗ = 1атЬс1а х: х-3 # >» тобб - 1атЬба х: х%б # >>> Т = сотрозе(тобб, Схтсз2, тхпизЗ) # »> а11(Г(х)==((1-3)*2)%6 Рог х 1п гапде(100000О)) # Тгие
Для этих однострочных математических операций (С1пез2, пипизЗ и т. п.) мы могли бы просто написать исходное выражение как минимум столь же лег- ко; но если бы составные вычисления включали ветвления, управляющие кон- струкции, сложную логику и т. п., это уже было бы не так Встроенные функции а11() и апу() полезны для проверки того, выполняется ли предикат для элементов итерируемого объекта. Но также удобно уметь спра- шивать, выполняется ли «любой,'все» из набора предикатов для одного элемента данных в композиционном стиле. Мы можем реализовать это так: а11_ргео = ХаггЬаа Нет, *1ез1з: а11(р(Иет) бог р 1п безбз) агу_ргеа = 1атЬаа Нет, Незбз: апу (р(Иет) бог р 1п безбз) Для демонстрации использования создадим несколько предикатов: >>> 1з_1б100 = рагб1а1(орегабог.де. 100) # меньше 100? »> 1з_дб10 = раггбаЦсрегабсг ,1е, 10) # больше 10? >>> бгот питз бтрогб 1з_рг1те # реализовано в другом месте >>> а!1_ргес!(71, 1е_Н1Э0, 1з_дС10, 1з_рг1те) Т гие >>> ргеЛоабез = (1з_1б100, 15_дб10, бз.ргхте) >>> а11_ргеб(107, *ргестсабез) Еа1зе Библиотека Соо1г имеет, возможно, более общий вариант под названием ]ихб(), который создает функцию, вызывающую несколько функций с одними и теми же аргументами и возвращающую коргеж результатов. Мы могли бы ис- пользовать это, например, так: »> Тгот Уоо1х.бипсбоо1х бтрогб )ихб »> ]охб( [1з_Н100, 1з_дб10, 1з_рг1те]) (71) (Тгие, Тгие, Тгие) »> а1.1(]ихб( [1з_1б100, бз_дб10, тз_рг1те])(71)) Тгие >>> _щхб( [1з_1б100, 1з_дб10, 1з_рг1те]) (107) (Еа1зе, Тгие, Тгие) Показанные здесь утилитарные функции высшего порядка — лишь неболь- шой набор для иллюстрации композиционности. Обратитесь к более подроб- ным текстам по функциональному программированию или, например, к пре- амбуле НазкеИ (НазкеП Рге1ис1е) — для ознакомления с множеством других идей по полезным утилитарным функциям высшего порядка (1н§1тег-огс1ег Гипсйопз). Модуль орегаЮг (ТЬе орегаЮг Мос1и!е) Как показано в нескольких примерах, всякая операция, которую можно вы- полнить при помощи инфиксных и префиксных операторов Ругйоп, соответ- ствует одноименной функции в модуле срегабог В тех случаях когда нужно передать функцию, выполняющую эквивалент некоторой синтаксической опе- рации, в другую функцию высшего порядка, использование имени из срегабог работает быстрее и выглядит изящнее, чем соответствующий 1 аптЬс!а. Например:
# Сравнение произвольного 1атЬба с функцией из ’орегаТог' еит1 = гебисе(1аггЬс1а а, Ь: а+Ь тТегаЫе, 0) гит2 = гес1исе(орегаХсг.ас!с!, пегаЫе, 0) еитЗ = зипДбДегаЫе) # Истинно ’питонический" способ Модуль ТипсЮоЬз (ТЬе Типс1оо12 Моди1е) Очевидное место в РуЛоп для размещения функций высшего порядка — мо- дуль ТипсГоо1$, и действительно, несколько таких функций там есть. Одна ко утилитарных функций высшего порядка в этом модуле удивительно мало. Со временем модуль получил несколько интересных дополнений, но разработ- чики ядра сопротивляются движению в сторону полноценного функциональ- ного языка программирования. С другой стороны, как мы видели в нескольких примерах выше, многие из наиболее полезных функций высшего порядка тре- буют всего пару строк (иногда одну) кода, чтобы написать их самостоятельно. Помимо гебисе(), обсужденной в начале этой главы, основной возмож- ностью модуля является раг11а](), которая уже упоминалась. Эта операция во многих языках называется «каррирование (епггуш^)». Некоторые примеры использования раг11а1() также обсуждались выше. Остальная часть модуля ГипсТос1з в основном посвящена полезным декора- торам (йесогабог.ч), что является темой следующего раздела. Декораторы (ОесогаЮгз) Хотя об этом легко забыть, вероятно, наиболее распространенным использо- ванием функций высшего порядка в Руйюп являются декораторы (бесогаСогэ). Декоратор — это просто синтаксический сахар (эуШах зщаг), который принима- ет функцию как аргумент и, если он запрограммирован корректно, возвращает новую функцию, которая каким-то образом улучшает исходную функцию (или метод, или класс). Напомним читателям: следующие два фрагмента кода, опре- деляющие зоте .Типе и оТбег_Типс, эквивалентны: ^епбапсеб бе* 5оте_Типс(*агдс): разз бе* оНзег_Типс(*агдз): разз оНэег_Типс = епбапсеб(о1Нег_Типс) При использовании синтаксиса декоратора функция высшею порядка обя- зательно применяется во время определения функции. Для их предполагаемых целей это обычно наилучший момент для применения. Но тот же декоратор в принципе всегда можно использовать и в другом месте программы более ди- намично (например, применяя декоратор к коллекции других функций, сгене- рированной во время выполнения). Однако это будет нетривиальным случаем использования. Декораторы используются во многих местах стандартной библиотеки и в рас- пространенных сторонних библиотеках. В некотором смысле они перекликаются
с идеей, которую раньше называли «аспектно-ориентированное программиро- вание (азресРопепГеб ргощапшшщ)». Например, декоратор азупсю.согосйюе используется, чтобы пометить функцию как сопрограмму (согоШше). Внутри Типс1оо1з размещены три важные функции-декоратора (йесога^ог йтпсйопя) — это Типс!юо1з. 1ги_сасбе. Типс1ос1з. СоСа1_огс1ег1пд и Рипс1оо1з.лгарз. Пер- вая «мемоизирует (тетотхез)» функцию (то есть кеширует переданные аргумен- ты и возвращает сохраненные значения вместо выполнения новых вычислений или ввода-вывода (1/0)). Вторая упрощает написание пользовательских классов, которым требуется использовать операторы неравенства. Последняя упроща- ет написание новых декораторов. Все эти цели важны и полезны, но они также в большей степени соответствуют духу упрощения «сантехники» программиро- вания на РуГЬоп в общем — почти синтаксическом — смысле, нежели компози- ционным функциям высшею порядка, на которых фокусируется эта глава. В целом декораторы полезнее, когда вы хотите заглянуть во «внутренности» функции, чем когда вы хотите рассматривать ее как подключаемый компонент в потоке или композиции функций, что часто делается для пометки назначения или возможностей конкретной функции.
Композиция функций и пайплайны Функциональное программирование предлагает элегантный способ органи- зации кода через композицию функций — мощный механизм, который позво ляет создавать сложные операции из простых переиспользуемых компонентов. В этой главе мы изучим, как строить цепочки обработки данных, создавать соб- ственные ртре-операторы и применять эти концепции .для решения реальных задач обработки текста и данных. Композиция функций (Тип сбои сотрозшоп) представляет собой фундамен- тальную концепцию, которая превращает программирование из императивно го описания последовательности действий в декларативное описание трансфор- маций данных. Вместо того чтобы думать о том, как изменить данные пошагово, мы думаем о том, какие преобразования нужно применить и в каком порядке. Этот подход не только делает код более читаемым и понятным, но и открывает возможности для оптимизации, параллелизации и переиспользования компо- нентов. Пайплайны (ргрейпез) в программировании работают по тому же принципу, что и конвейеры на заводе. Каждая функция в пайплайне выполняет одну кон- кретную операцию над данными и передает результат следующей функции. Та- кой подход позволяет разбить сложную задачу на простые независимые этапы, каждый из которых можно тестировать, отлаживать и модифицировать отдель- но. После изучения этой главы вы научитесь создавать гибкие и масштабируе- мые системы обработки данных, которые легко понимать и модифицировать. Основы композиции функций Композиция функций в математике означает создание новой функции пу- тем применения одной функции к результату другой. В программировании эта концепция позволяет нам соединять простые функции в цепочки для создания более сложного поведения. Представьте себе фабрику по производству мебели, где каждый цех выполняет одну операцию: один цех распиливает доски, другой шлифует их, третий покрывает лаком. Точно так же работает композиция функ ций — каждая функция выполняет одну четко определенную операцию и пере- дает результат дальше по цепочке бе* абб_Т1^е(ритбег: 1п1:) -> : ...Прибавляет пять к переданному числу...... гетигп пипЬег + 5 бе* пи1Т1р1у_бу_1;що(питЬег тпб) -> 1пб: ...Умножает переданное число на два...... гетигп питЬег * 2 бе* зчиаге(пит1Ьег: 1пт) -> 1п1: 1 1 Возводит переданное число в квадрат. гетигп питбог ** 2
Эти простые функции можно использовать по отдельности, но их истинная мощь проявляется при композиции. В императивном стиле программирования мы бы написали последовательность вызовов, сохраняя промежуточные резуль- таты в переменных. Функциональный подход предлагает более элегантное ре- шение через композицию. # Императивный подход - пошаговое выполнение беб ргосе83_питЬег_1трегаНуе(пип1Ьег: хпД) -> 1п1: '"Обрабатывает число императивным способом."" # 1Иаг 1: прибавляем пять з1ер1 = абб_-Г1уе(пап1Ьег) # Шаг 2: умножаем на два з1ер2 = ти1т1р1у.Ьу_Тло(зсер1) # Ша1- 3: возводим в квадрат гезиИ: = зциаге(з1:ер2) геСигп гезиИ # Функциональный подход - композиция функций беТ ргоссзз_питЬсг_Типс1:1опа1( питЬег: 1пС) -> 1п1: .......Обрабатывает число через композицию функций...... геЛигп 8риаге(ти11:1р1у_Ьу_1:у,'о(абб_'(:17е(пи1Т1Ьег))) Функциональный подход более лаконичен, но при увеличении количества функций в цепочке читаемость может пострадать из-за вложенности вызовов. Здесь на пометь приходит концепция пайплайнов, которая позволяет запи- сывать композицию функций в более естественном порядке — слева направо, как мы читаем текст. Создание базового пайплайна Пайплайн представляет собой структуру данных или механизм, который позволяет последовательно применять функции к данным, передавая резуль- тат каждой функции на вход следующей. В отличие от обычной композиции функций, пайплайн позволяет записывать операции в том порядке, в котором они выполняются, что делает код более читаемым и понятным: с!азз Р1ре11пе: ...Простая реализация пайплайна для композиции функций....... Феб___1п11___(зсИ, 1п1Д1а1_уа1ие): II II II Инициализирует пайплайн с начальным значением Агдз: 1пИ1а1_уа]це: начальное значение для обработки зоИ.уаФие = 1п11:1а1_уа1ие Феб р1ре(зе1Т, ФипсСюп) : II II II Применяет Функцию к текущему значению и обновляет состояние.
Агдз: ГипсИоп: функция для применения к текущему значению РеГигпз: зеИ для поддержки цепочечных вызовов зе1Т.уэ1ие = ТипсНоп(зе1Т уа1ие) гетигп зеТТ Ре'Г де1_гези11:(8е1Т): """Возвращает финальной результат обработки. гебагп зеИ.уаТие Теперь мы можем переписать нашу обработку числа, используя пайплайн. Это решение объединяет преимущества функционального программирования с читаемостью императивного кода: бе* ргосезз_»11:Ь_р1ре11пе(пип1Ьег: 1п1) -> мН: ...Обрабатывает число используя пайплайн. 1 ртреИпе = Р1ре11ге(па1Т1Ьег) гези11 = (ртреИпе .р1ре(абб_'Г17е) р1ре(гпи1'С1р1у_Ьу_1:«с) р!ре(здиаге) .деГ_гези11()) гетигп гези1"С # Демонстрация использования ог1д1па1_пипЬег = 3 ргосеззеб = ргосез8_й'11:Ь_р1ре11пе(сг1д1па1_пит0ег) рг1пТ(Т Результат обработки {опд1па1_питЬег}: {ргосеззеб}') # Выводит: Результат обработки 3: 64 Такой подход имеет несколько важных преимуществ. Во-первых, код чи- тается слева направо и сверху вниз, что соответствует естественному порядку мышления. Во-вторых, каждый шаг обработки явно виден и может быть легко добавлен, удален или изменен. В-третьих, пайплайн можно легко отлаживать, добавляя промежуточные выводы или проверки между’ этапами. Функциональная реализация р!ре-оператора Хотя классовый подход к созданию пайплайнов работает хорошо, функцио- нальное программирование предлагает более элегантное решение через созда- ние ргре-функции. Эта функция принимает начальное значение и последова- тельность функций, применяя их одну за другой: Тго1Г1 1ур1пд гпрогб Апу, СаПэЫе, ТуреУаг # Определяем типы для лучшей типизации Т = ТуреУаг(Т) 0 = ТуреУаг('б') беб ргре(1п1(:1а1_уа1ие: Т, *Типс1:1СПз: Са1.1аЫе[ [Апу ], Апу]) -> Апу:
Применяет последоветелоность функций к начальному значению Агдз: 1п1б1а1_уа1ие: начальное значение для обработки *Типсбсопз: произвольное количество функций для применения Пебигпз: результат применения всех функций гези1б = 1П1б1э1_уа1ие *ог Типсбтсп 1п Типстюпз: гези1б = ТипсЫоп( гезибт) гебигп гезибб Функциональная реализация р1ре-оператора позволяет писать очень лако- ничный и выразительный код. Каждый вызов р!ре создает новую композицию функций без изменения состояния внешних объектов: # Использование функционального р!ре оператора беТ ргосезз_и1бИ_Типсб1ОРа1_р1ре(питьег: тоб) -> 1пб: ...обрабатывает число, используя функциональный ртре..... гебигп р1ое(питрег, абб_Т1уе, ти1б1р1у_Ьу_б\л/о, здиаге) # Можем создать именованную композицию для переиспользовачия питЬег_ргосеззсг = 1этЬс!а х: р!ре(х, абб.ббуе, ти1б!р1у_Ьу_био, здиагс) # Демонстрация различных способов использования питЬегз = [1, 2, 3, 4, 5] ргоссззеб_питЬегз = [ргосезз_и1б11_бипсб1опа1_р1ре(пит) Тог пьт 1п питЬегз] рг1пб(Т"05работанные числа: {ргсссззеб_пишЬсгз}") # Выводит: Обработанные числа: [35 49, 54, 81, 100] Функциональный подход особенно хорошо работает с встроенными функци- ями РуТйоп, такими как пар, ТПбег и гебисе. Мы можем создать более сложные пайплайны, которые обрабатывают не только отдельные значения, но и коллек- ции данных: Тгоп! ТипстооТз 1трогб гебисе Тгот бурбпд бтрогб Ызб беТ б11бег_еуеп(питЬегз: 1_1зб[1пб]) -> Изб[1пб]: Оставляем только четные числа. гебигп 11зб(Т11бег(1атЬба х: х % 2 == 0, питЬегз)) беТ зит_а11(питЬегз: Ь1зг[1пб]) -> 1пб: ...Суммирует все числа в списке.... гебигп гебисе(1аггЬс'а а, Ь: а + Ь, питЬегз, 0) беТ богтаб_гези1б(боба1 1пб) -> збг: ...Форматирует результат в строку.... геТигп Т1Общая сумма четных чисел: {боба1}1 # Композиция функций для обработки списка чисел
бе-? ргосе53_питЬег_11з1(питЬегз: ЫзТЛпГ]) -> зСг: ...Обрабатывает список чисел через пайплайн..... гетигп р!ре(питЬегз, Т111:ег_еуеп, 5иш_а11, *сгта1:_ гезиИ) Продвинутые техники композиции Когда мы работаем с более сложными данными и операциями, обычных ргре- операторов может быть недостаточно. Продвинутые техники композиции вклю- чают в себя создание условных пайплайнов, обработку ошибок внутри компози- ции и создание переиспользуемых блоков обработки: Тгош 1:ур1пд ттрогС 0ртюпг1, Стоп бе^ сопс111:1опа1_р1ре(сопск1:1оп: оос1, Тгие_Гипс1:1оп : Са11аЫе[ [Алу], Апу], Та1зе_Т'ипс1юп : Ор11опа1[Са11аЬ1е[|Апу], Апу]] = Мопе): Создает функцию, которая применяется условно. Агдз: сопсПстоп: условие для выбора функции Сгие.бипсТгоп: функция для применения, если условие истинно Та1зе_’Гипс1:1оп: функция для применения, если условие ложно ЧеУигпз: функция, которая применяет соответствующую обработку II II II беТ сопсПТюпаГ-ргосеззог (уа!ое : Апу) -> Алу: 1Т сопсНИоп: гетигп 1гие_Типс'11оп(уа1ие) еИГ Га1зе_Типс1:1оп: гетигп Га1зе_1:ипсС1оп(уа1ие) е1зе: геСигп уа1ие # Возвращаем значение без изменений геУигп сопсИ1:1опа1_ргосе&зог бе* заТе_сПу1с1е( Лутзог : Т1са1): ...Создает функцию безопасного деления. 111 СеТ ЛУ1бе_Типс1лоп(уг1ие: Т1оа1) > 11п±оп[Т1оа1, ьй]: 1Т 61у1зог == Э; геТигп "Сшибка: деление на ноль" гетигп ма1ие / сПугзог геРигп агутбе.ГипсПоп Эти продвинутые техники позволяют создавать более гибкие пайплайны, ко- торые могут адаптироваться к различным условиям и обрабатывать исключи- тельные ситуации без прерывания всей цепочки обработки: бе* аауапсеа_питоег_ргосезз1пд(питЬег: ТТсас, зпос1б_боиЫе: роо1) ->
зСг: ...Демонстрирует продвинутую композицию с условиями. боиР1ег = ТатЬба х: х * 2 кЗепПту = 1атЬба х: х # Функция, которая не изменяет значение гезиИ = р!ре( питЬег, сопб±1:1опа1_р1ре(5Ьои1б_аоиЫе, боио1ег, 1бепТ1Ту), абб.Ттле, за(е ббу 1бе(3), 1атЬба х: ('Финальный результат: {х}" 1Г 1з1пе1:агсе(х, (1П1, Г1оаГ)) е1зе х ) геЬигп гезиК # Тестирование продвинутой композиции СезТ_сазез = [ (10, Тгие), # Число удвоится (10, Еа1зе), # Число не изменится (0, Тгие), # Проверка работы с нулем Тог питЬег, зЬоиТб.боиЫе 1п 1езТ_сазез: гезиИ: = асКапсес1_пи|т1Ьег_ргосезз1лд(питоег, зЬои1б_боиЫе) рг1гТ(Т"0брзботка (питает) (удвоение: {збои1б_боиЫе}): (гезиИ)") Композиция для обработки текста Обработка текста представляет собой идеальную область для применения композиции функций. Текстовые данные часто требуют последовательности операций: очистка, нормализация, токенизация, фильтрация и агрегация. Ка- ждая из этих операций может быт ь реализована как отдельная функция, а затем скомпонована в мощный пайплайн обработки: 1трог1: ге (гот Туртпд (трог! ЫвТ беТ с1еап_гехг(гехг; згг) -> з(г: 11 'Очищает текст от лишних символов и пробелов.'" 1 # Удаляем лишние пробелы и символы новой строки с1еапоб = ге. зиЬ( г'\з+ ' ‘ ', тех!. 51г1о()) # Удаляем специальные символы, оставляя буквы, цифры и базовую пунктуацию с1еапес1 = ге.зиЬ(г'[А\*Азс1еагеб) геЬигп с1еапеб бе( 1о_1о,легсазе(1:ех(: зтг) -> з(г: ..Приводит текст к нижнему регистру..... гебигп ГехГ. 1ог.'ег() бе( гетоуе_з!тог(:_иогбз(Гех(: зХг, плп.]епдГГт: 1п(: = 2) -> збг: ..Удаляет слова короче указанной длины,""' иогаз = 1ех1 .зр111:()
Г11Гегеб_шогбз = [логе! Гог люгб 1п иогбз 1Г 1еп(\мог<1) >= го1п_ 1епдГ.Ь] гебигп ' ' .]01п(Г11Гегеб_й0гбз) бе* Сокеп12е(ГехГ: збг) -> ЫзНзГг] 1 1 Разбивает текст на тскены (слова).... # Простая токенизация пс пробелам с удалением пунктуации Гокепз = ге.Г1пба11(г'\Ь\*л+\Ь’, ГехГ) гебигп Гокепз Создав базовые функции для обработки текста, мы можем комбинировать их б различных комбинациях для решения конкретных задач. Например, для под- готовки текста к анализу' настроений или для создания облака слов: бе* соипТ_логб_Ггеаиепсу( Гокепз: 1_131[зГг]) -> б!сГ: ..Подсчитывает частоту встречаемости слов....... Ггециепсу = {} Гог Гокеп 1п Гокегз: Ггециепсу[Гокеп] = Ггециепсу.деГ(Гокеп, 0) + 1 гебигп Ггедиепсу бе* Г11тсг_соттоп_иогбз(Ггед1епсу_б1сГ: бтсг, ттп_Ггедиепсу; 1пГ = 2) -> 61сГ: ..Оставляет только слова с частотой не менее указанной. "" геСигп {»огб: Ггец Гог вдэгб. Ггед тп Ггедиепсу_бтсб.1гетз() 1Г Ггец >= т1п_Ггециепсу} беГ дет_Гор_ио1'бз(Ггеаиепсу_б1сГ: бтсг, Гср_п: 1пГ = 10) -> ЫзГ[Гир1е]: ..Возвращает топ-М самых частых слов. 1 1 зогбеб..м/огбз = зогГеб(Ггедиепсу .бтсГ. 1Гетз(), кеу-1атоба х: х[1], геуегзе=Т гие) гебигп зогГеб_м/огбз[ :бор_п] # Создаем пайплайн для анализа текста бсГ апа1ухе_Гсхт(бехб: зГг, т1п_'логб_1спдг1"|: 1лГ = 3, т1п_Ггециепсу: 1пГ = 2, Гор_п: 1пГ = 5) -> 1_1зГ[Гир]е]: "" Анализирует текст и возвращает топ самых частых слов...... # Создаем специализированные функции с параметрами гетоуе_зГюгГ = 1атбба ГхГ: гегесуе_зпогГ_йо,'б8(1хГ, т1п_иогб_ 1епдГГ|) Г11Гег_ГгеднепГ = 1атоба тгед_б1сГ: ГПСег_соптоп_м/сгбз(Ггед_ б!сГ, т1п_*гедиепсу) дет_Гор = 1атЬба Ггед_б1сС: деГ_Сор_исгбз(Ггец_б1сГ гор_п) гебигп р!ре( ГехГ, с1еап_ГехГ, Го_1очегсазе, гешоуе .зПсгС,
бокелбге, соипС_л'ог б_Тгедиепсу, ТИТег.ТгедиепГ, дег_ Гор ) Теперь мы можем протестировать наш пайплайн обработки текста на реаль- ном примере. Этот подход демонстрирует, как композиция функций позволяет создавать сложные системы обработки данных из простых переиспользуемых компонентов: # Пример текста для анализа затр1е_1:ех1: = .. Функциональное программирование - это парадигма программирования, которая основана на концегции функций. В функциональном программировании функции являются основными строительными блоками программы. Функции могут быть переданы как аргументы другим функциям, возвращены как результат выполнения функций и присвоены переменным Это позволяет создавать гибкие и переиспользуемые программы. # Анализируем текст бор_и/огбз = апа1уге_1ех1:(затр1е_1ех'(:, т1п_иогб_1епд1б=4, т1п_ Тгедиепсу=2, Тор_п=5) рг1пГ("Анализ текста о функциональном программировании ') бог иогб, Тгецоепсу 1п Гор.л'огбз: рг1п1(Т" ’ {иогб} : {-Ггедиепсу} раз1) # Создаем более специализированный анализатор для коротких текстов због1:_1:ех1:_ала1.у7ег = ТатЬба Тех!: р!ре( 1ехС, с!еап_ТехТ, 1о_1оу»егсазе, 1атЬба 1x1: гетоуе..због1:_щогб8(1:х'1, 2), # Более короткие слова для коротких текстов Рокетте, соипб.иотскГгедиепсу, 1аггЬса Тгед_б1сТ: де1_бор_исгбз(Ггед_б1с1:, 3) # Топ-3 для кратности ) Композиция для обработки структурированных данных Работа со структурированными данными, такими как списки словарей или объекты, требует более сложных пайплайнов. В этом контексте композиция функций позволяет создавать мощные системы трансформации данных, кото- рые могут фильтровать, преобразовывать, группировать и агрегировать инфор- мацию: Ггот бурт-пд б-ггрогг ЭбсГ, Апу Тгот ФабеЩте 1трогб бсбеНте Гготг СипсГооЗз бироЩ гебиое
# Пример структурированных данных - список продаж за1ез_6аСа = [ {"16 ': 1, 'ргобисС1 : "1арСор", ’рп.се": 1200, диапСИу' : 2, баСе": '2024-01-15"}, {"16 : 2, "ргобисС": "тоизе", ' ргГсе ' : 25, "диапМСу ' : 5, "баСе" 2024-01 15'}, {"16 ': 3, 'ргоаисС": "кеуЬоаг 6' , "ргхее": 80 циапСИу' : 3, баСе": '2024-01-16"}, {"16 : 4, ргобисС": "1арТор". "ргхее": 1200, диапСИу' : 1, баСе": 2024-01-16"}, {"16 : 5, ргобисС": "топПог", ргхее : 300, циапМСу' : 2, баСе": 2024-01-17 '}, Се* аб6_СоСа1_Г1е16( гесогбз . С18С[01сС[зСг, Апу] ]) -> ЫзХ[01с1: [ 31:г, Алу]]: ...Добавляет поле СоСа1 как произведение цены на количество."1 гетигп [ {**гесогб, "То1а1": гесогб] рг1се"] * гесогб[1 цигпПСу 1 ]) Гог гесогб 1п гесогбз 6ег Г1Иег_Ьу_пип_СоТа1(т1П_То-(:а1: НоаГ) : " 1 Создает функцию фильтрации по минимальной сумме... 6еГ Г11Сег_ГипсС1оп(гесог6з: Ь1 зГ[г, Апу]]) -> ЫзС]01с1[з1г, Апу] ] . геСигп [гесогб *ог гесогб 1п гесогсз 1г гесогб.деС(’ЮСа1 , 0) >= 1п1п_1оса1] гетигп ГПтег_Гипс1юп беб дгоир_Ьу_ргобисС(гесогбз: Ы51[01с1[з1г, Алу]]) -> 01ст[зГг, 1_1зС[01с1:[зСг, Апу] ] ] : ..Группирует записи по продукту..... дгоиреб = {} Гог гесогб 1п гесогбз: ргобисб = гесогб[ "ргобис!"] 1Г огобисг пег хп дгоиреб дгоиреб[ргобис!] = [] дгоиреб[ргобис!].аррепб(гесогб) гетигп дгоиреб Создав базовые функции для работы с данными, мы можем строить сложные пайплайны аналитики. Каждая функция выполняет одну четко определенную операцию, что делает код тестируемым и поддерживаемым: беГ сэ1си1а1е_ргобис1_з1э1з( ргобисС_гесогбз: Пз1:[01сС[з1г, Апу]]) -> 01сС[з1:г, Апу] 1 1 Вычисляет статистику по продукту... 1Г го! ргобис1:_гесогбз геСигп {"соипС": 0, ЮСаТ-геуепие 1 : 0, 'а7д_огбег 1: 0) СсСа1_геуепие = зит(гесогб["СоСаТ.1] Гсг гессгб хп ргобисС_тессгбз)
соипб = 1еп(ргодисГ_гесогбз) ауд_огс!ег = То1а1_гееепие / соипГ гебигп { 'соипГ": соипГ, " Г.оГа1_геуепие 1 ГоГа1_ге>/епие, ’ауд.огаег": гоипс1(ауд_ отбег, 2) } с!еГ аддгедаГе_Ьу_ргос1исТ(дгоирес1_с1аГа: 01сГ[зТг, Ыз1[Э1сГ[зГг, Апу]]]) -> 01С1:[31 г, 01с1[з1г, Апу]] : Агрегирует данные г.о каждому продукту."" геГигп { р гос1 ис! : са1си1аТе_ргобисГ_зГаТз(гесогбз) Гог ргооисГ, гесогбз 1п дгоиреб_с1аГа . НетзС) } с!еТ зогТ_Ьу_геуепие(зТаТз_д1сГ: ОтсТ[зТг 01сТ[зТг, Апу]]) -> 1131[1ар1е] : ...Сортирует продукты по общей выручке...... ргодис18_1л/11:Ь_геуепие = [ (ргодись, зЬаЬз[ "ЬоГа1_геуепие" ]) Гог ргосисГ, зГаТз 1п зГаГз_д1сЬ.11етз() геГигп зогГед(ргодисЬз_л1ьб_геуепие, кеу=1атЬда х: х[1], геуегзе=Т гие) # Создаем комплексный пайглайн аналитики деГ апа1уге_8а1ез_даГа(за1е8_г есогдз : Из! [ 01с1 [ зЬ г, Апу]], ттП-СоСа!; НоаГ - 0) -> Из![1ир1е]: ...Анализирует данные продаж и возвращает продукты по выручке....... Г11тег_Ьу_Ьога1 = Г11Гег_Ьу_т1п_Тоса1(т1п_'Ьоьа1) геГигп р1ре( за!ез _гесогс!з. аддГога1_Г1е1д, Г11Тет_Ьу_ТоТа1, дгоир,Ьу_ргодисГ, аддгедаЬе_Ьу_ргодись, зог1:_Ьу_геуепие ) Теперь можем использовать наш пайплайн для анализа продаж с различны- ми параметрами фильтрации. Это демонстрирует гибкость композиционного подхода — мы можем легко изменять логику обработки, добавляя или удаляя функции из пайплайна: # Анализ всех продаж а!1_3а1ез_апа1уз1з = апа1уге_за1ез_да1:а(за1ез_да1:а) рг1п1:( 'Анализ всех продаж (по убыванию выручки): ) Гог ргодесГ, геуепие 1п а11_за1ез_апа1уз1з:
р гъпГ (Г 1 {ргобис!}: ${ геуепие}") рг1п1("\п" + "="*50 + 1\п") # Анализ только крупных продаж (ог $200) 1агде_за1ез_апа1уз1з = апа1у7.е_за1ез..баТа(за1ез .баба, т1п_Сота1=200) р гтпТ (11 Анализ крупных продаж (от $200);") Тог ргобисТ, геуепие 1п 1агде_за1ез_апа1уз13: рг1пТ(Т 1 {ргобисТ}: ${геуепие}") # Создаем специализированный анализатор для конкретной даты беТ сгеа1е_баТе_Т11Тегеб_апа1угег(ТагдеТ_баТе: зТг, т1п_ТоТа1: Т1оаТ = 9)- ...Создает анализатор для конкретной даты.’’ беТ ТПТег_Ьу_баРе( гесогаз: ЫзТ[С1сТ[ зТг, Апу]]) -> 1_1зТ[01сТ[зТг, Апу] ] : геТигп [гесогб Тог гесогб 1п гесогаз 1Т гесогб)"баТе" ] == ТагдеТ_ба1е] сеТ баТе_зрес1Т1с_апа1у2ег(за1ез_гесогаз: Ыз^Оз.С'ЦзТг, Апу]]) > ЫзТ[Тир1е] : Т111ег_Ьу_Ю1а1 - Т111ег_Ьу_(П1п_Тс1а1(п11п_Уо1а1) геТигп р1ре( за1ез_гесогбз, Т1 11сг_Ьу. баТе, а<1б_То1а1_Т1е]б. Т111ег_Ьу_То(:а1, дгоир_Ьу_ргобзсТ, аддгедаТе_Ьу_огоаисТ, зог1_Ьу_геуепае ) гетигп бате .зрес1Т1с_апа1ухег # Анализ продаж за конкретную дату )ап_15_апа1у2ег = сгеаге_баТе_Т111:е^еб_апа1у2е•"("2024-01-15 ) ]ап_15_гези1(з = ]ап_15_а,та1у2ег(за1ез_баТа) рг1пТ(Т"\пПродажи за 15 января 2024: ) Тог ргобисТ, геуепие 1п )ап_15_гези1Тз: рг!пТ(Т"{ргобисТ}: ${ге7епие)") Обработка ошибок в пайплайнах При работе с реальными данными неизбежно возникают ошибки: отсутству- ющие поля, некорректные типы данных, сетевые проблемы или логические ис- ключения. Композиция функций должна учитывать эти сценарии и предостав- лять механизмы для обработки ошибок без прерывания всего пайплайна: Тгош гуртпд ТпроП бпгоп, СаИаЫе Тгош Типс1со1з пт,рог! нгаоз
1трог1 1одд1пд # Настраиваем легирование для отслеживания сшибок 1одд1пд Ьаз1сСопТ1д(1еуе1=1одд1пд.1МРО) 1оддег = 1одд1пд.деТЬоддег(___пате__) сХазз РХреХтпеРезиХб: ...Класс для представления результата операции в пайгпайне. 11 ОеГ __1П1Т__(зеХТ, уаХие: Апу = Нэпе, еггог: збг = Ноле, Хз_ зиссезз: ЬооХ = Тгие): зеИ.уаХие = уаХие зеХТ, еггог = еггог зеХГ. Хз_зиссезз = 1з_зиссезз беТ___герг__(зеХТ) : 1Т зеИ 1з_зиссезз: гебигп Е"Зиссезз({зеХТ.уаХие})" е1зе: геЕигп Г'Еггог({зе1Т еггог))' беЕ заЕе_гипсЕХоп(бехааХт_7аХие: Апу = Иопе): ...Декоратор для безогасногс выполнения функций в пайплайне...... беЕ аесогаТог(Еипс: СаПаЫе) -> СаИаЫе: @и/гарз(Еипс) беЕ 'лгаррог(р1реИпс_гсзиИ: ипХоп[Р^реХ1пексзиХ1, Апу]) -> Р1ре11пеНези11:: # Если входное значение уже содержит ошибку, пропускаем выполнение 1Е 1з1пз1;аГ|Се(р1ре11ге_гезиИ, РХреХХпеРезиХЕ) апс! по! рХреХ1пе_гези11.1з_зиссезе: гебигп р1ре11пе_гезиТб # Извлекаем значение для обработки уа1ие = рХреИпе.гезиИ. уаХие 1Е 181пз1апсе(р1реХ1пе_ гезиХЕ, РтреИпеНезиХТ) е!зе рзреИпе-гезиП: тгу: гезиИ = Типс(уа1ие) геТигп Р1ре11пеРези1б(\-а1ие=гези11:, 1з_$иссезз=Тгие) ехсер!: ЕхсерИоп аз е: еггог_пеззаде = Г'Ошибка в {Типепапе______}: {збг(е)}" Ходдег.еггог(еггог_теззаде) гетигп Р1ре11пеРези1б( уа1ие=с1еТаи11:_уа1ие, еггог=еггог_теззеде, 1з_зиссезз=Еа]зе гебигп игаррег гебигп бесогаТог
Используя механизм безопасного выполнения, мы можем создавать устойчи- вые к ошибкам пайплайны, которые продолжают работать даже при возникно- вении исключений в отдельных функциях: Себ забе_р1ре(1п1С1а1_7а1ие: Апу, *6ипсб1опз: СаПаЫе) > Р1ре11псКези1б: ..Безопасная версия рбре-оператора с обработкой ошибок 111 гези1б = Р1реИпеРези1б(уа1ие=1п1б1а1_уа1ие, 1з_зиссезз-Тгие) бог бипсбтоп 1п бипсИопз: гези1т = 6ипст1оп( гези1б) # Если возникла ошибка, прекращаем выполнение пайплэйна 16 поб гезиИ. 1з_зиссезз : Ьгеак гебигп гезибб ©забе_6ипсб1оп(с1е6аи1б_уа1ие=0) беб забе_с11У1ае_ву_гего(питЬег: 61саб) -> 61оаб " "Функция деления, которая может вызвать ошибку.'11 гетит питЬег / 0 # Намеренно вызываем ошибку @забе_бипсб1оп(с1ебаи1б_уа1ие= И/А ) бе* забе_збг1пд_сопуегз1оп(пс,тЬег: 11пюп[1пб, 61саб]) -> збг: "" Безопасное преобразование числа в строку.......... 16 пиггЬег 1з Копе: габзе УабиеЕггог("Получено значение Попе") гесигп б'Результат: {питЬег}' @за6е..6ипсб1оп(бетаи1б_уа1ие-Э) бе-*" забе_здиаге(питЬег: Бп1оп[1пб, 61оаб]) -> Р1оаб; ...безопасное возведение в квадрат...... 16 поб 1з1пзбапсе(питЬег, (1пб, 61оаб)): габзе ТуреЕггог(6"0жидается число, получен {буре(питЬег)} ') гетигп питЬег ** 2 # Демонстрация работы с ошибками бе* бетопзбгате_еггог_11апс111пд(): ...Демонстрирует обработку ошибок в пайплайнах." # Пайплайн с ошибкой - деление на ноль гези1б_иббЬ_еггог = забе_р!ре( 10, забе_здиаге, забе_с1171с1е_Ьу_хего, # Здесь возникнет ошибка забе_зб!"1пд_соп7егз1оп # Эта функция не выполнится ) ргбпб(6"Результат с ошибкой: {гези1б_й1тЬ_еггог}") рг1.пб(6 'Финальное значение: {гези1б_«1бЬ_еггог уэ1ие}') # Успешный пайплайн зиссеззби1_гези1б = забе_р1ре(
5, 5аТе_8циаге, 1атЬба х: Р1ре11пеРези]Л(уа1ие=х/5, 18_8иссе8з=Тгие), # Безопасное деление заТе.зСгтпд.сспуегзюп ) рг!п1(Т1 Успешный результат: {зиссеззГиХ.гезиИ}") рг1пТ(Г Финальное значение: {зиссеззГи1_гезиИ.\/а1ие}1 ) бетоп8Ггэге^еггог_НапЛ1пд() Механизм обработки ошибок особенно важен при работе с внешними источ никами данных, где сетевые проблемы, недоступность сервисов или некор- ректные данные могут нарушить работу пайплайна. Безопасная композиция функций позволяет создавать отказоустойчивые системы обработки данных. Асинхронные пайплайны Современные приложения часто работают с асинхронными операциями: об- ращения к базам данных, АРЬвызовы, операции с файловой системой. Компо- зиция функций может быть адаптирована для работы с асинхронным кодом, что позволяет создавать эффективные пайплайны обработки данных: нпрогС азупсто йпрогС агэНССр йпрогС Стте Тгош Суртпд птрогС ЫзУ, БюС азупс с!еГ Те1с11_изег_с1с1:а(изег_1с1: 1П1) -> Б1с1:[зТг, Апу]: 1 'Имитирует асинхронное получение данных пользователя.... # Имитируем задержку сетевого запроса аиатб азупсю.з1еер(0.1) геТигп { '16' изег_1с1, "пате": 'Г"11зег_{изег_1с1}", ’етаИ": Г изег {иеег_1б}@ехапр1е.сот", "зсоге ': изег_1с! * 10 } азупс бе'! епг1сК_й,1'(б_аст1У1Ту(изег_ба1:э: 01с1[з1:г, Апу]) -> О1с1[5'1г, Апу] : 11"Обогащаем данные пользователя информацией об активности аиай азупс!о.з1еер(0.05) # Имитируем запрос к другому сервису асСгуну. зсоге = изег_ба1:а [" зсоге 1 ] * 1.2 геСигп { **изег_ба1:а, "асС1у1Су_зсоге": гоипб(асС1у1Су_зсоге, 2), "18_асС1уе : асИудЛу..зсоге > 50 } а.зупс беС са1си1а1:е_изег_га1:1пд(ияег_ба1:а: ОгсНзбг, Апу]) -> ОтсНзСг, Апу]:
11 Вычисляет рейтинг пользователя. "" алабТ азупс1о.81еер(0.03) Ьазе_зсоге = изег_баба["зооге"] асГ1711у_Ьопиз = изег_йаГа[1 асНу11:у_зсоге" ] * 0 1 гаПпд = Ьазе_зссге + асТ1\'1Ту_Ьопи8 гетигп { **изег_ба1:а, " гаНпд": гоигсЦГс11пд, 2), "Пег": "Ргетбит" 1Т гаттпд > 100 е1зе "ЗТапбагб" } азупс беТ азупс_р1ре( 1п11:1а1_уа1ие Апу, *азупс_Типс(:10П8) -> Апу. ""Асинхронная версия ртре-оператора." 1 1 гезиИ = 1пИ1а1_уа1ие Тог эзупс_ТипсТ1оп 1п азупс_ ТипсПопз : гезиИ = ЭИЭ11 азупс_ТипсПоп( гезиИ) гетигп гезиИ Асинхронные пайплайны позволяют эффективно обрабатывать данные, тре- бующие внешних запросов, не блокируя выполнение программы. Это особенно важно при обработке больших объемов данных или при работе с медленными внешними сервисами: азупс беТ ргосезз_изег_р1ре11пе(изег_1б: 1пТ) -> О1сб[з1г, Апу]: ..Обрабатывает данные пользователя через асинхронный пайплайн..... гетигп аиаИ азупс_рюе( изег_тб, ТеСсб .изег .аата, епг1сН_л1Тп_аст1У1бу, са1си1аТе_изег_ гаПпд ) азупс аеТ ргосезз_ти1т1р1е_и5еге(и5ег_1бз. ПзТЦпТ]) -> 1_1зТ[О1сС[зТг, Апу] ] : ..Обоабатывает данные нескольких пользователей параллельно # Создаем задачи для параллельного выполнения Сазкз = [ргосез8_изег_р1ре11пе(изег_1а) Тог изег_1б 1п изег_1бз] # Ожидаем завершения всех задач гезиИз = аиатТ азупсю.сат1пег(*Сазкз) геТигп гезиИз азупс беТ беторз1гаГе_азупс_р1ре11пе(): ..Демонстрирует работу асинхронного пайплайна...... 81агб_б1те = Типе.Т1те() # Сбрабатываем данные нескольких пользователей параллельно изег_1бз = [1, 2, 3, 4, 5] ргосеззес1_изег8 = алаИ ргосез8_ти1Т1р1е_изег8(изег_1бз) епб_С1те = Пте.Пте() ргосеь81пд_Т±те = епб.Пте - 8Таг1_Т1те
рппЦГ Обработано {1еп(ргосе8зе6_изегз)} пользователей за {ргосезз1пд_Т]те: ,2Т} секунд1') Тог изег 1п ртосеззес1_изегз: рг1пГ(-Г 'Пользователь {изет[ ‘ 16 ]}: рейтинг {изег[' гаНпд’ ]}, уровень {изег['11ег']} 1) # Запуск асинхронной демонстрации # азупс1о.гип(6етопз1:га1е_азупс_р1рс11г:е())
Асинхронное функциональное программирование Асинхронное поограммирование и функциональный подход на первый взгляд могут показаться противоположными концепциями. Традиционное функциональное программирование оперирует детерминированными, пред- сказуемыми функциями, которые для одинаковых входных данных всегда возвращают одинаковый результат. Асинхронность же вносит элемент не- предсказуемости во время выполнения операций. Однако при более глубоком рассмотрении становится очевидно, что эти подходы прекрасно дополняют друг друга, создавая мощную комбинацию для решения современных задач. Эта глава познакомит вас с тем, как элегантно сочетать принципы функцио- нального программирования с асинхронными возможностями РуЙтоп. Вы нау- читесь создавать композиции асинхронных функций, работать с потоками дан- ных и применять функциональные паттерны в асинхронном контексте. Теоретические основы асинхронного функционального программирования Асинхронное функциональное программирование основывается на концеп- ции корутин (согошшез) — специальных функций, которые могут приостанав ливать свое выполнение и передавать управление обратно в цикл событий. В от- личие от обычных функций, корутины не блокируют выполнение программы во время ожидания результата медленных операций. Ключевая идея заключается в том, чго асинхронные функции остаются чи- стыми в функциональном смысле — они по-прежнему зависят только от своих аргументов и не имеют побочных эффектов, влияющих на глобальное состояние программы. Единственное отличие состоит в том, что результат возвращается не немедленно, а в виде объекта Ритоге или СогоеНпе, который можно ожидать с помощью ключевого слова элатТ. Представьте себе кулинарную аналогию: обычная функция похожа на при- готовление блюда от начала до конца без перерывов. Асинхронная функция на- поминает работу шеф-повара, который может переключаться между несколь- кими блюдами — пока одно тушится, он нарезает овощи для другого, а затем возвращается к первому когда оно будет готово. При этом каждый рецепт оста- ется детерминированным — одни и те же ингредиенты и последовательность действий всегда дают одинаковый результат. В контексте функционального программирования асинхронность не нару шает принципов иммутабельносги и отсутствия побочных эффектов. Корутины возвращают новые значения, не изменяя существующие данные. Композиция асинхронных функций работает по тем же правилам, что и композиция син- хронных функций, но с дополнительной возможностью параллельного выкол нения операций.
Цикл событий (еееЩ 1сор) служит координатором, управляющим выполнени- ем корутин. Он решает, когда приостановить одну корутину и запустить другую, обеспечивая аффективное использование времени процессора В функциональ- ном контексте цикл событий можно рассматривать как механизм применения функций к потокам данных, где каждый элемент потока обрабатывается асин- хронно Монадический подход к асинхронности позволяет нам думать об асинхрон- ных вычислениях как о значениях, обернутых в контекст выполнения. Это оз- начает, что мы можем применять к ним привычные функциональные опера- ции — отображение, фильтрацию, свертку, — но с учетом асинхронной природы вычислений. Ругйоп предоставляет все необходимые инструменты для реализа- ции таких паттернов через модуль азупс1о и синтаксис азупс/алаИ. Основы азупс/ашаН в функциональном стиле Переход от синхронных функций к асинхронным в функциональном стиле требует понимания того, как ключевые слова азупс и аиаИ влияют на поведе- ние функций. Асинхронная функция определяется с помощью азупс беТ и может содержать точки ожидания, обозначенные аиат!. Важно понимать, что такая функция не выполняется немедленно при вызове, а возвращает объект корути- ны, который необходимо явно запустить: йлрог! азупсю Ггот 1ур1пд 1трогР СаИаИе, Апу азупс беТ риге_азупс_1:ипс1:1оп(^а1ие: 1п1) -> 1п1: Чистая асинхронная функция, которая удваивает переданное значение. имитирует асинхронную операцию с помощью з!еер. # Симулируем асинхоонкую операцию аиаИ азупсю.з1еер(0.1) геТигп уа1ие * 2 азупс беТ бетопз1:га1:е_0а81с_а8упс(): '""Демонстрация базового использования асинхронных функций.""' гезиИ = элаИ риге_эзупс_-Гипс11оп(5) рг1п1(Г Результат асинхронной функции: {гезиИН) # Запуск асинхронной функции азупсю. гип(бетопз1:га1:е_Ьэз1с_азупс()) В этом примере риге_азупс_Типсгюп остается чистой функцией в функци- ональном смысле — она зависит только от аргумента уа1ие и всегда возвра- щает удвоенное значение. Асинхронность не нарушает детерминированность функции, а лишь изменяет механизм получения результата. Ключевое сло- во ала 1г указывает точки, где функция может быть приостановлена для выпол- нения других задач. Композиция асинхронных функций требует особого внимания к точкам ожидания. Каждый вызов асинхронной функции должен быть отмечен аий,
что может показаться громоздким на первый взгляд. Однако это дает явный кон- троль над тем, тде именно осуществляются асинхронные операции: азупс беТ ти11:Фр1у_азупс(х: ФпФ, ФасТог: ФпФ = 2) -> ФпФ: ..Асинхронно умножает число на коэффициент...... аиаФФ азупсФо.з1еер(0.05) # Имитируем вычислительную задержку геФигп х * ГасТог азупс беС аСс1_аяупс(х: ФпФ, ас1с1епс1: ФпФ = 1) -> ФпФ: ..Асинхронно прибавляет число к значению...... амагф азупсФо з1еер(0.05) # Имитируем вычислительную задержку гетигп х + аббепб азупс йеФ сотрозе_азупс_соега1:Фопз(уа1ие: ФпФ) -> ФпФ: II II II Композиция асинхронных операций: сначала умножаем, затем прибавляем. Каждая опеоаиия выполняется последовательно с ожиданием результата. # Последовательная композиция - результат первой операции # передается во вторую пиФТюФФес! = а'латФ ти1Ф1р1у_азупо(маФие, 3) гезиИ = аиаФФ а<Фо_азупс(ти11:Фр1Феб, ’10) геФигп гезиИ # Демонстрация композиции азупс беТ гип_сотрозФТФоп_ехатр]е(): зГагГИФте = азупсто.деФ_еуепТ_1оор().ФФте() гезиИ = аиаФФ сотросе_азупс_орегаТ1опз(5) епсЦФте = азупсю .деФ_еуепФ_1сор() Л1те() ргФпФ(-Г'Результат композиции: {гезиИ}1) ргФпТ(Т"Ьремя выполнения: {епс!_1:Фте зФагГЛФте ,2Т} секунд") азупсФо. гип( гип_сотрозФ1:Фоп_ехатр1е()) Последовательная композиция асинхронных функций работает аналогично композиции обычных функций, но каждый шаг должен ожидать завершения предыдущего. В данном примере общее время выполнения составляет около 0.1 секунды, поскольку операции выполняются одна за другой. Это подходит для слу- чаев, когда результат одной функции необходим для выполнения следующей. Создание функций высшего порядка для работы с асинхронными функция- ми требует понимания того, как передавать и вызывать корутины. Асинхронная функция высшего порядка принимает другие асинхронные функции в качестве аргументов и возвращает новую асинхронную функцию: Тгот УурФпд ФтрогФ Туре^аг, АлаФтаЫе Т = Туре^'аг('Т') II = ТуреУаг( 'II') азупс <1еГ азупс_шар_зФпд1е(Типс: Са11аЫе[[Т], АйаФ1аЫе[К] ], хаФие: Т) -> 0: II II II Применяет асинхронную функцию к одному значению. Эквивалент тар для одного элемента в асинхронном контексте.
гебигп аиаН Гипс(уа1ие) азупс беГ а8упс_сотрозе( Гипс! Са11аЫе[[Т], Аиа1ТаЫе[11] ], Типс2: Са1±аЬ1е[[б], Ала1ГаЫе[Апу] ] ) -> Са11аЬ1е[ [Т], АйаНаЫе[Апу] ] • Создает композицию двух асинхронных функций. Возвращает новую асинхронную функцию. азупс беб сотрозесЦуа1ие: Т) -> Апу: тптегтеатате = аиаИ Гипс1(ма1ие) гебигп алатТ Типс2(1пбегтеб1а1:е) гебигп сотрозеб # Использование асинхронной композиции азупс ФеТ бетоп8бгабе_гзугс_сотроз1б1оп(): # Создаем композицию функций соп'розеск'Гипс = а«е!б азупс_соп'ро8е( 1атЬс1а х: ти1б!р1у_азупс(х, 2), 1атЬба х: абб азугс(х, 5) ) # Применяем композииию гези1б = атабб сотрозеб_Типс(10) рг1пб(Г Результат асинхронной композиции: {гези1б}') # Об * 2) + 5 = 25 азупсбо. гип(бетоп8бгабе_азупс_сстро81б1оп(’)) Асинхронные функции высшего порядка позволяют создавать гибкие ком- позиции, которые можно переиспользовать с различными наборами функций. Важно заметить, что сама функция ззупс_сотрозе является асинхронной, по- скольку она должна вызвать переданные асинхронные функции для создания композиции. Параллельная обработка и конкурентность Одним из главных преимуществ асинхронного программирования являет- ся возможность выполнения операций параллельно, когда они не зависят друг от друга. В функциональном контексте это означает применение функций к не- зависимым данным одновременно, что значительно увеличивает производи- тельность программы: азупс беб р'-осезз_1бет_а8упс(1бет; збг, Фе1ау: Т1оаб = 0.1) -> збг: Асинхронно обрабатывает один элемент данных. Имитирует обработку с помощью задержки. рг1пб(б'Начинаем обработку: {Нет}") алабб азупсбо з!еер(бе1ау)
гезиН = Г'Обрабстано : {Нет.иррег()}" рг!п1:(Т "Завершена обработка: {гезиН}") гетигп гезиИ азупс беТ 8едиеп1:1а1_ргосезз1пд(Иет8: 11зГ[з1:г]) -> ИзНзРг]: Последовательная обработка элементов. Каждый элемент ждет завершения предыдущего. гезиИз = [ ] 81аг1:_г1п1е = азупс!о дес .емепС ,1оор() Л1те() Тог Нет 1п Иетз1 гезиН = ам/аИ ргосезз_Пет_азупс( Иет) гезиИз .аррепЩ гезиН) епсШте = азупсто,де1_еуепТ_1сор()И1те() рг1пТ(Т"Псследовательчая обработка заняла: {епб_Ите - зСаЩ Ите: ,2Т) сек 1) гетигп гезиИз # Демонстрация последовательной обработки азупс беТ бепю_зедиепТ1а1(): Иетз = ["элемент!", 'элемент? 1, 1элементЗ"] гезиИз = аи/аИ зедиег1Т1а1_ргосе581пд(Иетз) Тог гезиН 1п гезиИз: рг1пТ( гезиИ) эзупсто.гип(бето_зедиеп11а1()) Последовательная обработка выполняет операции одну за другой, что при- водит к суммированию времени выполнения каждой операции. Для трех эле- ментов с задержкой 0.1 секун ды каждый общее время составит около 0 3 секун- ды. Данный подход удобен, когда результат обработки одного элемента нужен для обработки следующего. Параллельная обработка независимых элементов позволяет значительно со кратить общее время выполнения за счет одновременного выполнения опера- ций. Руйюп предоставляет несколько способов организации параллельного вы- полнения асинхронных задач: эзупс беТ рага11е1_ргооезз1пд(Иетз: Из![збг]) -> 11зТ[зТг]: II II II Параллельная обработка элементов с помощью азупсто.даТНег. Все элементы обрабатываются одновременно. зТаНЛзле = азупс1о.де'С_еуеп1:_1оор() И1те() # Создаем корутины для всех элементов согоиИпез = [ргссе83_Ие1П_азупс(Ие1Т1) Тог Иет 1п Иетз] # Запускаем все корутины параллельно гезиИз = алаИ а8упс1о.сатбег(*согоис1пе8)
епсШте = азупсГо.де1_еуегп_1оор() .11те() ргитЦТ"Параллельная обработка заняла: {епдИГте - з1агС_ Тите: ,2Г} сек") геСигп гезиИз # Демонстрация параллельной обработки азупс бе'! бетэ_рагэ11е1(): Пета = ["элемент!1, 'элемент?", "элементЗ"] гезиИз = аиаИ рага11е1_ргосезз1пд(Иетз) Гог гезиИ 1п гезиИз: рг1пг( гезиИ) азупсьо. гип(с!ето_ра 'а11е1()) Функция азупсю. дагбег запускает все переданные корутины параллель- но и ждет завершения всех операций. Время выполнения в атом случае будет приблизительно равно времени самой долгой операции (около 0.1 секунды), а не сумме времени выполнения всех операций. Это демонстрирует удобство асинхронного программирования для решения независимых задач. Более гибкий подход к параллельной обработке данных предоставляют за- дачи Цазкз). Создание явных задач дает больше контроля над их выполнением и позволяет обрабатывать результаты по мере их готовности: азупс беГ Г1ехГЬ1е_рага11е1_ргосезз1пд(Иетз: 11зГ[зСг]) -> 11зС[зСг] Гибкая параллельная обработка с использованием Тазкз. Позволяет обрабатывать результаты по мере готовности. зСагС_Г1те = азупсго.деС_еуепС_1оор().С1те() # Создаем задачи для каждого элемента Тазкз = [азурс1о.сгеаТе_Га8к(ргосезз_Иегг_азупс(Иет)) Гог Пеп хп Иетз] # Собираем результаты по мере готовности гезиИз = [ ] Гог Газк 1п азупс1о.аз_ссп1р1еСес1(1азкз): гезиИ = алаИ газк гезиИз аррепс!(гезиИ) рг1пС(Г 'Получен промежуточный результат: {гезиИ}' ) епсСГйпе = азупсьо.деТ_еуепС_1оор()Н1те() рг1г,1(ГТибкая обработка заняла: {епс1_11п1е - зСагТ_Г1те: ,2Г} сек") геСигп гезиИз # Демонстрация гибкой параллельной обработки азупс бе( с!ето_Г1ех1Ые(): Иетз = ["быстрый", "медленный", "средний"] гезиИз = а^аИ Нех1Ые_рага 11е1_ргосезз1пд(Иетз) рг1пГ("Итоговые результаты: ') Гог гезиИ хп гезиИз:
ргбпб(Т' {гези1б}") азупсбо. гип(бето_Т1ех1ббе()) Использование азупсбо.аз_сопр1ебеб позволяет обрабатывать результаты в порядке их готовности, а не в порядке запуска задач. Это особенно полезно, ког- да операции имеют различное время выполнения, и вы хотите начать обработку результатов, не дожидаясь завершения всех задач. Композиция асинхронных функций Создание сложных асинхронных вычислений через композицию простых функций является фундаментальным принципом функционального програм мирования. В асинхронном контексте композиция требует особого внимания к управлению потоком выполнения и правильному использованию операторов ожидания: Тгот Типсбсобз бггрогб игарз Тгот бурбпд бтрогб СаПаЫе, А«аббаб1е, Апу ФеТ азупс_сиггу(Типс: СаПаЫе) -> СаПаЫе: Декоратор для каррирозания асинхронных функций. Позволяет частично применять аргументы к асинхронным функциям II II II Фигарз^бипс) Реб сиггбеб(*агдз, *хклагдз): 6Т 1еп(агдз) + 1еп(к1л'агдз) >= Сипе._собе__.со_агдсоипб : # Если переданы все аргументы, вызываем функцию гебигп бипс(*агдз, **ккагдз) е1зе: # Иначе возвращаем частично поимененную функцию беб рагбта1(*тоге_агдз, ’к*тоге_киа-дз) : гебигп сиггбеб(*(агдз + тоге агдз), **{**клагдз, **тоге_кйагдз}) гебигп рагтба! гегигп сиггбеб @азупс_сиггу азупс беТ азупс_орегат1оп(базе: бпб, тибббрббег: бпб, абсбпоп: гпб) -> бпб: Асинхронная операция с тремя параметрами. Вошолняет (базе * тибббрббег) + абббббоп с задержкой II II II алабб азупсбо.збеер(0 05) гезпбб = базе * тиббьрббег + аббббюп гебигп гезибб # Демонстрация кароироеания асинхронных функций азупс беб бето.азупс_сиггу(): # Частичное применение - фиксируем множитель и добавление бооб1е_апб_абб.беп = азупс_ооегаббоп(.тибтбр1бег=2, абббббоп=10)
# Применяем к различным базовым значениям гезиИз = анаИ азупс1О.да1Ьег( соиЫе_апб_абб_Геп(5), # (5 * 2) + 10 = 2Р ооиЫе_апб_абб_Геп(7), # (7 * 2) + 10 = 24 ФоиЫе_апб_абб_ Геп(З) # (3 * 2) + 10 = 16 ) рг1пГ("Результаты каррированной функции:") Гог 1, гезиИ 1п епитегаге(гезиИз, 1): рг1п1(Г" Результат {!}: {гезиН}") азупсто. гип(бето_азупс_сиг гу()) Каррирование асинхронных функций позволяет создавать специализиро- ванные версии общих операций. В данном примере мы создали функцию, ко- торая удваивает значение и прибавляет десять, применив частичную апплика- цию к более общей функции. Это демонстрирует, как функциональные техники адаптируются к асинхронному контексту. Создание пайплайнов (рщеПпез) из асинхронных функций позволяет строить сложные обработки данных из простых переиспользуемых компонентов. Каж- дый этап пайплайна представляет собой асинхронную функцию, которая при нимаег результат предыдущего этапа; Ггот Сур1пд ГтрогС ТуреУаг, Са11аЬ1е, АиаИа01е Г = ТуреУаг('Т') с!азз АзупсРГреИпе: Класс для создания пайплайнсв из асинхронных функций. Поддерживаем цепочечный вызов и композицию операций. беГ___1пН____(зе1Г, 1п1ГГа1_Гипс: Са11аЫе[[Т], АлаНаб1е[Апу ] ] = Иопе): зе1Т . бипсНопз = [ ] 1Г 1п1Г1а1_Гипс: зеТГ.ГипсГюп.з ароепб(1п11:1а1_Гипс) бе-Р ДЬеп(зе1Г, Гипс: Са11аЫе[ [Апу], АкаИаЫе[Апу] ]) -> 'АзупсР1ре11пе' II II II Добавляет функцию в пайплайн. Возвращает новый пайплайн для цепочечных вызовов. II II II пеи_р1ре11пе = АзупсР1ре11пе() пеи_р1ре11пе. ГцпсПопз = зе1Г .ГипсНопз + [Гипс] геГигп пеи_р1ре11пе азупс беГ ехесиГе(зе1Г, 1пН1а1_уа1ие: Т) -> Апу: Выполняет весь пайплайн, передавая результат каждой функции следующей.
сиггепб_уабие = бпбббаб_уабие Тог Типе бп зебТ.Типсббопз: сиггепб_уабие = алабб Типс(’сиггепб_уабие) геТигп сиггепб_уабие # Определяем этапы обработки данных азупс беТ уабббабе_тприб(баба: зТг) -> збг: ......Проверяет входные данные..... аиабб азупсбо.збеер(0.01) 6Т пот ааба ог 1еп(бата.збгбр()) == 0 габзе УабиеЕггог("Пустые данные не допускаются") гетигп баса збгбр() азупс беТ бгапзбогт_То_иррег(баба: збг) -> збг: ......Преобразует данные в верхний регистр. 1 1 аиабб азупсбо.з1еер(0.02) гебигп баса.иррег() азупс беТ абб_р-еТбх(баба: збг) -> збг: ......добавляет поефикс к данным. аиабб азупсбо.з1еср(0.01) гетигп Т 'РК0СЕ88Е0: {баба}" азупс беГ сабсибабе_бепдбИ(баба: збг) -> ббсб: ...Вычисляет длину стооки и возвращает результат."11 алатб азупсбо.збеер(0.01) гетигп { "огбдюаб": баба, "бепдбН": 1еп(баба), 1ргосеззеб _аб": азупсбо.деб.еуепб_боор().ббте() } # Создание и использование пайплайна азупс беТ бепо_азупс_р1реббпе(): # Создаем пайплайн обэаботки рбреббпе = (АзупсРбре11пе(уа1ббабе_1приб) . б1леп(бгапзТо|Гт_бо_иррег) .бМеп(абб_рге1:1х) бНеп(сабсибате_бепдб1т)) # Обрабатываем различные входные дачные безб_баба = [" Ьеббо даогбб , руббоп 'азупсбо збагб_ббпе = азупсбо.деб_еуепб_1оор().ббте() гези!бз = алабб азупсбо.дабИег( *[рбреббпе.ехесибе(баба) Тег баба бп безб_баба] ) спа.ббте = азупсю.деб_еуепб_1оор() .ббте() ргбпб( Результаты пайплайна:1) Тог гезибб бп гезиббз: ргбпб(Т {гезибб}")
ргтп-ЦГ'Время выполнения: {елсСИте - зСагСНте: .ЗТ} секунд") а.зупсго. гип(с1ето_аяупс_р1ре11пе()) Пайплайн позволяет декларативно описать последовательность обработ- ки данных, где каждый этап выполняет конкретную задачу. Важно отметить, что при параллельном выполнении нескольких пайплайнов каждый проходит все этапы независимо, чго обеспечивает максимальную производительность для пакетной обработки данных. Мемоизация асинхронных функций требует особого подхода, поскольку ре- зультатом асинхронной функции является корутина, а не непосредственное зна- чение. Правильная реализация должна кешировать завершенные результаты, а не корутины: 1прог1 Типсбоо1з Тгот Турзпд ттрог! Э1сТ, Апу, ТирЗе ОеТ азупс_тето12е(Типс: Са11аЫе[..., АиаИаЫе[Апу] ]) -> Са11ао1е[..., А\чаИаЬ1е[Апу] ] : Дексратор для мемсизации асинхронных функций. кеширует результаты вызовов для повышения производительности. сасбе: 01с1:[Тир1е[Апу, .. ], Апу] = {} @Типсгоо1з.«гарз(Типс) азупс беТ тето17есЦ*агдз, **клагдз) : # Создаем ключ кеша из аргументов сасЬе_кеу = агдз + Тир]е(зог1:ес1(к\л.,агдз. Иетз())) И сасГ1е_кеу 1п сасйе: рг1п1:(Г Используем кешированный результат для {агдз}1) гетигп сас1зе[сас11е_кеу] # Вычисляем результат и кешируем его рг!пТ(-Г 'Вычисляем новый результат для {агдз}") гезиИ = а ла И Тигс(*агдз, **киагдз) сасбе[сас1зе_кеу] = гезиИ гетигп гезиИ # Добавляем методы для управления кешем те1то17.ес1.сасЬе_с1еаг = 1атЬс1а : сасЬе ,с1еаг() тетохгес!. сасйеДпТо = 1атЬба: ^''Размер кеша: {1еп(сасЬе)} элементов гебигп тетс17ес1 @а5упс_тепю12е азупс СеТ ехрепз1уе_са1си1а1:1оп(п: хп!) -> тпТ: Дорогостоящее вычисление числа Фибоначчи с использованием асинхронности.
Демонстрирует эффект мемоизации. рг1п1:(Т" Выполняем расчет для п={п}") аиаК азупсю. з1еер(0.1) # Имитируем долгое вычисление 1Т п <= 1: геРигп п гебигп аиаИ ехрепз1уе_са1си!а1:1оп(п-1) + аиат!: ехрепз1уе_ са1си1а1:1оп(п-2) # Демонстрация мемоизации азупс ДеТ е1ето_тето:1га11оп(): рг1п1("Первый вызов (без кеша):") з Та г С Щите = азупсю. де1_еуеп1:_1оор(). 11те() гезиШ = а«а11 ехрепз1уе_са1си!а1юп( 5) епб_11п>е = азупсю.де1_еуеп1:_1сор() Л1те() рг1п1(Т 'Результат: {гезиШ}, время. {епсШте - 81:аг1:_11те:.2Т} сек") рг1пГ("\пВторсй вызов (с кешем): ) зТагР.Пте = азупсЮ.де1-_еуеп1:_1оор(). 11те() гези11:2 = алаг!: ехрепз1уе_са1си1а1юп(5) епР_1:1те = азупсю .деИ_еуеп1:_1ооо(). Г1те() рг1п1(Т 'Результат: {гези11:2}, время: {епсЦюе зЮгР-Ите: ,2Т) сек") рг1п1(Т"\п{ехреп51Уе_са1си1а(:юп.сасбе_1пГо()}") азупсю. гип(с1ето_тето17а1:1оп()) Мемоизация асинхронных функций показывает значительное улучшение производительности при повторных вызовах с теми же аргументами. В примере первый вызов требует выполнения всех вычислений, а второй вызов мгновен- но возвращает кешированный результат. Это особенно ценно для рекурсивных асинхронных функций, где промежуточные результаты могу г использоваться многократно. Обработка потоков данных с азупсю Работа с потоками данных в асинхронном контексте открывает возможности для эффективной обработки больших объемов информации в реальном време- ни. Функциональный подход к потокам данных позволяет применять знакомые операции — отображение, фильтрацию, свертку — к асинхронно поступающим данным, создавая элегантные и производительные решения. Асинхронные генераторы представляют собой мощный инструмент для соз- дания потоков да] шых, ко г орые ген ер]груются по требованию В отличие от обыч- ных генераторов, они могут выполнять асинхронные операции между генераци- ей элементов, что делает их идеальными для работы с внешними источниками данных.
1трог1 азупсю бтрогС гапбот Тгот Сургпд ттрог-!: ЛзупсбепегаСсг, Апу азупс Се^Г азупс_ба1:а_зоигсе(соип1:: вЩ бе1ау_гапдс: Гир1е = (0.1, 0.3)) -> Азупс6епега'1сг[1гЛ, Иопе]: Асинхронный генератор данных, имитирующий поток от внешнего источника. Генерирует числа со случайными задержками между элементами +ог 1 1п гапде(соип1:): # Имитируем задержку получения данных ст внешнего источника се1ау = гапбот. ип1Гогт(*бе1ау_гэпде) алатД азупсю. з1еер( с!е1ау) # Генерируем случайное значение уа1ие = гапс!от гагсПпД("1, 100) рг1п1(Г"Получено значение: {уаТсе}1) у±е!с1 уа1ис азупс беГ сопзите_с1а1:а_зтгеап1(): Потребитель асинхронного потока данных. Демонстрирует базовую обработку элементов по мере их поступления. рг1пр( Начинаем обработку потока данных...") зХагД.ттте = азупсю.де1_еуеп1_1оор() ,т11ге() С<Ла1_зит = 0 соипР = 0 # Обрабатываем элементы потока го мере их появления азупс Тог уа!ие ю азупс_ба1:а_зоигсе(5) : Гоба1_зиш += уа1ие соип! += 1 ауегаде = 1оГа1_зит / соипб рг1пИ(Г'Обработано {соипи} элементов, среднее: {ауегаде:.2Т}") епб_Г1те = азупсю .де1_еуеп+:_1ооэ(). С1п,е() ргю'ЦТ"Поток обработан за {епсЦипе - зСагТ_Т1гге: ,2Г} секунд") рг1п1(Т"Итоговая сумма: {Со1:а1_зит}, сроднее: {ЮТаТ-Зит / соипС:.2Г}") азупсю. Г1'п(сопзите_бата_з1:гбап1()) Асинхронные генераторы позволяют обрабатывать данные по мере пх по- ступления, не дожидаясь получения всего набора данных. Это особенно важно при работе с потоками данных неизвестного размера или данными, поступаю- щими в реальном времени В примере каждое значение обрабатывается сразу после получения, что обеспечивает минимальную задержку обработки.
Функциональные операции над асинхронными потоками требуют адапта- ции привычных паттернов к асинхронному контексту. Создание асинхронных версий пар, Т111:ег и гебисе позволяет применять функциональный подход к потоковой обработке данных: Ггот Гуртпд ИрогГ АзупсИегаЬХе, Са11аР1е, ТуреУаг, АзупсбепегаГог Т = ТурсУаг('Т’) О = ТурсУаг( 'II') азупс беГ азупс_ггар( Гипс: Са11аЫе[[Т], АмаИаЫе[и]] азупс_ИегаЬХе: АзупсИегаЫе[ Г] ) -> АзупсСепегаТог[и, Мопе]: Асинхронная веосия гпао - применяет функцию к каждому элементу потока. Каждый элемент обрабатывается асинхронно по мере поступления. азупс Гог Неги 1п азупс_ИегаЬХе: гезиИ = ашаИ Гипс(Иет) у1е!с1 гезиИ азупс беГ азупс_Г1Иег( ргебХсаГе: Са11аЬ1е[ [Т], АлаИаЫе[Ьоо1] ], азупс_ИегаЫе: АзупсИег аЫе[ Г] ) -> АзупсбепегаГог[Т, Мопе]: Асинхронная веосия Г1ХГег - фильтрует элементы потока по условию. Проверка условия выполняется асинхронно. II II II азупс Гог Нет Хп азупс_ИегаЫе: ХГ аиаИ ргеб1саСе(Иет): уХеХб Нет азупс беГ азупс_гебисе( Гипс: СаИаЫе [ [ II, Т], АиаИаЫе[11] ], азупс_ИегаЬ1е; АзупсИегаЬХе [Т], ХпХГХаХ: II ) Асинхронная версия гебисе - свертывает поток к одному значению. Функция свертки применяемся асинхронно к каждому элементу. асситиХаГог = ХпХГХаХ азупс *ог Нет 1п азупс_ИегаЫе. асситиХаГсг = амаХТ Гипс(асситиХагог, Иеп) гетигп асситиХаГог # Определяем асинхоонные функции для обработки азупс беГ зциаге_азупс(х: ХпГ) -> ХпГ: 1 1 Асинхронно возводит число в квадоат. алаИ азупсХо зХеер(6.01) # Имитируем вычислительную задержку
геРигп х * х азупс с1еТ 1з_еуеп_азупс(х: 1пГ) -> Роо!: '1"Асинхронно проверяет, является ли числе четным. 1 1 аиаИ азупс1о.з1еер(0.005) # Имитируем задержку проверки геРигп х % 2 == 0 азупс с1еТ абс1_а5упс(асс: 1пР, х: 1пР) -> ТпР: '""Асинхронно складывает аккумулятор с текущим значением. аиаИ азупего зЗеер(0.005) геРигп асе + х Эти функции составляют основу для функциональной обработки асинхрон- ных потоков. Важно отметить, что каждая операция сохраняет асинхронную природу обработки, позволяя эффективно использовать время ожидания вво- да вывода для выполнения других задач. Композиция асинхронных потоковых операций позволяет создавать слож- ные пайплайны обработки данных из простых переиспользуемых компонентов. Каждый этап пайплайна может выполняться параллельно с другими этапами, обеспечивая высокую пропускную способность: азупс ФеР сгеаСе_питЬсг_зггеат(8Тагг. 1пР, епб: 1пР) -> АзупсбепегаРог[1п1, Мспе] : Создает поток чисел в заданном диапазоне с небольшими задержками. Имитирует поступление данных с постоянной скоростью Рог 1 1п гапде(зРагС, епб + 1): ала 11: азупс1о.з1еер(0.02) у!е1с! г азупс беГ 0ето_зСгеат_р1ре11пе(): Демснотриоует композицию асинхронных потоковых операций. Создает пайплайн: чиста -> квадраты -> четные -> сумма. рг1пТ( Создаем пайплайн обработки потока:") рг!п1:( "Числа 1-10 Квадраты Только че!ные Сумма ) зСагТ_С1те = азупего. деб_ехеп'1_1оор(). Риге1') # Создаем поток исходных данных питЬегз = сгеа1е_питбег_зСгеат(1, 10) # Применяем цепочку трансформаций зциагез = азупс_1тар(здиаге_азупс, питЬегз) емеп_здиагез = азупс_Г111ег(1з_е\еп_азупс, здиагез) # Собираем промежуточные результаты для демонстрации гези1тз = [] азупс Тог здиаге 1п е'/еп_здоагез: гезиИз аррепс^зриаге)
рг1п1(Г'Четный квадрат: {здиаге}") # Вычисляем финальный результат Т1па1_зип1 = аи/атИ азупс_гебисе(абб_азупс, азупс_11:ег_Ггот_ 1131( гезиИз), 0) епб.Стте = азупсю.де1_е\'еп1:_1сор() .МтеО рг±п1:( Г Итоговая сумма четных квадратов: {Т1па]_зпт}1) ргюЦГ 'Время выполнения пайплайна: {епд_Т1те - зТагЫЫте: .2С} секунд") азупс беТ азупс_11:ег_Тгоп"_1ю1:(11:етз: Изг) -> Лзупс6епегаю'[Лпу. Чопе] .......Вспомогательная функция дпя создания асинхронного итератора из списка.... Тог 1Теп 1п ТТетз: у!е1с1 Нет азупсю. гип(с1ето_зТгеат_р1ре11пе()) Пайплайн демонстрирует, как функциональные операции естественно комбинируются для создания сложной логики обработки. В данном примере мы последовательно применяем возведение в квадрат, фильтрацию четных чи- сел и суммирование, создавая понятный и легко модифицируемый код. Обработка множественных потоков данных требует координации между раз- личными источниками информации. Асинхронное программирование предо- ставляет инструменты для элегантного решения таких задач через объединение и синхронизацию потоков' ТтрогТ азупсю Тгот ТурТпд ТтрогТ АзупсТТегаЫе, 11п1оп азупс беТ тегде_азупс_зСгеа,пз(*зСгеатз: АзупсТТегаЫе) •> АзупсОепега1:ог[Апу, Мопе]: Объединяет несколько асинхронных потоков в один Элементы выдаются по мере их поступления из любого потока II II II # Создаем задачи для каждого потока Тазкз = [] з1:геа1г_11:ега1огз = [ ] Гог 1, зТгеат 1п епитегэТе^Тгеатз) : ТТегабог = аНеЦзТгеат) зТгеапцИегаТогз.аррепсЦГЛегаТог) # Создаем задачу для получения следующего элемента Сазк = азупсю .сгеа'ТеЫазкЫдеС-пехЫтСетГтЛегаТог, 1)) Тазкз.аррепЩТазк) асТГлеЫазкз = зе1(1азкя) л/Ы1е аегюе Ызкз:
* Ждем завершения любой из задач боне, репсйпд = аи/аП а8упс1о.ла11:(ас1::1уе_Газк8 геГигп_ ?/Ьер=азупо1о. Е1К8Т_С0МР1ЕТЕ0) Гог сошр1е1еб_Га.зк 1п поре: Ггу: ма1ие, зтгеаш_1пбех = аиаП сстр1е1еб_сазк 1Г уа1ие 1з поГ ЗГорАзупсИегаГГоп: у!е1б \-а1ие # Создаем новую задачу для следующего элемента того же потока по№_Газк = азупс1о.сгеаГе_Газк( _деГ_пехт_1гет(з1:геа1т_11ега1ог з [ зГгеат_ 1гс1ех], зСгеат_1пбех) ) асНуе.Газкз .абб(пеи_Газк) асГт уе_Газкз . бтзсагб(соп1р]еСеб_Газк) ехсерб ЗЮрАзупсТГегаГюп: # Поток закончился асНуе.Тазкз. б1зсагб(сстр1еГес1_1:азк) азупс беГ _деГ_пехС_1Гет( ПегаТсг, зГгеат_1пбех): ''"Вспомогательная функция для получения следующего элемента из потока.""‘ Ггу: уа!ие = аиа1С апехГ(ИегаГог) гетигп ма1ие, зГгеат_1пбех ехсерГ ЗборАзупсИегаПоп: гебигп ЗГорЛзупсИегаНсп, зГгеат_1пбех # Создаем различные потоки данных для демонстрации азупс беГ ГазГ_з1:геа1г.() -> АзупсбепегаГог[зГг, Моле] : ...Быстрый поток данных..... Гог 1 1п гапде(З): аиаГГ азупсто.з1еер(0.1) у 1е1с1 Г ' Быстрый-{1} “ азупс беГ з1ои_зтгеап’() -> АзупсСепега1ог[зГг, Мопе]: ...Медленный поток данных.""" Гог т 1п гапде(2): алатГ азупсю. з1еер(С.З) у1е1с1 Г"Медленный-{1}" азупс беГ тес11ит_зГгеап() -> АзупсбепегаГог[з1г, Мопе]: ''"Средний поток данных..... Гог 1 1п гапде(4): аиаГГ азупсГо.з1еер(С.2) У1е1с1 Г 'Средний-{1}11 Объединение потоков позволяет обрабатывать данные из нескольких источ- ников одновременно,, реагируя на события по мере их поступления. Это особенно
важно для построения реактивных систем, которые должны обрабатывать собы- тия от различных источников в реальном времени: азупс деГ бето_з1:геа11_п1егд1пд(): Демонстрирует объединение нескольких асинхронных потоков. Показывает, как элементы обрабатываются в порядке поступления. II II II рг!п1:( "Объединяем три потока с разной скоростью:") з! агТ-Мте = азупсю. деТ_е7еп1:_1.оор(). Т1те() # Объединяем потеки пегдеб = тегде_азупс_з1:геатз(Таз'(:_з1:геат(), з]о«_з1:геат(), тебют_зСгеыт()) # Обрабатываем объединенный поток соипТ = 0 азупс Тог Нет ъп тегдес!: сиггепД.Мте = азупс1о.деб_еуепГ_1оор()Л1те() еТарзеб - сиггепр_11те - зТагТ.Нте рг1пТ(Т [ {е1арзесГ .2Т}з] Получен элемент: {Нет}1) соипб += 1 епй_Т1те = азупсю.де1_е\'епб_1оор() .Т1те() рг1пб(Т'Обработано {соип!} элементов за {епсШте - збагб_ Т1те:,2Т} секунд") # Запускаем демонстрацию (эта часть не будет работать без полной реализации гаегде_азупс_зТгеатз) # азупсю. гоп(с1сто_зтгеат_п1егд1пд()) Паттерн «объединение потоков» демонстрирует мощь асинхронного функци- онального программирования для создания отзывчивых систем Элементы обра- батываются в том порядке, в котором они становятся доступными, независимо от их источника, что обеспечивает оптимальное использование ресурсов и ми- нимальную задержку обработки. Обработка ошибок в асинхронном контексте Корректная обработка ошибок в асинхронном функциональном программи- ровании требует особого внимания к распространению исключений через це- почки асинхронных вызовов и композиции функций. Функциональный подход к обработке ошибок через монадические паттерны хорошо адаптируется к асип хронному контексту: Тгош Хуртпд юрогТ Бепегю, ТуреУэг, 11пюп, СаИаЬЗе, АвдгъТаЫе Тгош епит ътрогТ Епит Т = ТуреУаг('Т') Е = ТуреУаг(' Е ’) I) = ТуреУаг( 'II') с1азз АзупсКези11(6епег1с[Т, Е]):
Монада ПезиИ для асинхронных вычислений. Инкапсулирует результат или ошибку без использования исключений. йс^____1г1б_(зеЗФ, уа1ие Т = Моле, еггог: Е = Иопе, 1з_зиссезз; Ьоо1 = Тгие): зе16._уа1ие = уа!ие зе16._еггог = еггог зе1Т._1з_зиссезз = 1з_зиссезз @с1аззтебЬос1 Феб зиссезз(с1з, уэ1ие: Т) -> 'АзупсРези1б[Т, Е]': " "Создает успешный результат......... гебигп с!з(уа!ие=уз1ие, 1з_зиссезз=Тгие) @с1аззтебЬоС (1еТ Га11иге(с1з, еггог: Е) -> 'АзупсРези1б[Т, Е]': .......Создаст результат с сшибкой. "" гебигп с1з(еггог=еггог, 1з_зиссезз=Еа1зе) Феб 1з_зиссезз(зе1Г) -> Ьсо1: .......Проверяет, является ли результат успешным...... гетигп зеИ ,_1з_зиссезз СеГ 1з_6а11иге(зе16) -> Ьсо1: .......Проверяет, содержит ли результат ошибку...... гебигп поб зе!6._1з_зиссезз с1е6 деб_уа!ие(зе1Т) -> Т: ...Возвращает значение или вызывает исключение."1 16 зе16. _1з_зиссезз: гебигп зе16._уа1ие габзе Уа1иеЕггог(6"Полытка получить значение из неудачного результата: {зе!С,_еггог}") Феб деб_еггог(зе1Т) -> Е: Возвращает ошибку или Мопе." гетигп зе16._еггог 16 псб зе16._1з_зиссезз е!зе Мопе азупс беб тар(зе!С, Типе Са1]аЫе[ [I], А«а1баэ1е[11] ]) -> 'АзупсРези1б[II, Е] ’ : Применяет функцию к значению, если результат успешный. Пропускает применение, если результат содержит ошибку. II II II 16 зе!6,_1з_зиссезз: бгу:
пеи_\?а1ие = аи/аИ Типс(8е1Т._уа1ие) геТигп АзупсРезиИ.8иссез8(пеи_уа1ие) ехсер! ЕхсерНоп аз е: геТигп АзупсРези1Т.ТаПиге(е) гебигп АзупсРезиИ. ТаП'иге(зе1Г. _етгог) азупс СеТ Т1аТ_тар(зе1Т, Типе: Са11аЫе[[Т], Айа11аЫе['АзупсРези11[11, Е]']]) -> 'АзупсКези1Т[и, Е]': Применяет функцию, возвращающую АзупсРезиИ, избегая вложенности. Используется для цепочечных вычислений с возможными ошибками. 1Т зе1Т._1з_зиссезз: Рту: геТьгп аиаИ Типс(зе1Т._ма]ие) ехсер! ЕхсерТТоп аз е: гетигп АзупсКези11.Та11иге(е) геТигп АзупсРезиИ.Та11иге(зе1Т._еггог) # Асинхронные функции с возможными ошибками азупс беТ Лу1с1е_азупс(а: Т1оа1, Ь: Т1оаТ) -> АзупсРези1Т|Т1оа1, збг] : II II II Асинхронное деление с обработкой ошибки деления на ноль. Возвращает АзупсРези1Р вместо выбрасывания исключения. аиаТТ азупсТо.з1еер(0.01) # Имитируем асинхронную операцию 1Т Ь == 0: геТигп АзупсРезиИ.Та11иге('Деление на ноль недопустимо1) гетигп АзупсРезиИ. зиссезз(а / Ь) азупс бе Г зцгб_азупс(х: Т1оа!) -> АзупсРези1Т[Т]оаТ, 51г] : Асинхронное извлечение квадратного корня. Обрабатывает ошибку для отрицательных чисел. аиаИ азупсТо.з1еер(0.01) 1Т х < 0: геТигп АзупсРези11.Та11иге('Квадратный корень из отрицательного числа') Ттрог-р тагИ гебигп АзупсРезиИ .зиссез8(та11т.8ЦГ1(х)) азупс аеТ 1од_азупс(х: Т1оаТ) -> АзупсРези1б[Т1оа1:, зТг]: Асинхронное вычисление натурального логаркфма
Обрабатывает ошибки для недопустимых значений. аи'аИ: азупс1о.з1еер(0.01) 1Т х <= 0: гетигп АзупсРезиИ.Та11иге( Логарифм определен только для положительных чисел") ьтрогТ таТК геТьгп АзупсРезиИ.зиссезз(таСб.1од(х)) Монада АзупсРезиИ позволяет элегантно обрабатывать ошибки в цепочках асинхронных вычислений без использования исключений. Каждая операция возвращает обернутый результат, который содержит либо значение, либо ин- формацию об ошибке. Это обеспечивает явную обработку ошибок и предотвра щает неожиданные исключения в цепочках вызовов.
Мемоизация и кеширование Представьте себе библиотекаря, который каждый раз, когда посетитель спра- шивает о местоположении одной и той же книги, идет и шлет ее заново во всех залах. Через некоторое время библиотекарь понимает, что гораздо эффективнее записывать результаты своих поисков в специальный блокнот и при повторных запросах просто заглядывать в него. Именно так работает мемоизация в про- граммировании — техника кеширования результатов вычислений для избежа- ния повторных расчетов. Мемоизация (тепилхайоп) является одним из фундаментальных принци- пов оптимизации в функциональном программировании. Эта техника особен- но эффективна при работе с рекурсивными алгоритмами, где одни и те же вы- числения могут повторяться множество раз. В Руйтоп мемоизация реализуется через различные механизмы кеширования, от встроенных решений до пользо- вательских декораторов. В этой главе мы изучим, как использовать встроенные возможности РуГйоп для кеширования результатов функций, создавать собственные кеширующие декораторы под специфические нужды проекта и применять техники мемоиза- ции для радикального улучшения производительности рекурсивных алгорит- мов. Вы научитесь не только применять готовые решения, но и понимать прин- ципы их работы, что позволит создавать оптимальные стратегии кеширования для различных сценариев. Основы мемоизации и ее преимущества Мемоизация основывается на принципе чистых функций — функций, кото- рые для одинаковых входных данных всегда возвращают одинаковый результат без побочных эффектов. Когда функция обладает этим свойством, результат ее вычисления можно сохранить в кеше и при повторном вызове с теми же аргу- ментами просто вернуть сохраненное значение. Рассмотрим классический пример вычисления чисел Фибоначчи без мемои- зации: бе* Т1Ьопасс1_па1'/е(п: 1пб) -> 1пУ: II II II Наивная реализация вычисления числа Фибоначчи. Демонстрирует экспоненциальную сложность без кеширования. II II II # Базовые случаи 1Т п <= 1: геТигп п # Рекурсивные вызовы приводят к повторным вычислениям гетигп Г1Ьопасс1_па1уе(п - 1) + Т1Ьопасо1_ла1Уе(п - ?)
# Замер времени выполнения для демонстрации неэффективности 1прог1 Т1те зтагТ.-Цте = Мте 1:1гге() гезиИ = НЬопасс1_па1уе( 35) епсЕМте = Т1те.г1те() рг!п1:(Т"Результат: {гезиЪт}"") ргт.п'ЦГ'Время выполнения: {епсЕНте - зТагТ.Нте:.4Т} секунд") Этот код демонстрирует фундаментальную проблему наивной рекурсии. Для вычисления Т1Ьопасс1_па1Уе(5) функция вызывается 15 раз, а для Т1Ьо- пасс1_па! уе(35) количество вызовов достигает более 29 миллионов. Временная сложность алгоритма составляет 0(2 лп), что делает его практически неприме- нимым для больших значений. Теперь применим простейшую форму мемоизации с использованием слова- ря Руйюп: беТ Т1Ьопасс1_тето1геб(п: 1пб, сасбе: (Ис! = Моле) > 1п1: Мемоизированная версия вычисления числа Фибоначчи. Использует словарь для хранения промежуточных результатов. # Инициализируем кеш при первом вызове 1Т сасбе тз Чопе: сасбе = {} # Проверяем наличие результата в кеше 1Г п 1п сасбе: геХигп сасбе[п] # Базовые случаи 1Т п <= 1 гезиИ = п е!зе: # Рекурсивное вычисление с кешированием гези11 = Т1Ьопасс1_тето12ес1(п - 1, сасбе) + Т1Ьопасс1_ тето12ес1(п - 2, сасИе) # Сохраняем результат в кеше сасбе1п] = гезиИ геСитп гезиИ # Демонстрация радикального улучшения производительности зСагс_т1те = Ите .Ите() гези11: = Т1 Ьопасс1_тето1гес1(35) епсШте = Т11геЛ1те() рг1п1:(Т" Результат: {гезиИ}") рг1п1:(Т"Бремя выполнения: {епсШте - зУагР.Ите: .бТ} секунд") Результат впечатляет — время выполнения сократилось с нескольких секунд до долей миллисекунды. Мемоизированная версия имеет временную сложность
0(п) и пространственную сложность 0(п), что является кардинальным улучше- нием по сравнению с наивным подходом. Однако ручная реализания мемоизации через передачу словаря в качестве параметра имеет недостатки Это нарушает чистоту7 функции и требует допол- нительных действий от программиста. Рубтоп предоставляет более элегантные решения. Встроенная мемоизация с/ипсюо18.1ги_саске Руйюп включает в стандартную библиотеку мощный инструмент для мемои- зации — декоратор @1ги_оас11е из модуля Типс1со1з. 1Ж11 означает Ьеазг Кесепйу Изей (наименее недавно использованный), что указывает на стратегию вытесне- ния элементов из кеша при достижении его максимального размера. Применим @1ги_сасНе к нашему примеру с числами Фибоначчи: Тгош Типс1со1з 1ггрог1: 1ги_сасбе @1ги_сасбе(пахз17е=128) бе? Т1Ьопасс1_сас1зеа(п: 1п1) -> птС: Версия с использованием встроенного 1_КВ кеша. Автоматически управляет памятью и оптимизирует доступ. # Базовые случаи остаются без изменений 1Г п <= 1: геСогп п # Рекурсивные вызовы автоматически кешируются гетигп Г1Ьопасс1_сасбеб(п - 1) + НЬопасс1_сас1теб(п - 2) # Демонстрация производительности и возможностей кеша з1аг1:_1:1те = 1лте.11те() гезиИ = Т1оогасс1_сас1теб(10О) # Можем безопасно вычислить большие числа епб_Г1те = 11те.Т1те() рг!п1:(Т Результат для п=100. {гезиИ}") рг1П’С(‘Р Время выполнения: (епб_?пте - зГагНПте:.6Т} секунд') # Информация о состоянии кеша сас1теИпТо = Т1Ьопассг_сас(|еа.сас)те_1пГо() рг1п1:(Т Статистика кэша: {сас!те_1пбо}") Декоратор @1ги_сасбе предоставляет не только автоматическое кеширова- ние, но и полезные методы для управления кешем. Метод сас1тс_1П'(о() возвра- щает именованный кортеж со статистикой использования кеша, включая ко- личество попаданий Отйз), промахов (тюзез), текущий размер и максимальный размер кеша. Важной особенностью @1ги_сасбе является то, что он использует аргументы функции в качестве ключей кеша. Это означает, что все аргументы должны быть хешируемыми (ЬазйаЫе). Рассмотрим более сложный пример: @1ги_сасКе(тахз12е=256) бе'? ехрепз1Уе_соггри1:аТ1оп(х: Т1оа1:. у: Нсаг, огес1зюп: тпс = 2) -> Т1оа1::
Имитация дорогостоящего математического вычисления. Демонстрирует кеширование функций с несколькими параметрами. # Имитируем сложные вычисления задержкой птрсгг тэгИ пгрсгб Р1гге # Небольшая задержка для демонстрации М|те.81еер(0.1) # Пример сложной математической операции гезиН = таДЬ.зцг1 (х**2 + у**2) * шаСН зт.п(х) * та1Ь.соз(у) геТигп гоипс( гезиН, ргес!з1оп) # Первый вызов - вычисление и кеширование збагщНте = 11те.11те() гезиИ1 = ехрепзПе .соприГаНоп^З . 14, 2 71, 3) ТггзТ.саП.Ите = б1теЛ1те() - з1аг1_11те # Второй вызов с теми же параметрами - из кеша зХаПщПте = Нше .Нте() гезиИ2 = ехэеп.з1Уе_сопри1а1'1оп(3.14, 2.71, 3) зесонб_са11_1:1те = г1те.11те() - зТагТ.Гхте ргтп-ЦГ'Результат: {гези1т1}') рг1п1(Т" Первый вызов: (Т1гз1:_са11_1:1те: .412} секунд') рг1п1;(Т "Второй вызов: 1зесопс1_са11_1:1те: .60 секунд") рг1пТ(Г"Ускорение: {Т1гз1_са11_1:1те / зеоопсСсаП-Пте :. 06}х") Параметр шахзтае заслуживает особого внимания. Он определяет макси- мальное количество результатов, хранящихся в кеше одновременно. Когда кеш заполняется, наименее недавно использованные элементы удаляются. Установ- ка тахз1хе=Мспе создает неограниченный кеш, что может быть полезно, но тре- бует осторожности с точки зрения потребления памяти: @1ги_сасйе(п1ахзз/е-Ьюпе) # Неограниченный кеш беТ рг1те_сЬескег(п: 1п1) -> Ьоо1: Проверка простоты числа с неограниченным кешем. Подходит для случаев, где важно сохранить все результаты. 1Г п < 2: геТигп Ра1зе 1Г п == 2: гетигп Тгие 1Г п % 2 == 0: гетигп Га1зе я Проверяем делимость только на нечетные числа дс зцг1:(п) нтрегб табб Тог 1 1п гапде(3, 1п1(тагп зрг1:(п)) + 1, 2):
16 п % 1 == 0: геТигп Га1зе гебигп Тгие # Демонстрация накопления результатов в кеше 1агде_питЬегз = [982451653, 982451654, 982451655, 982451653] # Повюр последнего Тог пит ю 1агде_питЬегз: 18_рг1те = рг1те_сЬескег(пит) сасЬе_зТагз = рг1те_спескег.сасИе_1пТо() рг!пТ(Т 1 {пит}: {'простое' 16 1з_рг1те е1зе 'составное'} (кеш: {сасТ|е_з1аТз.ЫТз} погаданий) 1) Продвинутые техники использовиния 1ги_саске Декоратор @1ги_сас1эе имеет несколько тонкостей, понимание которых кри- тически важно для эффективного использования. Одна из них связана с тем, что кеш привязан к конкретному экземпляру функции. Это может привести к неожиданному поведению в некоторых сценариях: @1ги_сас11е(г1ахз1хе-128) Себ гесигзтуе.ТасТоггаЦп: 1п6) > 1пг: Рекурсивное вычисление факториала с кешированием. Демонстрирует эффективность кеша для математических функций. II II II # Базовый случай 16 п <= 1: геТигп 1 # Рекурсивный вызов автоматически использует кеш гебигп п * гесигз1уе_6ас1ог1а1(п - 1) # Измерение производительности для больших чисел ьтрсгб Т11ге # Первое вычисление заполнение кеша збагб = 61те г1те() гезиШ = гесиг51Уе_ТасТог1а1( 500) 11те1 = 61теЛ1те() - збагб # Повторное вычисление - использование кеша зГагГ = 61те.61те() гези112 = гесигз1уе_6аобсг1а1(500) 11те2 = Ыте.61те() - збагб # Вьчисление немного болошего числа - частичное использование кеша збагб = Ите.ИтеО гезиНЗ = гесигз1уе_6аобог1а1(660) Т11геЗ = Нте.ИтеО - збагб рг1пб(6"Первэе вычисление 5001. {11те1 :.66} секунд") рг±п1(Г'Повторное вычисление 506!: {Т1те2:.66} секунд") рг1пб(6 Вычисление 600!: {ИтеЗ: . 66} секунд") рг!п1(6 Состояние кэша: {гесигз1уе_6асбог1а1.сасНе_1п6о()}")
Важно понимать, что @1ги_сасКе создает глобальный кеш для функции. Это означает, что все вызовы функции из любой части программы используют один и тот же кеш. В многопоточных приложениях это может быть как преимуще- ством, так и источником проблем. Рассмотрим пример работы с методами классов: с1азз МаТКСрегаНопз: Класс для дсмонстоаыии использования 1ги_сасбс с методами. Показывает особенности кеширования в обьектно-ориентированном коде. беТ___1п11__(8е1-Г, соеРТтсгепС: НсаГ) : зеИ. ссеТНсдеп! = ссеТТюьепТ @1ги_сасбе(тахз1хе=64) беТ ехретз1уе_са1са1аТюп(зеИ, х: Г1оат) -> Т1оа1: Дорогостоящая операция с кешированием. Важно: кеш разделяется между всеми экземплярами класса! пирогу 11те Типе .з1еер(0.05) # Имитация сложных вычислений геУигп зе1Г.ссеТТ1с!еп1 * х ** 2 + х # Создание двух экземпляров с разными коэффициентами са1с1 = МаТЬОрегаПопз^ 0) са!с2 = МаТЬ0регаг1опз(3.0) # Вызовы с одинаковыми аргументами, но разными экземплярами гевиШ = са1с1 .ехрепз1'уе_са1си1а1юп(5.0) гези1С2 = са1с2 ехрепзз уе_са!си] а1юп(5.3) # НЕ будет взят из кеша! ргтп-ЦГ'Резупьтат для са1с1: {гезо111}") ргтп-ЦГ'Результат для са1с2: {гези11:2}") рг1п1:(Г|Состояние кэша: {са1с1 .ехрепз1Уе_са1си1а1:1оп. сасНе_1п1о()}") Этот пример демонстрирует важную особенность: кеш учитывает все аргу менты функции, включая зе1Г. Поскольку зеИ разный для каждого экземпляра класса, результаты не будут переиспользоваться между экземплярами. Для решения подобных проблем можно использовать кеширование на уров- не экземпляра: Тгот ТипсУоо1з ътрог!: 1ги_сасбе с!азз ОрУьтбхебМаУЬОрегаТьопз: Улучшенная версия с кешированием на уровне экземпляра . каждый экземпляр имеет свой независимый кеш. беТ___1П1Г__(зе1Т, соеГТ1с1епТ : Г1оаУ) :
зе1Т .сое-ТТтстепТ = сое-ГТ1с1еп1: # Создаем кешированную версию метода для каждого экземпляра 8е1Г._сасКес1_са1са1а1:1оп = 1ги_сасЬе(шахз12е=64) (зеИ._ ехрепз1хе_са1си1а1:1эп) йеТ _ехреп517е_са1си1агюп(зб11, х: С1оат) -> Поа1: Внутренний метод без привязки к зе!Т в кеше. Использует атрибуты экземпляра через замыкание. 1ГГООГ1: Ите Ите. з1еер(0.0Ь) геСигп зеИ.соеН1с1епЬ * х ** 2 + х СеТ ехрепз1уе_са1си1а1?1сп(зе1'(:, х: Г1саТ) -> Т1оаС: Публичный интерфейс использующий кешированную версию. гебагп зс1Т._сасЬес1_.са1си1а1:1оп(х) беТ саспе.тпСо^еИ): 'Предоставляет информацию о состоянии кеша экземпляра............. гебагп зе1Т. _сасЬеб_са1си1а1:1оп. сас1те_1пТо() бе'Г сасЬе_с1еаг(бе1Г): ...Очищает кеш экземпляра..... зеИ. _сас1тес1 _са1со1а1:1оп. сас11е_с1еа г () # Демонстрация независимых кешей орт_са1с1 = 0р1с1т12ес1Ма1:110регас1оп8(2 0) орТ_са1с2 = 0р1:1п112ес1МаТ1ЮрегаС1опз(3.0) # Каждый экземпляр тегеро имеет свой кеш гезиШ = ооС_са1с1.ехрепз1уе_са1си1аС1оп(5.0) гези1С2 = оэ1_са1с2.ехрепз1уе_са1си1а1±оп(5.0) рг1п1:(Т"Кеш первого экземпляра. {орС_са1с1 .сасбе_1пТо()} 1) рг1пт(-Г"Кеш второго экземпляра. {орТ..са!с2. сасНе_1п~ГоО}1) Создание собственных кеширующих декоратороЕ: Хотя @1ги_сас1те покрывает большинство потребностей в кешировании, ино- гда требуются специализированные решения. Создание собственных кеширую- щих декораторов позволяет точно настроить поведение кеша под специфиче- ские требования проекта. Начнем с простого декоратора, реализующего базовую мемоизацию: беб з1тр1е_1пеп1о12е(Гипс): Простой декоратор мемоизэции без ограничения размера кеша Подходит для функций с о-раниченным набором возможных аргументов
сасбе = {} беб у.таррег(*агдз. **клагдз) : # Создаем ключ из всех аргументов кеу = зГг(агдз) + з1г(зсг1еб(клагдз.11етз())) # Проверяем наличие результата в кеше 1Г кеу 1п расЬе: геТогп сасбе[кеу] # Вычисляем и сохраняем результат гезоИ = Типс(хагдз, **к«агдз) сасЬе[кеу] = гезиИ гебигп гезиИ # Добавляем полезные методы для управления кешем «гаррег.сасИе = сасЬе 'л/гаррег.сасИе_с1еаг = 1атЬба: сасЬе.с1еаг() 'Л1гаррег.сасНе_1пТо = 1агпЬс1а. Т "Размер кеша: {ТеЩсасбе)}" геТигп игаррег @з1тр1е_тето12е беб з1э1л/_Типс11оп(п: 1п1, тиШрИег: Т1оа1 = 1.0) -> НоаР: Функция с искусственной задержкой для демонстрации кеширования итрогр Р1те Рггге. з1еер(0.1) геРогп п * шиИтрИег * 3.14159 # Демонстрация раооты простого декоратора рг1пР("Первый вызов:") гезо1Р1 = з]ом/_ТипсР1оп(5, ти1Р1р11ег=2.Э) рг1пр(1:"Результат: {гезиШ}") рг!пР("кпПовгооный вызов:') гезо1Р2 = 51о1Л'_'(:ипсР1сп( 5, ти1Р1р11ег=2.0) рг1пР(1:"Результат. {гези112}") рг1пР(зРои_'(:ипсР1ОР .сасбе_1пТо()) Простой декоратор работает, но имеет ограничения. Создание строкового ключа из аргументов неэффективно и может не работать корректно с некоторы- ми типами данных Улучшим его: (тот РопсРооТз РтрогР щгарз мпрогр НазЫтР ипрогТ р!ск1с беТ абуапсеб_тепю12е(тахз12е: 1пТ = 128, 11:1: Лоа! = \опе): Продвинутый декоратор мемоизации с поддержкой ТТ1_ и ограничения размера.
Агдз: тахзТ/е: максималоный размер кеша И1: время жизни записи в кеше в секундах II II II ОеТ бесогаТог(’(:ипс) : саспе = {} ассезз_огбег = [] # Для реализации 1Р11 @игарз(Гипс) беТ игаррег(*агдз, *хк«агдз): 1трог1 типе # Создаем надежный ключ кеша бгу: # Пытаемся исполозсвать ЬазЬ для быстрого ключа кеу = ЬазП((агдз, 1ир1е(зсгТеб( киагдз. 11етз())))) ехсерт ТуреЕггог’ # Если аргументы не хешируются, используем р!ск1е + ЬазЬ зегхаИгеб = р!ск]е.битрз((агдз, киагдз), ргоТосо1=р1ск1е . Н1(ЗНЕ8Т_РРОТОСЭ1_) кеу = Иазп11Ь.тб5(зег1а112еб) .1техс1дез1() сиггепТ.Нте = Сипе. Т1те() # Проверяем наличие в кеше и актуальность И кеу 1п саспе, уа1ие, ИтезТатр = сасЬе| кеу] # Проверяем ТТ1_ если он задан 1Т И1 18 Мопе ог (сиггепТ_Ите - Тилезгашр) <= 111: # Обновляем порядок доступа для 1_Р11 асое85_огс1е1'. гетоме(кеу) асоезз_огбег. аррепб(кеу) гетигп ма1ие е!зе. # Удаляем устаревшую запись бе1 сас1те[кеу] ассезз..отбег гетоуе(кеу) # Вычисляем результат гезиИ = Типс(*агдз. **киагдз) # Управляем размером кеша (ЬРЬ емтсДюп) 1Т 1еп(сасЬе) >= тахзтге: # Удаляем самую старую запись о1безт_кеу = ассезз_огбег.рор(О) бе1 сао1те[о1с1е8Т_кеу ]
# Сохраняем результат с временной меткой сасНе[кеу] = (гезиН, сиг гепС.Нте) ассезз_огс1ег. аррепб(кеу) -еТигп гезиН # Добавляем методы управления кешем аеТ сасНе_1пТо(): геСигп { 'зтге': 1еп(сасЬе), ' 113X3126' : тахз12е. 'Ш 1 : Ш, 'кеуз': 11зТ(сас11е.кеуз() ) } сеТ сас1пе_с1еаг(): сасЬе.с1еаг() ассезз_огбег,с1еаг() игароег .сасКеПпТо = сасН_тпСо лгарэег.сасНе_с!еаг = сасНе_с1еаг югарэег ._сасГ)е = сасКе # Для отладки гетигп вдгаррег геТигп бессгатог # Демонстрация продвинутого декоратора @аскапсес1_тето12е(тах812е=5. Н1=2.0) беТ ар1_са11_31ти1аС1ОП|'егс1ро1п1:: зТг, рагатз: НсТ) -> сНсС: Имитация ЛР1 вызова с кешированием на 2 секунды. 1ШВСГ1: Нте 1трог1 гапбот # Имитируем сетевую задержку Нгге. з1еер(0.5) геСигп { 'епбротпТ' : епоро1п1:, 'ДаТа1: Г гезропзе_ба1:а_{гапбэт. гапб1п1:( 1060, 9999)}", И1тез1атр': Гтте.С1те(), 'рагатз': рагатз } # Тестирование ТН-функииональности рппГ("Первый вызов АРТ:") гезропзе! = ар1_са11_з1ти1аТ1оп( изегз", {"раде : 1}) ргшпЦГ'Данные: {гезвопзе1 [ 'баса' ]}") рппг( "ХпПоеторный вызов (из кеша):")
гезропзе2 = ар1_са11_з1ти]а11оп("изегз", { раде": 1}) рг!п1:( Г'Данные : {гезрог.зе2[ 'сата' ]}") рг!п1:(Т "Данные идентичны: {гезропзе! [' с!а1а' ] == гезропзс2 [' ба1а ’ ]} ) # Ждем истечения ТТ1_ рг!п1("\пОжидание истечения ТТ1....") ттрогХ 1:1 те Ите. з1еер(2.5) рг1п1:("\пВызэв после истечения ТТ1_:") гезропзеЗ = ар1_са11_81ти1ат1оп("изегз , {"раде": 1}) рг±п1:(12 Новые данные: {гезрспзеЗ[ 'ба^а' ]}") рг1п1:(Т"Ин<|:ормация о кеше: {ар1_са11_з1ти1а(:1оп. сасбе. 1пТо()}") Кеширование с учетом типов данных Различные типы данных требуют разных подходов к кешированию. Рассмо- трим специализированные декораторы для работы с различными сценариями: Тгот ТипсСоо1з ХтрогТ: игаэз Тгот Уурюд гпроП Апу, СаПаЫе, ОРсС, Из! хтрогТ «еакгеТ с!азз 1пзтапсеМе1го1хе: Декоратор для кеширования методов экземпляра. Использует слабые ссылки для предотвращения утечек памяти. беТ____1п11:_(зе1Т, тахзх/е: 1П1 = 128): зе1Г.тахзтхе = тахзтге зе1Т.сас11ез = иеакгеТ .ИеакКеуС1с1:1Опагу() аеТ____са11__(зеП, те1.1тоа): @™ггрз(те11тоб) беГ йгаррег(1пз1:эпсе, *агдз, **киагдз): # Получаем или создаем кеш для данного экземпляра 1Т 1пз1:апсе по! тп зеИ.саспез: зе1Т.сас11ез[1пз'(:апсе] = {} сасбе = зеИ.сасНез[1пз1:апсе] # Создаем ключ из аргументов (без зе1Т) кеу = (агдз 1ир1е(зог1:еб(кйагс8.1Сетз()))) 1Т кеу 1п саспе: гебигп сасИе[кеу] до 1_йб) # Управляем размером кеша 1Т 1еп(сас1те) >= зе1Т.тахз1хе: # Простое удаление первого элемента (можно улучшить
о1безб_кеу = пехб(1бег(сас1те)) с1е1 саоИе[о1безб_кеу] # Вычисляем и кешируем результат -ези1б = тебНоб(1пзбапсе, *агдз, **киагдз) сасбе[кеу] = гезиИ гебигп гезиИ: гетигп вдгароег беТ сасЬе_с1еаг_а11(зе1Т) : 1 ’ Очищает зсе кеши для всех экземпляров.... Тог сасКе 1п зе1Т .сасИез.уа1иез(): сасбе ,с!еаг() сбазз ОабаРгосеззог: Пример класса с кешированием методов на уровне экземпляра. беТ___1П1Т__(зе1Т, бабазеб_пате: збг): зе1Т.бабазеб_пгте = баб азеб_пат1е зе1Т.ргосезз1Г|д_сопТ1д = {"погтаИге": Тгие, "ТПбег_ оибИегз1 Рабзе} @1пзтапсеМето12е(п1сХ512е=50) беТ ргосезз_баба(зебТ, баба: 1_бзб[Тбоаб], орегаббоп: збг) -> 1_бзб[Тбоаб] : Обработка данных с кешированием результатов. Каждый экземпляр имеет свой независимый кеш. бтрогб ббте ббте.збеер(0.1) # Имитация сложной обработки 1Т орегаббоп == 'здиаге1. геТигп [х ** 2 Тог х бп аата] е11Т орегаттсп == "погтаббге": тах_уа! = пах(баба) 1Т баТа е1зе 1 гебигп [х / шах_уа1 Тог х 1п баТа] еНТ орегаТТоп == РоиЫе' : гебигп [х * 2 Тог х 1п баба] ебзе: гебигп бабе.соруО @ТпзбапсеМепот2е(п1аХЗТ2е-20) беб аддгейаТе_зТаТ15Т1сз1зеИ| баба: Изб[Т1оаб]) -> Обсб[збг, Г1оаб] : II II II
Вычисление статистик с кешированием. итрог! зХаИвИсб 16 пот баба: гетигп {"шеап": 6, 1 теббап 0, "зббеу": 0} геТигп { "теап": зтаТ1зТ1с5.теап(с1аТа), "тескап": зТаТ1зТ1сз ,теб1ап(с1ага), 'зТс1еу": зТаТ1зт1сз.зТаеу(с1аТа) 1Т 1еп(баба) > 1 е!зе 0 [ # Демонстрация кеширования на уровне экземпляра ргосесзог! = 0аТаРгосеззог( "с'аТазеТ.А1 ) ргосеззог2 = ОаТаРгэсеззог("саТазеТ_В") ТезТ_РаТа = [1.0, 2.0, 3.0, 4.0 5.0] # Обработка данных разными процессорами гези1Т1 = ргосеззсН .ргосезз.с1аТа(Те5Т_баТа, ' зриаге ') гсзи1Т2 = ргэсеззог2 ргосезз_баТа(Тсзт_баТа, ''здиаге") # Независимо1й кэш гези1ТЗ = ргосезяог1 .ргосезз_с1а1а(ТезТ_с1а1а, здиаге') # Из кеша рг!пТ(Т Результат процессора 1: {гези1Т1[ :3]}...") рг1пт(Т Результат процессора 2: {гези!с2[ :3]}. ..") рг1пТ("Третий результат взят из кеша процессора 1 ") # Статистики также кешируются независимо зТаТз1 = ргосеззог1.аддгеда1е_зТаТ1зТ1с8(Те8т_с1аТа) 8ТаТз2 = ргосез8ог2.аддгедаТе_8ТаТ1зТ1с8(Те8Т_с1аТа) рг!пТ(Т Статистики 1: {зТаТз1}") рг1пТ(Т Статистики 2: {зТаТз2}") Оптимизация рекурсивных алгоритмов через мемоизацию Рекурсивные алгоритмы представляют собой идеальный случай для приме- нения мемоизация Многие классические алгоритмы, такие как динамическое программирование, получают огромные преимущества от кеширования проме- жуточных результатов. Рассмотрим задачу о рюкзаке — классический пример динамического про- граммирования: @1гц_сас1те(тахз12е=1024) бе* кпарзаск_гесигз17е(саэас1бу: 1пб, «етдКбз: Тир1е, ча1иез: 1ир1е, п: бпб) -> 1пт; II II II Рекурсивное решение задачи о рюкзаке с метризацией. Агдз: сарасббу: вместимость рюкзака мебдИбз: веса предметов (кертеж для хешируемости)
уаЛиез: ценности предметов п: количество рассматриваемых предметов Ребигпз: максимальная ценность, которую можно поместить в рюкзак # Базовые случаи ЛТ л == 0 ог сарасЛбу == 0: гетигп О # Если вес предмета больше вместимости рюкзака 1Т 1л/е1дЬгз [п-1 ] > сарэсИу: гетигп кпарзаск_гесигзбуе(сарасббу, лебдЫз, уаТиез, п-1) # Выбираем максимум из двух вариантов: # 1. Включить текущий предмет # 2. Не включать текущий предмет шсЛиде.ббет = уаЛиез[п 1] + кпаозаск_гесигз1уе( сарасЛбу - ие1дббз[п-1], йеюббз, уаЛисз п-1 ) ехсЛис1е_ббет = кпарзаск_гесигзЛуе(сарасббу, лебдНбз, уаЛиез, п-1) гебигп тах(бпсЛибе_ббет ехс!ибе_ббет) # Функция-обертка для удобства использования беТ зоЛуе_кгарзаск(саргсббу: бпб, Лбешз: Л1зб) -> бпб: Решает задачу о рюкзаке для списка предметов. Агдз. сарастбу: вместимость рюкзака Иетз список кортежей (вес, ценность) Лб поб Иетз: гебигп 0 |Л/е1дбсз = бирЛе(1бет[0] Тог Лбет 1п Иетз) уа1иез = ГирЛе(Пеп[ 1 ] *сг Нет Лп Иетз) гебигп кпарзаск_гесигз1Уе(сарасббу, иебдНбз, уаЛиез, Леп(Лбетз)) # Демонстрация решения задачи о рюкзаке Лбетз = [ (10. 50), вес: 10, ценнссто: 60 (20, 100), вес. 20, ценность: 100 (30, 120), вес: 30, ценность: 120 (40, 166), вес: 40, ценность: 160 (50, 206), # вес: 50, ценность: 200 кпарзаск_сарасббу = 100
з1аг1_Ттте = г1теЛ1те() тах_уа1ие = зо1уе_кпаряаск(кпарзаск_сарас11’у, Нете) ехесиТюп.ТТте = Нте. 11те() - зТагГ.Ите рг1п1:(Т Максимальная ценность: {тах_уа1ие}") рг1п1:(Т Время выполнения: {ехеси1:1сп_1:1П1е:. 5Ф} секунд1) рг!п1:(1:"Статистика кеша: {кпарзаск_гесигз1Уе.сэс1те_1П'1:о()}') Другой классический пример — алгоритм вычисления расстояния Левен штейна (редакционное расстояние между строками): @1ги_сасЬе(г1ахе12е=2048) с!е4: 1еуеп51т1:е1п_с)1згагсе(з1:г1 : збг, зтг2: зсг) > 1пб: Вычисляет расстояние Левенштейна между двумя строками. Использует мемоизацию для оптимизации рекурсивных вызовов. II II II # Базовые случаи 1Т 1еп(збг1) == 0: гебигп 1еп(збг2) 1Т 1еп(збг2) == 0: гебигп 1еп(збг1) # Если последние символы совпадают 1Т збг1[-1 ] == зтг2[-1] гебигп 1емепзЫ:е1п_с11з1:апсе(зсг11 :-1 ], збг2[:-1]) # Рассматриваем три операции: вставка, удаление, замена хпзегб.ор = 1еуепзНбе1п_а1збапсе(збг1, зтг2[:-1]) + 1 0е1ебе_ор = 1е7еп8Г|бе1п_с!18бапсе(збг1 [ :-1 ], збг2) + 1 гер1асе_ор = 1е7егз1лбе1п_^1збэпсе(збг1 [ 1], збг2[: 1]) + 1 гетигп т1п(1пзегб_ор, Фе1ебе_ор, геэ1асе_ор) # Демонстрация алгоритма безб_збггпдз = [ ("кбббеп", 1 зтбббпд"), ("ргодгапнппд", "дгитгппд ), ("а1дог1сЬт", "бодапббип"), ("руб1юг1", 'дама') бог збг1, збг2 1п безб_81г1пдз: Обзбапсе = ТеуепзЕТетП-ОтзСапсе^Сг!, з1г2) ргтп-Ц-Е 'Расстояние между '{з1г1}' и '{8Тг2}’: {сНзТапсе} ) ргтпчД’Г ЛпИнфсрмация о кеше {1еуепз1тТе1п_Лз1:апсе .сасПе_1пТо()}") Мемсизация особенно эффективна для алгоритмов с перекрывающимися подзадачами. Рассмотрим задачу подсчета количества способов достичь определенной суммы с помощью заданных монет: @1ги_сасЬе(пах81/е=1024) Феб соип'С_иауз_1:о_г1аке_с1лапде(атоипт: тпТ, сотлз: 1:ир1е) -> тпТ:
Подсчитывает количество способов получить сдачу заданными монетами. Классическая задача динамического программирования. # Базовые случаи 1Т ашоипт == 0: геТигп 1 # Один способ дать нулевую сдачу - не дать ничего 1Т атоипб < 0 ог по! со1пз: геРигп 0 # Невозможно дать отрицательную сдачу или без монет # Два ваоианта: использовать текущую монету или не использовать изе_со1п = соипр_1мауз_1о_таке_с1тгпде(ап1оаг|1 - со1пз[0], со!пз) зк1р_со1л = соип1_иаув_1о_таке_ск1апде(атоипР, со1пз[1 :]) гебигп изе_со1п + зкхр.сохп беТ таке_с1тапде_апа1у51з(1агдеР_атоип1: 1п1, ауаПаЫе_со1пз: Изб): Анализирует различные способы размена для заданной суммы. со!пз_1ир1е = Рир1е(зог1ед(ауа11аЬ1е_со1пз, геуегзе=Тгие)) # Сортируем для лучшего кеширования зРагб-Мте = Ите 11те() лауз = соипб_\мауз_Ро_таке_с1тапде(РагдеР_атоип1, со1пз_1ир1е) ехесиР1ол_Р1П1е = Рипе. Рипе () - зТагб-Ите гебигп «ауз, ехесиР1оп_Р1те # Демонстрация задачи о размене монет амаИаЫ е.сотпз = [1, 2, 5, 10, 25] # Номиналы монет в копейках ТагдеС.атюипТз = [30, 50, 100, 200] Тог атоипс 1п гагде1_апоигРз: «ауз, ехес_11те = паке_сКапде_апа1уз1з(атоип1, ауа11аР1с_со1пз) рг1п1(Г Сумма {атоапб}: {лауз} способов размена за {ехес_ Р1те:.4Г}с") ргтпб(Т"Финальная статистика кеша: {соипс_«ауз..1о тгке_сбапде.сас1те_ 1пГо()}") # Очистим кеш для демонстрации разницы в производительности соипР_йауз_1о_таке_с(тапде.сасИе_с1еаг() рг1пб("ХпПовгэрное вычисление без кеша:") зТагТ.Мте = Т1пе.Т11те[) щауз_по_сасЬе = соипр_йауз_Ро_таке_с1тапде(100, 1ир1е([1, 2, 5, 10, 25])) Р1те_по_сасБе = Ите.ИтеО - зба'"СТите рг1п1:(Т"Время без предварительного кеша: {1:11ге..по_сасКе: .41:}с")
Параллельное программирование в функциональном стиле Современные вычислительные задачи все чаще требуют аффективного ис- пользования многоядерных процессоров и распределенных систем. Функцио- нальное программирование предоставляет элегантные решения для параллель ных вычислений, позволяя создавать безопасный и предсказуемый код. В этой главе мы изучим, как сочетать принципы функционального программирования с параллельным выполнением задач в Руйюп. Функциональный I юдход к параллельному программиро ванию особенно при- влекателен благодаря неизменяемости данных и отсутствию побочных эффек- тов. Эти характеристики делают параллельные вычисления более безопасными и устраняют многие проблемы, связанные с состояниями гонки и синхрониза- цией потоков. Мы рассмотрим, как модуль солеи г гепт.ТиРигез интегрирует- ся с функциональными паттернами, изучим операции шар-гебисе и разберем практические подходы к построению эффективных параллельных систем. Основы функционального параллельного программирования Функциональное программирование предоставляет естественную основу для параллельных вычислений. Чистые функции, не имеющие побочных эф фектов и не зависящие от изменяемого состояния, могут выполняться парал- лельно без риска взаимного влияния. Этот принцип кардинально отличается от императивного подхода, где разделяемые данные требуют сложных механиз- мов синхронизации. Неизменяемость (ппппйаЬШгу) играет ключевую роль в безопасности парал- лельных вычислений. Когда данные не могут изменяться после создания, мно- жественные потоки могут безопасно работать с одними и теми же структурами данных без блокировок или мьютексов. Это упрощает разработку и устраняет целые классы ошибок, связанных с параллельным программированием. Декларативная природа функционального программирования позволяет со- средоточиться на логике вычислений, а не на деталях управления потоками. Вместо создания потоков вручную и управления их жизненным пиктом мы опи- сываем операции надданными, а гоп Игле епутгоптепР берет на себя оптималь- ное распределение работы между доступными вычислительными ресурсами: Тгош гургпд бтроН Ьтзг, СаПаЫе, ТуреУаг дтрсг! т!те Ггот ТипсНо1з 1ггрог1: гебисе Т = Туре7аг('Т') Р = ТуреУаг(' Р') бе* 5едиеп11а1_тар_ехатр1е(баСа: ЫзХЦпр]) -> 1_1з1[1п1]:
Демонстрирует последовательное применение функции к каждому элементу. Это ЬазеИпе для соавкения с параллельными версиями СеТ ехрелз1уе_орега1:1оп(х: 1п1) -> 1п1: # Имитация вычислительно сложной операции Г1те.з1еер(0.1) гебигп х**2+2*х+1 # Последовательное применение функции гезиИ: = [ ] Гог Нет 1п баГа: гезиИ. аррепс1(ехреп81уе_орегет1оп(11:ет)) геГигп гезиИ # Тестирование производительности последовательного подхода 1ез1:_6аГа = 11зТ(гапде(10)) згагТ.Пте = Гтте 11гге() зециепНаНгезиИ = зепиепТ1а1_тар_ехап1р1е(1:ез1:_с1аТа) зедиеп11а1_1:1те = Ите Л1те() - зГагГ.Пте рг!п1:(Г"[1ослсдовательное выполнение: {зедиепТ1а1_1:1те : .2Г} секунд") Этот пример показывает традиционный последовательный подход к обра- ботке данных. Каждый элемент обрабатывается по очереди, что приводит к ли- нейному росту времени выполнения с увеличением размера данных. Для десяти элементов с задержкой 01 секунды каждый общее время выполнения составит около одной секунды: 6еГ 1т1ти1:аЫе_ба1:а_ехапр1е(): Демонстрирует работу с неизменяемыми структурами данных для безопасного паоаллельного доступа. Тгот со11есТ1опз ГтрогР пашеб1ир1е # Создание неизменяемой структуры данных Оа1аРо1п(: = патесйир!е('ба!аРойгтД', ['16', 'уа!ие', 'МтезГатр' ]) # Функция трансформации, не изменяющая исходные данные беТ 1:гапз^сгт_баГароз-п!:(ро1п1:: ОаГаРоГлТ) -> Са1эро1п1:; Создаст новый баГаРозп!: с преобразованным значением геГигп Па1аРо1п1:( 16=ро1п1. 16, уа1ие=ро!п1:. уа!ие * 2, ЩтезГатр-ротгЧ.-ИтезГатр + 1 ) # Исходные данные ог1д1па1_6а1:а = [
Эа1аРо1п1:(1, 100, 1003), Эа1гРоа.п1 (2, 200, 2000), Эа1:гРо1п1ЦЗ, 300, 3003) # Трансформация создает новые объекты, не изменяя исходные 1гапз1сгшеб_ба1а = [ СгапзГогт_ба'1аро1п1:(ро1п1:) Тог роТпТ 1п ог1д!па1_баГа] 4 Исходные данные остались неизменными рг!пТ('Исходные данные:", ог1д1па1_баТа[0]) рг1,п1( "Преобразованные данные", Ггап5Гогтеб_баса[0]) геТигп огТд1па1_баТа, 1гапзГогтеб_баГа 1тггиТаЫе_баТа_ехатр1е() Использование неизменяемых структур данных, таких как патебГирГе, га- рантирует, что трансформации создают новые объекты, не влияя на исходные данные. Это позволяет множественным потокам безопасно читать и обрабаты- вать одни и те же данные параллельно. Модуль сопсиггепгкпигез и функциональные паттерны Модуль сопсиггепТ.ТиТогез предоставляет высокоуровневый интерфейс для асинхронного выполнения функций с использованием потоков или процессов. Этот модуль идеально сочетается с функциональными паттернами, позволяя легко параллелизовать операции тар. Г111 е г и гениев без изменения основной логики. Основные абстракции сопсиггепт.Гитигез включают ТбгеабРооТЕхеси бог для 1/О-Ьоипй задачи Р госеззРоо1Ехесито г для СРО-Ьоипс! операций. Функци- ональный подход к использованию этих инструментов заключается в передаче чистых функций в методы ехесгЛог, что обеспечивает предсказуемое поведение и упрощает отладку: Тгош сопсиггепГ.Гибигез дпрогТ ТИгеабРооТЕхесибог, РгосеззРоо1Ехесигсг, аз_сотр1етеб пирог! ГГгге пирог! та1Е| Ггот Турдлд ГтрогГ ИзГ, СаПаЫе бе* сри_1пГепз1уе_ГипсГ1оп(п: 1п!) -> Г1оаГ: Вычислительно сложная функция для демонстрации РгосеззРооТЕхесибсг. Вычисляет сумму квадратных корней от 1 до п. II II II 1о1а1 = 0 Гог 1 1п гагде(1, п + 1): 1ога1 += табб здгт(1) геГигп 1отэ1 беГ 1о_1п1:ег|3]уе_Гипст1оа(бе1ау: Поаб) -> зег:
1/0-интенсивная функция для демонстрации ТИгеабРообЕхесибог. Имитирует сетевой запрос или чтение файла. Тите.з1еер(бе1ау) гетигп Т"Обрабогано за {с!е1ау} секунд" с!еФ рага11е1_1гар.и1б!1ЛЬгеабз(Типс: СаПаЫе, баба: 1.151, ггах. логкегз: 1п1 = 4): Параллельное применение функции, используя пул потоков. Подходит для 1/О-Ьоипб операций. И1б1з ТНгеаб?оо1Ехесибог(тах_щогкегз=тах_«гагкегз) аз ехесибог: * Запускаем все задачи бибигез = [ехесибог.зиЬт1б(6ипс, Нет) Фог Нет 1п баба] # Собираем результаты в том же порядке гезиИз = [бибиге.гезиИ() Тог бибиге 1п Тибигез] гебигп гезиИз # Демонстрация параллельного выполнения 1/0-Ьоипб задач бо.баба = [0.1, 0.2, 0.15, 0.1, 0.25] збагб_б1те = Мте. т!гге() бКгеаб_ гезиИз = ра 'а11е1_тар_й1т11_б,1геабз(1о_бпбепз1уе_бипсС1оп, 1о_баба) бКгеабЛзте = ббтеЛ1те() - збагб_ббте рг1пб(б"Параллельное выполнение (потоки): {бГиеаб.бтте: .26} секунд1) рг1пб(б"Резулотаты : {бпгеаб_гези1бз[ :2]}...") # Показываем первые два результата Использование ТНгеааРообЕхесибог значительно ускоряет выполнение 1/0-Ьошм! операций. Вместо последовательного ожидания каждой операции все задачи выполняются параллельно, что сокращает общее время выполнения до времени самой долгой операции плюс накладные расходы на управление по- токами: беб рага11е1_тар_иИб_ргосеззез( Гипс: Са11ао1е, баба: Изб, тах_ иогкегз: 1пб = Мопс): Параллельное применение функции, используя пул проиессов. Подходит для СРП-Ьоипб операций. иИК РгосеззРоо1Ехесибог(тах_йогкегз=тах_иогкегз) аз ехесибог: # Используем тар для автоматической обоаботки итерируемого объекта гезиИз = 11зб(ехесибог.тар(Типс, баба)) гебигп гезиббз беТ сотраге_зедиепт1а1_уз_рага11е1():
Сравнивает производительность последовательного и параллельного выполнения для СРК-1пТепз1уе задач сро-СаДа = [1С000, 15000, 20000, 12000, 18000] # Последовагельнсе выполнение ьТагД-Мте = Ите .11те() зедиепНа!.гезоТтз = [сри_1п1:епз1Уе_'(:ипс7:1оп(п) Тог п 1п сри_ баСа] зеаиеп1:1а1_1:1те = Тите ггте() - зИаг'СЫте # Параллельное выполнение зГагС.Нте = Сипе. 11те() рага11е1_гези11з = рага] 1е1_тар_й11;Г|_ргос.еззез(сри_1п(:егз1уе_ ЕипсЕгоп, сри_с!а1:а) рага11е1_11те = Г1те.11те() - згагГ.Пте рг1пГ(Т 'Последовательно: {зедиепЫа1_11те: .2Т} секунд") рг1пС(Т Параллельно: {ра,а!1е1_Т1те:.2Г} секунд ) ргхпЦТ'Ускорение: {зедоепг1а1_1:1те / рага11е1_1:1П1е: ,2Т)х") # Проверяем, что результаты идентичны ргхпН-Р 'Результаты совпадают: {зедиеп1-1а1_гези1тз == рага11е1_ гезиНз}") сотраге_зедиеп1:1а1_У8_рага11е1() РгосеззРоо]ЕхесиСог создает отдельные процессы для СРЬ'-ппегшуе задач, позволяя обойти ограничения С1оЬа11п[егрге1ег Ъоск (С1Ь) в Руйгол. Каждый про- цесс получает собственный интерпретатор Руйгол, что обеспечивает истинное параллельное выполнение вычислений: бе41 аЙуапсес1_Ги1:иге_1'1аг1с1] 1пд(): II II II Демонстоирует продвинутые техники работы с КоСиге-объектами, включая обработку исключений и получение результатов по мере готовности. II II II СеЕ ро1еп11а11у_Еа111пд_Еипс1:1оп(х: хпС) -> 1пТ: ..Функция, которая может вызвать исключение."' 1Т х == 5: гахзе Уа1иеЕггэг(Т"Специальная ошибка для значения {х}") геСигп х ** 2 баса = [1, 2, 3, 4, 5, 6, 7, 8] иНН Т1ггеас1Роо1Ехеги1:ог(тах_иогкегз=4) аз ехесиТог; # Создаем словарь для сопоставления Ги1:иге с исходными данными
ГиГиге_Со_иа1ие = { ехеситог.зиЬт1Г(роГепГ1а11у_Га111пд_Гипст1оп, к): х Тог х 1п Фета } зиссеззГи1_гези11з = [] ГаПеб_гези11;з = [ ] # Обрабатываем результаты по мере готовности Гог ГиГиге 1п аз_сотр1егеб(ГиСиге_1о_уа1ие) : ог1д1па1_уг1ие = Гитиге_Го_уа1ие[ГиГиге] Ггу: гезиИ = ГиСиге.гсзи11() зисоеззГи1_гезиИз.аррепб( (ог1д1па1_уа1ие, гезиИ)) рг!пГ(Г 'Успешно обработано {ог1д1па1_уа1ие} > {гезиИ} 1) ехсерГ ЕхсертГоп аз ехс: Га 11ес1_ гезиИз. аррепЩ (ог1д1па1_уа1ие, зН (ехс))) рг1п1(Г"0шибка при обработке {ог1д!па1_уа1ие}: {ехс}") геГигп зиссеззГиНгезиИз, ГаПсскгезиИз зиссеззГи1, Га11еб = асмапсеб_ГиТиге_Ьапб11пд() рг1пЦГ ЛпИтого: {1еп(зиссеззГи1)} успешных, {1еп(Га11еб)} с ошибками") Обработка ЕиГи ге-объектов с помощью аз_сотр1с!еб позволяет получать результаты в порядке их готовности, а не в исходном порядке отправки задач. Это особенно полезно для задач с различным временем выполнения, поскольку позволяет начать обработку результатов, не дожидаясь завершения всех задач. Мар-Ребисе операции в функциональном стиле Мар Ребисе представляет собой мощную парадигму параллельных вычисле- ний, которая разделяет обработку данных на две основные фазы: шгррГпд (пре- образование) и гебистпд (агрегация). Функциональный подход к Мар Редисе в Руйтотт позволяет элегантно решать задачи обработки больших объемов дан- ных с автоматическим распределением нагрузки. Фаза Мар применяет функцию трансформации к каждому элементу входного набора данных, создавая промежуточные пары «ключ значение». Фаза Ребисе агрегирует все значения, связанные с одним ключом, используя ассоциативную операцию. Такая архитектура естественным образом поддерживает паралле- лизм на обеих фазах: Ггот соПесГюпз ГтрогГ беГаиПбю! Гго.п сопсиггепТ.ГиГигез ГтрогТ РгосеззРоо1Ехеси1:ог Ггот ГипсГ.0013 ГтроП гебисе ГпрогГ срегэГог Ггот ГурГпд итрог! ЭтсГ, 1_1зГ, Гир1е, Агу, СаПаЫе
йе* рагэ11е1_тар_рЬазе(таррег_Типс: СаИаЫе, баСа_сЬипкз: 1_1зТ[1_1зС[Апу] ] тах_логкегз: 1пТ = Иопе) -> 11зС[Ь1зТ[Тир1е]]: II II II Параллельная фаза Мар, котоэая применяет гпаррег функцию к оПипк’ам данных. Каждый сЬиг.к обрабатывается в отдельном процессе. сеТ ргссезз_с1типк(с1типк: ИзТ[Апу]) -> 1_1зТ[Тир1с] : ...........Обрабатывает один сНипк данных, применяя таррег к каждому элементу.... гезиИз - [] Фог Нет 1п сЬипк: # таррег_Типс возвращает список пар (кеу, уаЗие) тарреб_ра!гз = таррег_Типс(1Сет) гезиИз .ехТепб(тарреб_ра!гз) геТигп гезиИз иИЬ РгосоззРоо1ЕхесиТог(тах_й'сгкегз=тах_м,'огкег8) аз охссиТог # Параллельно обрабатываем каждый сПипк сКипк-гезиИз = 11зТ(ехесигог.тар(ргосезз_ск1ипк, баТа_сКипкз)) гетигп сЬипк_гези11з бе-*2 иогб_соипт_тар|эег(Ипе: зТг) -> ИзТ[ Гир1е[зТг, 1пТ]]: II II II Маррег функция для подсчета слов. Принимает строку текста и возвращает пары (слово, 1). # Очищаем и разбиваем строку на слова м/эгбз = 11ге.1ом/ег() .зТг!р() ,зр11Т() # Создаем пару (слово, 1) для каждого слова гетигп [ (логб, 1) Тог нсгб 1п иогбз 1Т мгогб. тза1р1та() ] беГ зк1иТТ1е_апб_зог1(|парреб_гезиИ8: 1_±зТ [|_1зТ[Тир1е] ]) -> О±сТ[зТг, ЫзТ | Лпу ]1 : йаза 81тиТТ1е: группирует все значения по ключам. Это промежуточная фаза между Мар и Ребисе. II II II дгоиреб_баТа = беТаиИб1с1(11зС) # Собираем все пары из всех сбипк'ов Тог с(типк_гези1Т 1п тарреб_гези1Тз: Тог кеу, уа!ие 1п с1тспк_гези1Т: дгоиреб_баТа[кеу].арэепб(уа1ие) гетигп а1сс(дгоиреб_баТа) # Демонстэация Мар-Зебисе для подсчета слов ТехТ_бата = [
"функциональное программирование эго мощная парадигма1', "про-раммирование на руСЬоп позволяет использовать функциональный стиль", "паоаллельное программирование упрощается в функциональном подходе", "руТКоп предоставляет отличные инструменты для параллельных вычислений" ] # Разбиваем данные на сКипк’и для параллельной обработки с11ипк_812е = 2 баСа_сКипкз = [1ех1_ба1а[1:1+с1пипк_з12е] Тог 1 1п гапде(0, 1еп(1ехС_ бага), сНипк_з12е)] рг1п1(Т"Данные разбиты на {1еп(ба1а_сКипкз)} сНипк'а") # Фаза Мар тарреб.гезиИз = ра.-а11е1 _тар_рЬазе(тогб_соипТ_таррег, баЫ.сЬипкз) рг1пТ(Т"Мар фаза завершена: {1еп(тарреб_гези11:з)} результатов") # Фаза 8НиТТ1е зНиТТ1еб_ба1:а = 5НиТТ1е_агб_зогТ(п1ароеа_геза11:з) рг1п1(Т"Найденс {1еп(зпиТТ1еб_ба1а)} уникальных слов") Эта реализация демонстрирует классическую структуру Мар- Ребисе. Дан- ные разбиваются на куски, которые обрабатываются параллельно в фазе Мар Ре- зультаты затем группируются по ключам в фазе 5биТТ1е, подготавливая данные для финальной агрегации: беТ рага11е1_гебисе_рбазе( гебисег_Типс : СаПаЫе, дгоиреб_баГа• ОТсНзТг, 11&1: [ Апу ] ], тах_иогкегз: 1пТ = Мспе) -> 01с1[зТг, Апу]: Параллельная фаза Зебисе, которая агрегирует значения для каждого ключа. беТ гебисе_кеу_7а1иез(кеу_уа1иез_ра1г: Тир1е[зТг, 1_1з1[Апу]]) -> Тор1с[31 г, Апу | : Применяет функцию гебисег к списку значений для одного ключа. 1 кеу, уа1иез = кеу_уа1иез_ра1г гебисеб_уа1ие = гебисег_Типс(уа1иез) геТигп кеу, геоисеб_уа1ие # Преобразуем словгрь в список пар для параллельной обоабэтки кеу_уа1ие_ра1гя = 11з1(дгоиреб_ба1а.11етз()) «116 Ргосе88роо]ЕхесиТог(тах_йогкег8=тах .адгкегз) аз ехесиТог: # Параллельно применяем гебисег к каждой группе гебиссб.рал-з = 11з1:(ехесиСог,тар(гебисс_ксу_уа1иез, кеу_ уа1ие_ра1гз)) # Преобразуем обратно в словарь геСигп 61С1(гебисеб_рабгз)
беб зит_гебисег(уа1иез: Ыз1[1пб]) -> бпб: II II II Функция гебисег дгя суммирования значений. Используется для подсчета общего количества каждого слова гебигп зит(уа1исз) беб сопр1ете_т1ар_ гебисе..ехат1р1е(): Полный пример Мар-Ребисе операции для подсчета частоты слов. II II II # Более крупный набор данных для демонстрации 1агде_бехб_баба = [ 'функциональное программирование предоставляет олегантные решения , 1руббоп является отличным языком для функционального программирования", 'паоаллельные вычисления в рубБоп становятся проще с функциональным подходом1, "тар гебисе операции идеально подходят для обработки больших данных", "функциональный стиль программирования упрощает параллельную обработку1, "сопсиггепб бибигез модуль поддерживает функциональные паттерны" # Разбиваем на сбипк’и сбигкз = [1агде_техб_бата[1:1+2] бог б бп гапде(0, 1еп(1агде_ бехб_баба), 2)] # Мар фаза ргбпб("Выполняется Мар фаза...") шарреб_гези1бз = рага11е1_шар_рБ|азе(исгб_соипб_тпаррег, сИипкз) # 5Бихб1е фаза ргбпб("Выполняется 81тибб1е фаза...") зЬибб1еб_ааба = зБибб1е_агб.зогб(таореб_гези1бз) # Ребисе фаза ргбпб("Выполняется Ребисе фаза...") ббпа1_гези1бз = рага11е]._гебисе..рНазе(зип1_гебьсег, збиТЛеб иата) # Сортируем результаты по частоте зогбеб_гези1б8 = зогбеб(ббпа1_гези1бз.ббетз(), кеу=1атЬба х: х[1], гемегзе=Тгие) ргбпб("\пТоп-1е самых частых слов:') бог лого, соипб бп зогбес_гези1бз[ :10]: ргбпб(б {тогб}: {соипб}')
гетигп Г1пэ1_ гезиИз мэгбИгедиепстез = сошр1ете_тар_гебисе_ехатр1е() Полная реализация Мар-Редисе демонстрирует, как функциональные прин- ципы упрощают параллельную обработку данных. Каждая фаза может масшта- бироваться независимо, а чистые функции гарантируют предсказуемость ре- зультатов. беТ депег1с_тар..гебисе_Тгате1Л'ог к(): Обобщенная Фгатемюгк для Мар-Вебисе операций. Позволяет легко применять разные таррег и гебисег функции. с!азз МаоРеаисебоо: сеФ___1пИ___(зеИ, таррег: СаИаЫе, гебисег: СаИаЫе, сбипк-ЗПе: 1пС = 130, тах_иогкегз: хпТ = Моле): зеИ.таэрег = таррег зеИ. гебисег = гебисег зеИ.сбипк.зПе = сбипк_з12е зеИ .тах_йогкегз = тах_тогкегз беФ ехесиУе(зе1Т, ба1а: 1_1з1[Апу]) -> 01Ы[Апу, Апу]: 1" 'Вошолняет полный цикл Мар-Пебисе... # Разбиваем данные на сбипк'и сбипкз = [ба1а[1:1+5е1Г.сЬипк_з12е] Фог 1 1п гагде(0, 1еп(ба1:а), зеИ.сНипк_с12е) ] # Мар фаза тарреб. гезиИз = рага!1е1_тар_рНазе( зеИ.таррег, сМипкз, зеИ . тах_«огкегз ) # 8НиТФ1е фаза з1|и-ГТ1еб_бсХа = з11иТТ]е_апб_зогТ(|парреб_гезиИз) # Ребисе фаза Т1па1_гезиИз = рага11е1_гебисе_рбазе( зеИ. гебисег, збиТФТеб.баТа. зеИ.тах_юогкегз ) гебигп Ф1па1_гезиИз # Пример использования для анализа чисел беФ питЬег_ала1уз1з_таррег(пит: 1пГ) -> Ь1зТ[Тир1е[зФг, 1пФ]]: ...Маррег для анализа свойств чисел." 1 1 гезиИз = [ ] 1Ф пит % 2 == 0: гезиНз.аорепб( ('еуеп', пит)) е!зе:
гезиИз.аррепб(('обб', пит)) 1Т пит > 50: гезиИз. аррепс!( (' 1эгде’, пит)) е1зе: гезиИз.аррепб((’зтаЦ’, пит)) геГигп гезиИз сеТ ИзР.гебисег (ма1иез: Из1:[1пг]) -> Из1:[1пг] : ...Чеоисег который собираем все значения в список. геРигп зог1:еб(уа1иез) # Создаем и выполняем дсЬ питЬегз = 11з1:(гапде(1, 101)) # Числа от 1 до 100 апа1уз1$_]оЬ = МарРебисебоЩ тароег-питоег_апа1уз1з_тгррег, гебисег=11зг_гебисег, сбипк _812е=20, тах_»огкегз=4 гезиИз = апа1уз1з_]оо.ехеси1:е(питЬегз) рг1п1("Анализ чисел от 1 до 100: ) Гог саГедогу, питЬег5_11з1 1п гезиИз.Иетз() : ргтпНГ'{саГедогу}: {1еп(питЬегз_11зт)} чисел (первые 5: {питЬегз_11зС[:5]})") гебигп МарРебисеиоЬ МарРсбисеЕггтещогк = дспепс_тар_гес1исе_Натс,л/огк() Обобщенная Тгателогк показывает, как функциональные принципы позво- ляют создавать переиспользуемые компоненты для параллельной обработки данных. Одна и та же инфраструктура может применяться для различных задач, меняя только таррег и гебисег функции.
Валидация данных функциональными методами В современной разработке программного обеспечения валидация данных играет критически важную роль, обеспечивая надежность и безопасность при ложений. Традиционные подходы к проверке данных часто приводят к созданию громоздких классов с множеством методов или длинных условных конструк ций, которые сложно тестировать и поддерживать. Функциональное програм- мирование предлагает элегантную альтернативу через создание небольших пе- реиспользуемых функций-валидаторов, которые можно легко комбинировать и тестировать изолированно. В данной главе раскрывается мощь функционального подхода к валида- ции данных в Руйгоп, показывается, как создавать гибкие решения. Вы научи- тесь строить валидаторы как чистые функции, объединять их в цепочки прове- рок и интегрировать с современными библиотеками типа рубапгтс, сохраняя при этом функциональный стиль программирования. Материал базируется на концепциях, изученных в предыдущих главах, особенно на понимании функ- ций высшего порядка и композиции функций. Основы функциональной валидации Функциональная валидация строится на идее представления каждой провер- ки как чистой оункпии, котовая принимает значение и возвращает либо под- тверждение корректности, либо описание ошибки. В отличие от императивного подхода, где валидапия часто реализуется через множество условных операто- ров и исключений, функциональный стиль использует композицию простых функций для создания сложной логики проверки. Основное преимущество функционального подхода заключается в том. что каждый валидатор становится независимой единицей, которую можно те- стировать изолированно и переиспользовать в различных контекстах. Это при- водит к более чистому и поддерживаемому коду, где логика валидации явно вы- ражена через комбинацию небольших понятных функций: бгот Туртпд 1гтрог1: Оптоп, СаПаЫе, Апу, ЫзТ бгош ба1ас1аззез гшрэгт оа1ас1азз @Па1ас1азз с1азз '/аИбаГгспРезиИ: Результат валидации, содержащий информацию об успехе или ошибке. 1з_уа11б: Ьоо1 уа1ие: Алу = Моле еггогз: Ьхз1:[з1:г] = Мопе
йе!7 __роз1_1п11__(&е1Т): 1Т зеИ.еггогз 1з Иопе: зеИ.еггсгз = [ ] Се47 зиссезз(уа1ие. Лпу) -> УаДтбаТюпРезиИ:: ...Создает успешный результат валидации. " 1 гетигп Уа11ба1:1опРе8и11:(1з_уа1.1.б=Тгие уа1ие=уа1ие) ФеТ Та!1иге(еггогз: ИзС[зТг]) -> Уа11с1аТ1опКези1Т : " 1 Создает неудачный результат валидации.'1" гетигп Уа1!ба1:!опР.ези11:(!з_уа11б=Еа18е, еггогз=еггогз) # Определение типа валидатора как функции УаИаатог = Са11аЫе[ [Лпу ], Уа1тбаТ1опРези1с] Этет базовый код определяет фундаментальные структуры данных .для функ- циональной валидации. Уа1!баР!спРези11: служит контейнером для результа та проверки, инкапсулируя как успешные, так и неуспешные исходы. Функции зиссезз и ТаПиге предоставляют удобные способы создания результатов вали- дации, а тип УэИааТог определяет сигнатуру всех валидирующих функций. Создание простейших валидаторов начинается с определения функций, ко- торые проверяют одно конкретное условие. Каждый валидатор фокусируется на единственной ответственности, что делает код более понятным и тестируе- мым: бе47 пот_еггр1:у( уа!ие: з1г) -> УаттбаДюпРези!! : Проверяет, что строка не является пустой. 1! по! уз1ие ог по1 уа1ие. зРгтр(): геСигп "ГатТиге] [1 Значение не может быть пустым"]) геСигп зиссезз(уа!ие.зТг!р()) бе47 т!п_1епдрН(т!п!тит• 1п1) -> УэИбаТог: II II II Создает валидатор для проверки минимальной длины строки. II II II бе!7 уа1!ба1ог(уа!ие: зТг) -> Уа1тбаПопРезиИ: 1Т 1еп(уа!ие) < шДпгтит: гетигп Са!1иге([С Длина должна быть не менее {т!п!ти|л} символов"]) геТигп зиссезз(уаТие) гетигп уаИбаТог бе! тах_1епдД|т(тах![пит: !п1) -> УаИбаДог: II II II Создает валидатор для проверки максимальной длины строки. II II II бе! уа1!баТог(уа1ие: зТг) -> Уа11баТ!спРези1Т: I! 1еп(уа1ие) > тахДтит геТигп Та!1иге([Г Длина не должна превышать {шахДтит} символов"]) геТигп зиссезз(уа1ие) гетигп уаИбэДог
Здесь демонстрируются два подхода к созданию валидаторов. Функция поб_е!грбу представляет простой валидатор, который непосредственно проверя- ет условие. Функции т1п_1епдг1т и тах_1епдб1т являются фабриками валидато- ров — функциями высшего порядка, которые возвращают настроенные валида- торы с заданными параметрами. Фабричный подход особенно мощный, поскольку позволяет создавать пара- метризованные валидаторы. Вместо создания отдельной функции для каждой возможной длины мы создаем одну фабрику, которая генерирует специализиро- ванные валидаторы по требованию: йпрогб ге беТ табсЬе8_раббегп(раббегп: збг, еггог_теззаде: збг = Моле) > УаИбабсг: Создаст валидатор для проверки соответствия регулярному выражению. сотр!1еб_ раббегп = ге .сотрПе(раббегп) беТаи1т_теззаде = ^"Значение не соответствует требуемому формату" бе)" ма11бабог(уа1ие: збг) -> Уа11баб1оп4ези1б: 1Т поб соп1р11еб_рэббегп.табсп(уа1ие) : теззаде = еггог_теззаде ог беГаи1б_теззаде гебигп 6а11иге([теззаде]) гебигп зиссезз(ма1ие) гебигп уаИбабог беТ 1з_питегбс(уа1ие: збг) -> \/а11баб1опкези1б: Проверяет, что строка содержит только цифры. 16 пот уа1ие 1зб1д1б(): гебигп 6аПиге(["Значение должно содержать только цифры ]) гебигп зиссезз(1пб(уа1ие)) # Создание специализированных валидаторов етаб1_уаИбабог = табсЬез_раббегп( г’Л[а-хА-70 9._%+-]+@[а-?А-20-9.-]+\.[а-гА-7]<2,}$', 'Некорректный формат етаП адреса1 ) рбюпе_уа11бабог = табсЬез_раббегп( г'л\+?[1 9]\б{1,14}$', Некорректный формат телефонного номера" ) Этот код показывает, как создавать более специализированные валидаторы для проверки форматов данных. Функция табсНез_раббегп демонстрирует важ- ный принцип функционально!о программирования — замыкания, где внутрен- няя функция имеет доступ к переменным внешней области видимости. Компи- ляция регулярного выражения происходит один раз при создании валидатора, что повышает производительность при многократном использовании.
Композиция валидаторов Истинная сила функционального подхода раскрывается при необходимости объединения множественных проверок. Вместо создания монолитных функций с множеством условий мы можем комбинировать простые валидаторы в слож- ные цепочки проверок, сохраняя при этом читаемость и тестируемость кода: бе* соирозе_уа]1с1аРогз(*уа11с|аСсгз: УаЗлбаЮг) -> УаИйабог: Объединяет несколько валидаторов в один, применяя их последовательно. Прекращает выполнение при первой ошибке. беГ сото1пеб_'/а11да1ог(уа1ие: Апу) -> УаИйабюпПезиИ: сиггепб_уа1ие = уа1ие а11_еггогз = [] Гог уаИйабог 1п уаИйабогз: гезиИ = уа11с1абог(сиггепС_уа1ие) И по! гезиИ. 1з_уа11с1. а11_еггогз .ехбепсЦ гезиИ .еггогз) # Прекращаем выполнение при первой сшибке гебигп са11иге(а11_еггогз) # Используем валидированное значение для следующего шага сиггепб_уа1ие = гезиИ. уа1ие гебигп зиссезз(сиггепб_уа1ие) гебигп сотЬ1пес!_уа11с1Иог бе* уа11с1абе_а11(*уа11йабсгз. Уа11с1абог) -> УаНбабог: Выполняет все валидаторы независимо от результата, собирая все ошибки. II II II йеГ сошо1пес1_уа11с1абог(уа] ие: Апу; -> УаИйаббопКезцИ: аИ_еггогз = [ ] Г1па1_уа1ие = уа1ие Тог уа11бабог 1п уаИбабогз: гезиИ = уа11йабог(уа1ие) 1Г по! гезиИ. 1з_уа11б: аП.еггогз .ехбепсЦ гезиИ .еггогз) е1зе: # Сохраняем последнее успешно валидированное значение Г1па1_уа1ие = гезиИ. уа1ие 1Г аП.еггогз: гетигп Га11иге(а11_еггогз) гебигп зиссезя(Г1па1_уа1ие) гетигп сотЫпескуаИбабог
Функция сотрозе.уаИааюгз реализует последовательную композицию ва- лидаторов, где каждый следующий валидатор получает результат предыдущего. Это полезно для трансформирующих валидаторов, которые не только проверя- ют, но и преобразуют данные Функция уа11ба1:е_а11 выполняет все проверки независимо, что полезно для сбора всех возможных ошибок одновременно. Выбор между этими стратегиями зависит от конкретной задачи. Последо- вательная композиция более эффективна, поскольку прекращает выполнение при первой ошибке, но может скрыть дополнительные проблемы с данными. Параллельная валидация дает полную картину ошибок, нс может быть менее эффективной: # Создание составного валгдатора для имени пользователя изегпате. уаИбаГог = сотрозе ,уа11ба(:огз( поЦ.етрбу, ппп_1епд111(3), тах_1епдП1(50), таСсбеЗ-ОаЩеггЦ г'А[а-гА-20-9_]+$', 'Имя пользователя может содержать только буквы, цифры и подчеркивания" ) ) # Создание валидатора для сбооа всех ошибок пароля раззйог б.уаИбаТог = Ус11баТе_а11( поТ.етрСу т1п_1епдТ1э(8), таТсНез_ра1:1егп( г' *[А-2].*’, 'Пароль должен содержать хотя бы одну заглавную букву11 ), таСсбез_раЩегп( г'.*[а-г], 'Пароль должен содержать хотя бы одну строчную букву" ) тагсбез„раТтегп( г',*\б.*', 'Пароль должен содержать хотя бы одну цифру' ) ) # Демонстрация использования беТ ТезТ_уа11ба1:огз(): # Тестирование валидного имени пользователя гезиН = ц8егпагпе_уаИба1:ог("зо1тп_бое123") рг1пФ(Т' Валидация имени: {гезиН. 1з_уа11б}, значение' {гезиИ. уа1ие}") # Тестирование невелидного пароля гезиИ = ра88иогб_уа11баЮг( 'леак")
рг1пТ(Г"Валидация пароля: {гезиИ. 1з_уа11 б)") Тог еггог 1п гезиИ.еггогз: рг!п1:(Т" - {еггог}") 1ез1:_уаИба1:эгз() Этот пример демонстрирует практическое применение композиции валида- торов для создания сложных правил проверки. Валидатор имени пользователя использует последовательную композицию, поскольку проверки логически свя- заны — нет смысла проверять длину пустой строки. Валидатор пароля собирает все ошибки, что помогает пользователю исправить все проблемы одновременно. Создание условных валидаторов позволяет реализовать более сложную бизнес-логику, где выбор проверок зависит от самих данных или внешнего контекста: бе-*2 сопбИ1опа1_уа11ба!ог( сопбИ1оп: СаПаЫе) [Апу], Ьоо1], 1:гие_\/а11с1а1ог: УаПбаТог, Та1зе_уаИбаТог: УаИбаЫг = 1апЬба х зиссез8(х) ) -> УаИбаТог: Применяет различные валидатооы в зависимости от условия. се^ уа11баТог(уа1ие: Апу) -> УаИбаПопРезиИ: И сопбИтоп)уа1ие): гебигп тгие_уа11ба1ог(уа1ие) е1зе гебигп 1а1зе_уа11бабог(иа1ие) гебигп уаИбабог бе* орТ1опа1_уа11бабсг(уаПбабог: УаПбабог) > УаИбабсг: II II II делает валидатор необязательным - пропускает пустые значения. II II II беТ орЫопа1_уа] (уа1ие: Апу) -> УаИбаПопРези1б : 1Т уа1ие 13 Мопе ог уа1ие == гетигп зиссе5з(уа1ие) гебигп уаИбасог(уа1ье) гетигп орт1опа1_уа1 # Создание условного валидатора для возраста аде_уаИбабог = сопбИ1опа1_уа11бабог( 1ат1Ьба х: 1з1пзбагсе(х, збг) апб х.бзбтдИО, сопрозе_уа11бабогз( 13_пит1ег1С, 1ашоба аде: зиссезз(аде) И 0 <= аде <= 153 е1зе ба!1иге(["Возраст должен быть от 3 до 153 лет']) ). 1алЬба х: Та11иге([1 Возраст должен быть числом"]) )
# Создание необязательного валидатора для отчества т1бб1е_пате_уа11бабог = орб1опа!_уа11бабог( сопрозе_уа113абогз( поГ_ еггрГу, шах_1епдбН(10И) ) Условные валидаторы особенно полезны при работе с динамическими дан- ными, где правила валидации могу]' изменяться в зависимости от типа или со- держания данных. Необязательные валидаторы помогают обрабатывать поля, которые могут отсутствовать, но должны соответствовать определенным прави лам. если они присутствуют. Цепочки проверок и монадическая композиция Для более элегантной работы с последовательностями валидаций можно применить монадический подход, который позволяет создавать цепочки опера- ций, автоматически обрабатывающие ошибки. Это особенно полезно при созда- нии сложных валидацис.нных пайплайнов, где результат одной проверки влия- ет на выполнение следующих: бгот бурбпд нтрогб ТуреУаг, бепегбс Т = ТуреУаг('Т’) II = ТуреУаг(' II') с1азз Уа11баб1опС1та1п(6епег1с[Т|): Монадическая обертка для создания цепочек валидации. бе-*2_1пи___(зе1Т, гезиИ: УаИбабюпКезиИ): зеИ._гезиИ = гезиИ @с1аз8те Нюа беб от(с1з, уа1ие: I) -> 'Уа11с1аУ1ог.С1па1п[Т] ’ : 1 Создает цепочку с успешным значением.'" гебигп с1з(зиссезз(уа1ие)) @с1аззтеббоа беб Та11иге_оТ(с1з, еггогз: Ызб[збг]) -> 'Уа11баб1опСЬа1п[Т] ’ ..Создает цепочку с ошибкой. гебигп с1з(’та11иге(еггэгз)) беб сба1п(зе1б, уаИбабог: УаИбабог) -> 'Уа1±баб1опС!па1п[7] ’ : II II II Применяет валидатор к значению, если предыдущий шаг был успешным. 16 поб зеИ . _гезиИ. 1з_уа11б : гебигп Уа11баб1опС1та1п(зеИ._гезиИ)
пеи_гези11 = ма11с!а 1ог(ее1Т ._гези1!.уэ]ие) геТигп Уа11с1аМопСКа1п(пем/_гезиИ:) бе! тар(зе1!, ХгагзТсгтег: Са11аЫе[ [т], II]) -> 'Уа1!йа!!опСЬа!п[и]' : Применяет трансформацию к значению, если валидация успешна 1! по! зе1ГгезиИ 1з_ма11б: гетигп Уа1±йа!!опС1та!п(зе1ТгезиИ) Тгу: пе\к_уа1ие = 1гапз!огтег(зе1!._гези11 ,уа1ие) геТигп Уа1тйа!!опСЬа!п(зиссезз(пеи/_уа] ие)) ехсер! ЕхсерТлоп аз е: геТигп УаИбаНспСНатп(Та! 1иге([зТг(е) ])) Йе! де!:_ге5и11(зе1!) -> УаИйаНопПезиИ:: Извлекает итоговый результат валидации гебигп зе1!._гези1Т @ргорег!у ае! 1з_ма11с1(зе1Т) > Ьоо1: 11"Проверяет, успешна ли цепочка валидации.' геТигп зе1!._гези1!.!з_уа11й ©ргорегГу йе! уа1ие(зеИ) -> Т: 11"Получает значение из успешной валидации.' !! по! зеИ. _гезиИ . 1з_уа11й: га±зе Уа1иеЕггэг( 'Нельзя получить значение из неуспешной валидации") геТигп зе1Г._гезиИ^а1ие @р|орегТу йе! еггогз(зе1!) -> Е1з![з!г]: ..Получает список ошибок валидации 1 геТигп зе1!._гезиИ.еггогз Класс Уа11йа!!опСЬа!п реализует монадический интерфейс для работы с валидацией, позволяя создавать элегантные цепочки проверок. Метод сНа!п обеспечивает автоматическое прекращение выполнения при первой ошибке, а метод пар позволяет трансформировать данные в рамках валидационнот о пай- плайна: йе! Тг1ш_апй_]о1уегсазе(уа1ие: з!г) -> з!г: .......Трансформирует строку: удаляет пробелы и приводит к нижнему регистру.1" 1 гетигп ^а1ие.зтг!р(),1о«ег()
беГ рагзе_1п(:едег(уа1ие: зГг) -> 1п1: ..Преобразует строку в целее число. "" геГигп 1гт1(уа1ие) # Создание комплексной цепочки валидации для епаП беГ Уа11са1:е_еп1а11_о1ра1п(ета11: зГг) -> 7а110аСюпСНатп[з!г] : геГигп (Уа11баС1опСЬэ1п.оГ(ета11) .с1та1п(по1_етрГу) . 1гар(Гг1т_апб_1ойегсазе) . сЬа1п(е1га11_уа11батог) , сба1п(тах_1егдГГ|(255))) # Создание цепочки валидации для числового ввода беГ ^а11с!ате_адс_сНа1п(аде_з1:г збг) -> Уа11бат101тСИагп[1пС | : геГигп (Уа11с!а11опС1ра1п. оГ( аде_з!г) .сбатп(поГ_етр1у) .сГта1п(1з_питег1с) ,сЬа1п(1ап1ЬЭа аде: зиссезз(аде) 1Г 18 <- аде <= 65 е1зе Га11иге([ 'Возраст должен быть от 13 до 65 лет"]))) # Демонстрация использования цепочек беГ 1ез1_уа11ба1Гоп_с11а1п8(): # Тестирование валидации ешаП етаП_гези11 = уа11баГе_еп1а11_с11а1п(" боЬг.Сое@ЕХАМР1_Е.ССМ ") 1Г ета11_гези1т. 1з_\/а11б: рг 1П1 (Т 1 Ета11 валиден: {ета11_гези1Е.уа1ие}") е1зе: рг 1рл(Т"Ошибки етаП: ]ета11_гези1С.еггогз}") # Тестирование валидации возраста аде_гези11 = уаИбг Ле_аде_сНа1п( '25') 1Т аде_гези1Г,1з_уе11б: рг1пГ(Г"Возраст валиден: {аде_гези1т.ма1ие}") е1зе: рг1пГ(Г"0шибки возраста: {аде_гези1Г.еггогз}") # Тестирование невалидного случая 1пуа11б_аде = уа11бате_аде_сРа1п("аЬс") рг1пГ(Г"Невалидный возраст: {1П7а11б_аде еггогз}") Гезг_уа11ба11оп_с1та1пз() Монадическая композиция особенно эффект ивна при создании пайплайнов обработки данных, где каждый шаг может как валидировать, так и трансформи- ровать данные. Это приводит к созданию декларативного кода, который легко читается и понимается. Для обработки сложных объектов с множественными полями можно создать специализированные функции, которые применяют различные валидаторы к разным атрибутам объекта: Ггот ба1:эс1аззез ГтоогГ ба1ас1азз Ггот ГурГпд ГтрогГ ЭтсГ, Апу
(?бэбас1азз с1азз Озег: " "Модель пользователя."1' изегпате: збг етаП: збг аде: бпб рЬопе: збг = Копе бе Г уа11с1а('е_оЬ}есТ_б1е1с1з( сага: 01сб[збг. Агу], ТбеН.уаИбабогз: Э1сТ[зГг, УаИбатог] ) -> УаИдаМогРезиИ:: Валидирует поля объекта согласно заданным правилам. уа11Рабеб_бабэ = {} а11_еггогз = [] Тэг Т1е1:1_гате, уаИбабсг 1п Т1е1б_уа11бабогз 1бетз(): Т1е1а_уа1ие = баба.дсб(Т1е1б_пзте) гезиИ = уаИс1абог(Тбе1с1_уа1ие) И гезиИ. 1з_уа11с1: уа11с1абесЦс1аба | ТбеН.пате] = гезиИ.уа!ие е1зе: # Добавляем префикс с именем поля к каждой ошибке Гбе1б_еггогз = [Т"{Тбе1б_пате}: {еггог}" бог еггог бп гезиИ .еггогз] аИ.еггогз ,ехбепс(Т1е1б_еггогз) И а11_еггогз: геТигп Таб1иге(а11_еггогз) гетигп зиссезз(уа11бабес1_баба) # Определение валидаторов для полей пользователя изег_уа11бабогз = { 'изегпапе' : изегпате_уа1ббабог, 'ета11' : ета11_уа!1бабог, 'аде': соирозе_уа1ббабэгз( бз_питегбс, Затэда аде: зиссезз(аде) 1Т 16 <= аде <= 120 ебзе Таб1иге([ Возраст должен быть от 16 до 120 лет ]) ), рбопе’ : орТТопаНуаИбаТоИрЬспе-УаИсато'') } бе41 сгеа1с_уа11баТеа_изег(и8ег_баТа: О1сб[збг, Апу]) -> УаИбаТТопРезиИ: II II II Создает объект пользователя с валидацией всех полей. II II II
уа11ба!зоп_гези11: = уа11ба1е_оЬ]ес1_’Е1е1бз(изег_бэ1а, изег_ уаИбаЕогз) 1Т по! уаИба!1оп_гези1!. 1з_са11с1: ге!игп уа11ба!1оп_гезиИ # Создаем объект бзег из валидированных данных уа11оа!еб_ба!а = уа11ба!1оп_гези1!. уа1ие Ну: изег = 11зег(**уа11ба!еб_баса) гетигп зиссезз(изег) ехсерс Ехсер!юп аз е: ге!игп ГаПиге( [Т Ошибка создания пользователя; {з!г(е)}"]) # Тестирование валидации объекта !ез!_изег_ба!а = { изегпате' : ' ]оПп_аое’, ‘ета11 : ']обп@ехатр1е.сст', 'аде' : ‘25’, рбопе’: '+1234567398’ } гезиИ = сг’еа!е_уа11ба!еб_изег(!ез!_изег_баСа) И гезиИ. 1з_уа11с1: изег = гезиИ уа1ие рг1пГ(Т"Пользоеатель создан’ {изег}") е1зе: рг!п!("Ошибки валидации:") Тог еггог 1п гезиИ еггогз: рг1п!(Т" - {еггог}") Этот подход демонстрирует, как функциональная валидация может быть применена к сложным структурам данных. Функция уаИс1а!е_оЬ]ес!_1ые1с!з является универсальной и может быть переиспользована для различных типов объектов, просто изменив набор валидаторов полей. Интеграция с Рус1апТ1С в функциональном стиле Библиотека Рубап!1с предоставляет мощные инструменты для валидации данных, основанные на типах РуШоп. Хотя Руоап!1с использует объектно-ори- ентированный подход, его можно эффективно интегрировать с функциональ- ным стилем валидации, создавая гибридные решения, которые сочетают удоб- ство Рубап!1с с гибкостью функциональных валидаторов: Тгош рубап!1с 1гпрог1: ВазеМобеБ, уаБ1ба!ог, УаББбаПэпЕггог Тгот !урБпд пирог! Ор!юпаБ с1азз 1)зегМобеБ(ВазеМосеБ): Модель пользователя с базовой валидацией через Рус!апт1с изегпате: з!г
етаН: зТг аде: хпТ рНопе: 0рт1ог1а1[з1"г] = Ыопе с1азз СопТТд: # Разрешаем дополнительную валидацию уаИс1ате_а581дптепг = Тгие # Используем епит по значению изе_епит_уа1иез = Тгие Се-*1 рус1апТ1с_то_Типс1:1опа1( побе1_с1азз: Туре, абс1И1опа1_уа11Ца1:огз: 01оТ[зТг, Уа11с1а1ог] = Чоге ) -> Уа11ба1ог. Создает функциональный валидатор из модели Руг1апТ1с. с!еТ уа!1с1а1:ог_Типс(с1аТа: РтсЦзТг, Апу]) -> УаИбаТтопКезиИ: Тгу: # Основная валидация через РубепНс шсс!е1_1пз1:ипсе = тос1е1_с1аз8(**ба1:а) уаИба! еб_ба1а = гпобе1._1пз1 апсе. с11с1() # Применяем дополнительные функциональные валидаторы 1Т аббИюпа1_уа11ба1:огз: *сг Т1е1б_пате, аббИ1опа1_уа]1ба1ог 1п ас!с111:з.опа1 уа11аатогз.1сеп'1з(): 1Т Т1е1б_пате 1п уаНбаТеб. баТа • Т1е1б_уа1ие = уа11ааТеб_баТа[Т1е1б_рате] гезиИ = абаИ1опа1_уа11баТог(Т1е1б_уа1ие) 1Т пот гезиИ. 1з_уа11б: Т1е1б.еггогз = [Т1 {Т1е1с1_пате}: {еггог}" Тог еггог 1п гезиИ.еггогз] гетигп Та11иге(Т1е1б_еггог8) уа11са1еб_бата[Т1е1б_пате] = гезиИ.уа!ие гетигп зиссезз!уа11баТеб_баТа) ехсерТ УаИбаТтопЕг гог аз е: еггогз = [] Тог еггог 1п е.еггогз(): Т1е1д = " -> 1 .}о1п(з1г(1ос) Тог 1ос ю еггог['1ос ]) теззаде = еггог['тзд' ] еггогз.аррепй(Т' {Тте1с1}: {теззаде}") геТигп ТаПиге(еггогз) ехсерт ЕхсертТоп аз е: гетигп Та11иге([Т Неожиданная ошибка валидации: {8Н(е)}"]) гетигп уаИбаТог.Типс
б>ункция рубапб1с_со_ТипсС1опа1 создает мост между Рубапбтс и функцио- нальным подходом, позволяя использовать мощные возможности типизации Рубапббс, дополняя их пользовательскими функциональными валидаторами. Это особенно полезно для сложных бизнес правил, которые непросто выразить через стандартные валидаторы Рубапббс: беб Ьиз1пезз_ги1е_изегпате(ияегпате. 81г) -> УаПбаббопРезиТб: Бизнео-гравило: имя пользователя не должно содержать оскорбительных слов. богЫббел_йогбз - ['абмбп', 'госб', 'зузбет', безб‘] 1ом/ег_изегпате = изегпате.1ойег() Гог йогб 1п богЫбсеп.йогбз: 16 логб 1п 1ойег_изогпате: гебигп Та11иге([Т Имя пользователя не может содеожать слово '{йогб)’"]) гебигп зиссезз(изегпэте) беб Ьиз1пез8_ги1е_ета11_бо||1а1п(ета11: збг) -> У'аИбаИопРезиИ: Бизнес-правило. етаП должен быть из разрешенных доменов. аПоиеб.ботабпз = [1 сотрапу сот’, ’рагбпег.огд , 'с11епб.пет'] ботатп - ета11.зр!1б()[-1].1отег() 16 ботатп поб 1п аПолеб.ботабпз: гебигп ба!1иге( [Г’ЕтаП домен '-{ботатп}' не разрешен"]) гебигп зиссезз(егга11) # Создание гибридного валидатора изег_КуЬг1б_ уаИбабог = рубапб1с_бо_бипсбюпа1 ( бзегР'обе! { 'изегпаше : Ьизбпезз_ги1е_изегпате, 'етаН' : Ьизбпезз ги1е_ета11_ботатп } ) # Тестирование гибридной валидации безб_баба = { изегпате’: абт1п_изег’, 'етаН': 'изегОдтаН.сот', ' аде'• 25, 'рбопе’: '+1234567390 } геяибб = и8ег..1гуЬгбб_уа11бабсг(тезб .баса) 16 гези1б.1з_уа11б: рг1пб(6"валидация успешна: {гези1б.уа1ие}")
е1зе: рг1ггС( "Ошибки валидации ") Гог еггог 1п гезиИ. еггогз: рг1п1:(1Г1 - {еггог}") Этот пример показывает, что бизнес-правила могут быть реализованы как от- дельные функциональные валидаторы и интегрированы с базовой валидацией РуФапГ!с. Такой подход обеспечивает четкое разделение между техническими ограничениями (типы, форматы) и бизнес-логикой.
Функциональное тестирование Тестирование в функциональном программировании кардинально отли- чается от традиционных подходов тестирования императивного кода. Когда мы работаем с чистыми функциями (риге Гилсйопз) и неизменяемыми данны- ми (пптШаЫе ба(а), наши тесты становятся более предсказуемыми, надежными и выразительными. В этой главе мы изучим современные методы тестирования функционального кода на РуЛоп, познакомимся с концепцией тестирования на основе свойств (.ргорепу-Ьазеб сезин§) и научимся генерировать тестовые дан- ные функциональными методами. Функциональное тестирование выходит за рамки простой проверки входов и выходов. Оно позволяет нам описывать математические свойства наших функ- ций и автоматически проверять их на тысячах различных входных данных. Та- кой подход помогает находить граничные случаи, которые человек мог бы упу- стить, и дает нам уверенность в корректности нашего кода. Основы функционального тестирования Функциональное тестирование базируется на фундаментальном принци- пе определения чистых функций. Чистая функция всегда возвращает одинако- вый результат для одинаковых входных данных и не имеет побочных эффектов. Это свойство делает тестирование таких функций значительно проще и надеж- нее по сравнению с тестированием функций, которые зависят от внешнего со- стояния или модифицируют его, Традиционный подход к тестированию основан на примерах — мы создаем конкретные входные данные и проверяем конкретные ожидаемые результаты. Этот метод хорошо работает, но имеет существенное ограничение: мы можем проверить только те случаи, о которых подумали. Функциональное тестирова ние предлагает дополнительный уровень — тестирование свойств, где мы опи- сываем общие правила, которым должна подчиняться функция, а генерация конкретных тестовых случаев происходит автоматически’ хтрог! тасб Гсолз ДурДпд пгрог! (_15С, СаПаЫе беТ здиаге_гоо1(питЬег: НоаД) -> Т1оаг: Вычисляет квадратный корень из числа. Аидз: потЬег; положительное число РеТигпз: квадратный корень из числа
1Т питЬег < 0: гаТзе Уа1иеЕггог("СаппоТ са!си1аТ.е зциаге гоо1: сТ педаТТуе питЬег ) гетигп паТЬ.вргц(питЬег) # Традиционный тест на основе примеров беТ Тез1:_здааге_гоо1:_ехатр1ез(): ...Тестирует конкретные случаи вычисления квадратного корня....... аззегТ здиаге_гос1:(4.0) == 2,0 аззегб зциаге_гоот(9.0) == 3.0 аззегд сЬз(зциаге_гос1(2.9) - 1.414213562373095) < 1е-10 В этом традиционном тесте мы проверяем только три конкретных случая. Но что если есть другие значения, которые могут вызвать проблемы? Функци- ональный подход позволяет нам сформулировать свойство: для любого поло- жительного числа х квадрат квадратного корня должен равняться исходному числу: беТ ргорег1;у_зриаге_гоо1:_1пуегзе(пи1пЬег: Т1оаТ) > Ьоо1: Проверяет свойство: здгб(х)2 = х для положительных чисел. Агдз: пишоег: тестируемое число КеТигпз: Тгие, если свойство выполняется 1Т питЬег < 0; гебигп Тгие # Свойс_во неприменимо к отрицательным числам гезиИ = зциаге..гоо1:(питЬег) гетигп аЬ5(гезиИ * гезиИ - питЬег) < 1е-10 # Проверяем свойство на нескольких значениях 1ез1_уа1иез = [1.0, 4.0, 0.25, 100 0, 0.060001, 1000060.0] Тог уа!ие 1п Те$1_уа1иез: аззеН ргорег1:у_зциаге_гоо1:_1пуегзе(уа1ие), Т"Ргорегбу Та11еб Гог {уа1ие} Этот подход уже лучше, но мы все еще ограничены заранее выбранными зна- чениями. Именно здесь проявляется сила автоматической генерации тестовых данных. Чистые функции идеально подходят для тестирования свойств, поскольку их поведение полностью определяется входными параметрами Рассмотрим функ- цию сортировки как классический пример: беТ Типсб1спа1_зог1:(11:е1гз : 1_1з1:[1п1:]) -> Ызт [ 1пб ] : Сортирует список, не изменяя исходный список. Агдз: Иетз: список для сортировки
Ре1игп8: новый отсортированный список геРигп зогбеЩИетз) беТ ргорегту_8сг'1_р'езегуе8_1епд1:б(11е,п8: 1_1зФ[ 1п1:]) -> Ьоо1: 1 |Проверяе’г, что сортировка сохраняет количество элементов.11 зоггеб_1Гетз = ТипсИопа^зсг-ЦзЛетз) геРигп 1еп(8ог1еб_1Сетз) == 1еп(11етз) беТ ргорегту_зог1:_1з_огбегебщбеп1з: 11зт[1пХ]) -> ооо1: Провеояе’г, что оезультат сортировки действительно отсортирован.... зогСесЫРетз = ТипсИопаТ.зогСЦРетз) Тог 1 1п гапде(1еп(зогСеб_1гетз) - 1): 1Т 8ог1еб._11етз[1] > зогбесСИетзЦ + 1]: геСигп Еа1зе гебигп Тгие беТ ргорегбу_8сг1:_ргезег\'ез_е1етеп1:з(1бет& ИзГ [ хп! ]) -> Ьоо1: ' 'Проверяет, что сортировка сохраняет все элементы."'" 8ог1еб_11етз = Типсб1опа1_зсгЩ1бетз) # Проверяет, что каждый элемент из исходного списка есть в результате # и наоборот (с учетом повторений) гебигп зогбеб(1бетз) == зогбесЦббетз Эти свойства описывают фундаментальные характеристики функции сорти- ровки независимо от конкретной реализации алгоритма. Такой подход делает тесты более выразительными и помогает выявить ошибки в логике, а не только в реализации. РгореПу-Ьазес! тестирование с Нуро11пе818 Библиотека НуробНезтз представляет собой мощный инструмент для реали- зации тестирования на основе свойств в Руйтоп. Она автоматически генерирует разнообразные тестовые данные, включая граничные случаи, которые человек мог бы пропустить, и при обнаружении ошибки пытается найти наиболее похо- жий пример, демонстрирующий проблему. Основная идея Нуробпезбз заключается в том, что вместо написания кон кретных тестовых случаев мы описываем стратегии генерации данных и свой- ства, которым должны удовлетворять наши функции. Библиотека затем генери рует множество тестовых случаев и проверяет наши утверждения: Тгот КуробПеэбз 1тпрогг дбмоп, збгабедбез аз 31 нпрогб Пуроббезбз.збгабедгез аз зб @д1меп(зТ. 1:1оаб8(п11п_мг1ие=0. 0, тах_ма1ие=1 еб, а11ои_пап=Еа1зе, а11о«_1п'Е1п11у=Еа1зе)) беТ безб_здиаге_гооб_ргорегбу(пип1Ьег):
Проверяет свойство квадратного корня с автоматической генерацией данных. НуроГбеаГз автоматически генерирует различные положительные числа и проверяет, что здг’Цх)2 = х гезиИ = здиаге_гоо1:(питЬег) # Проверяем обратное свойство с учетом погрешности вычислений с плавающей точкой аззегС абз(гезиИ * гезиИ - пипЬег) < тах(пишЬег * 1е-10, 1е 10) @д1усп(з1. Г1оаГз(тах_уа1ие=-1е-10)) беГ 1:ез1_здиаге_гоо1_педа1:1уе_га18ез_ехсер1:1оп(педа1Гуе_питЬег): II II II Проверяет, что функция корректно обрабатывает отрицательные числа. Ну. здиаге_гоо1:(педа11уе_пип1Ьег) аззегб Еа1зе, Г'Ехресбеб УаТиеЕггог 'Гог {педаС1уе_питЬег}" ехсерГ Уа1сеЕггог: разз # Ожидаемое поведение Декоратор ©дъуег указывает НурсТНез 1 з генерировать тестовые данные в со- ответствии с заданной стратегией. В первом тесте мы просим генерировать по- ложительные числа от 0 до миллиона, исключая специальные значения как и бесконечность. Во втором тесте генерируются отрицательные числа для про- верки обработки ошибочных случаев. Эффективность НуроТПестз особенно проявляется при тестировании более сложных структур данных. Рассмотрим функцию для работы со списками: беГ ип1дие_е1етепГз(11:етз: Пэт. [ 1п1 ]) -> ь1зт[1п1]: Возвращает список уникальных элементов, сохраняя порядок первого появления. Лгдз: Иепз: исходный список РеГигпз: список уникальных элементов в порядке первого появления II II II зееп = зеГ() гезиИ = [] Гог Иен 1п Иетз: 1Г Пет поГ 1п зееп: зееп. абЩ Нет) гезиИ. аррепсЦ Нет) геГигп гэзиИ ©дзлеп (зс. 1181:8(51.1пгедегз()))
беТ Сез1:_ип1дие_е1етеп1:з_г.о_бир11са1ез(Петз) : ..Проверяет, что результат не содержит дубликатов. "" гезиИ = ип1дие_е1етеп1:з(Иет8) аззегс 1еп(гези1Г) == 1еп(зе1:( гезиИ)) ©дтуе-цз!. 11збз(з'!:. 1гт1едегз())) с!еТ тезг_ип1дие_е1етепГ5_р''езегуез_сгбег(петз): .......Проверяем, что порядок первых появлений элементов сохраняется.... гези1г = ип1дие_е1етеп1:з(Иетз) # Создаем словарь с индексами первых появлений Г1гз1:_1пс11сев = {} Тог 1, Иет 1п епипегате(Иетз) 1 1Т Пет по1 1п Т'1гз1_1пб1сез: Е1гз1_1пб1сея[Иет] = 1 # Проверяем, что результат отсортирован по индексам первых появлений Тог 1 и гапде(1еп(гезиИ) - 1): сиггсп1:_Т1г81:_1пбех = Т1гз+_1па1оез [ гезиИ [ 1] ] пехТИизТИпбех = ТТгзт 1пб1се8[ гезиИ [1 + 1]] аззеН сиггеп1_Т1гз1_1пс1ех < пехСТ1гзТ_1пс1ех @д!уеп(зТ.11313(зТ.ТпТеде!з())) беТ 1:езг_ип1дие_е1етепт8_соп1а1п8_а11_ип1дие(Иет8): 1 'Проверяет, Что результат содержит все уникальные элементы исходного списка." " гезиИ = ип1дие_е1етег1з(Иет8) ип1дие_Иетз = зеЦИетз) аззегС зе1(гезиИ) == ип!дие_Иетз Эти тесты проверяют различные аспекты функции ип1дие_е1етептз на ты- сячах автоматически сгенерированных списков различной длины и содержа- ния. НуроТЬезтз автоматически находит интересные случаи, такие как пустые списки, списки с одним элементом, списки со всеми одинаковыми элементами и списки с различными комбинациями повторяющихся элементов. Одной из самых ценных возможностей НурсТбезТз является сжатие (зйппктд) — когда тест «падает», библиотека пытается найти наиболее подходя- щий пример, демонстрирующий проблему. Это значительно упрощает отладку по сравнению с получением случайного сложного входного значения: бе! Ьидду_ббу1з1оп(б1У1бепб: Поай, бПтзог: Т1оа1) -> Ноаб: Функция деления с намеренной ошибкой для демонстрации сжатия. # Ошибка: не обрабатываем случай, когда 61у1зог очень близок к нулю И аЬз(б1у1зог) < 1е-160: # Слишком строгая проверка га±зе 2эго01у1з1ог.Еггог("01у1з1оп Ьу хего ) гебигп б1у1оепб / 61у1зог @д1уеп(зт. Г1оасз(а11О1л/_паг.=Еа1зе, а11эщ_1п1:1п1ту=Еа1зе),
8Г. Г]оа-1з(т1п_уа1ие=1е-99, тах_уа1ие-1е-50, а11оад_паг.=Еа1зе, а11ои_1п'(:1п11’у=Еа1зе)) Фе* 1ез1:_й1у1з1оп_и11Ь_зп1а11_с11718сг(с11У1аепс11 сИутзог) : Тест, который выявит олибку в обработке малых делителей. Нуро'Фтезтз найдет наиболее подходящий пример, демонстрирующий проблему. гезиИ = Ьидду_спу1з1оп(с!1у1бепс1, Лу1зог) 4 Проверяем, что результат конечен и корректен аззег-р пот таФт. 1зпап( гезиИ) аззегр по! таСб. 1з1пТ(гезиИ) аззег-р аЬз(гезиИ * б1У1зог - Ф1у1ФепФ) < аЬз(сИу1бепс1) * 1е-10 Когда этот тест «упадет», НузоРИез1з предоставит простой пример вро- де Ф1 \/1ФепФ=0.0, 61у1зог=1 е-99. а не сложную комбинацию чисел, что значи- тельно упростит понимание и исправление проблемы. Генерация тестовых данных функциональными методами Функциональное программирование предоставляет элегантные способы создания генераторов тестовых данных. Вместо процедурного подхода к ге- нерации мы можем использовать композицию функций, частичное приме- нение и другие функциональные техники для создания мощных и гибких ге- нераторов. Основной принцип функциональной генерации данных заключается в соз- дании небольших переиспользуемых генераторов, которые можно комбиниро- вать для создания более сложных структур данных. Это похоже на то, как мы соз- даем сложные функции из простых строительных блоков: (тот Типс1:со1.з тггрогт: рагЫа! (тот Сурлпд гпрогУ бепегаСог, ТуреУаг, СаПаЫе 1трог1 гапбот 1трсгТ з1:г1пд Т = ТуреУаг(’Т') бе'*2 сопзСап-(:_депегг1сг(уа1ие: Т) -> Са11аЫе[ [], Т]: II II II Создает генератор, который всегда возвращает одно и то же значение. Агдз: уа1ие: константное значение Кегигпз. функиия-генератор гетигп ТатЬба: уа!ие бе-Г сНо1се_депегэ1юг(ор1:1опз: Ызг[Т)) -> Са11аЫе[П, Т] II II II Создает генератор, который случайно выбирает элемент из списка.
Агдз: орТтопз: список возможных значений РеТигпз: функция -I енератор геТигп 1ашЬс1а: гапс!от1.с1то:1се(орС1эп5) беТ гапде депегаТог (птхп_уа1ие: 1пТ, птах_уа1ие: 1п1) -> Са11аЫе[ [], 1пТ] : Создает генератор целых чисел в заданном диапазоне. геТигп ТатгЬаа: •'апсот. гапс!1пТ(т1п_уа1ие, тах_уа1ие) # Создаем простые генераторы 8ша11_роз11:1Уе_1п1 = гапде_депега1ог(1, 100) уоие1_депегаСог = сПо1се_депегаТог([1 а', 'е', 'с', ‘и’]) бооТеап.-депе-аТог = с1то1се..депегаТог( [Тгие, Еа1зе]) # Демонстрируем использование рг1п1(Т"Случайное малое число: {зта11_розгТ1Уе_1пР()} 1) рг!п1:(Т" Случайная гласная: {уо'ле1_депега1;ог()}") рг1п1(Т"Случайный булев: {Ьос1еап_депегаТог()} ) Композиция генераторов позволяет создавать более сложные структуры дан- ных из простых компонентов. Функциональный подход делает такую компози- цию естественной и читаемой: беТ со1тЫпе_депега1огс(*депегаГсгз: Са11эЫе[[], Т]) -> Са11аЫе[[], Ыз*[Т]] : Комбинирует несколько генераторов в генератор списка. Агдз; депегаТогз: функции-генераторы РеТигпз: генератор, создающий список из результатов всех генеоаторов гетигп ТаггЬаа. [деп() Гог деп бп депегатогз] беТ гереаС.депегаДогГдепегаДог: Са11аЬ1е[[], Т], соипТ: бпТ) -> Са11ао1е[[], ЫзТ[Т]]: Создает генератор списка, повторяя один генератор заданное количесгво раз. геТигп 1аггЬ0а: [депегаТог() Тог _ бп гапде(ооипт)] с!еТ тауЬе_депегабог(депсгаТог: Са11аР1е[[], Т], ргоЬаЫ11Ту: Т1оаТ = 0.5) -> Са11аЫе[ [ ], Т] : Создает генератор, которьй мсжет не генерировать значение (возвоащать Копе).
йеФ тауое_деп(). 1Т гапФот. гапФот() < ргоЬаЫИбу: геГигп дегегаГогО геСигп Попе гетигп тауЬе_деп # Создаем составные генераторы соогд1па1е_депега1:ог- = сопЫпе_депега1огз( гапде_депега1ог(-100. 100), # х координата гапде_депегаРсг(-100. 180) # у координата ) гапс1оп1_'.тогс1_дег.ега1'ог = 1апЬса: ’ ]отп( сЬо1се_депега1ог(11зЦз1г1пд. азс11_1оиегсазе)) () Гог _ 1п гапде(гагФот. гапФ1п1:(3, 8)) ор1:1опа1_исга_депега1:ог = тауЬе_депегаЮг(гапс1от_у/огс1_депега-1:ог, 0 7) # Демонстрируем использование ргхгтЦГСлучайная координата: {соогР1па1:е_депега1:ог()}") рг1пС(Т Случайное слово: {гапдош_тегс1_депсга1:ог()} 1) рг1г11 (Г Опциональное слово: {ор11опа1_«огс1..дегега1ог()}") Функциональные генераторы особенно полезны при создании генераторов для пользовательских типов данных. Мы можем определить структуру генера- ции один раз и затем переиспользовать ее в различных контекстах: Ггош баГасТаззез 1прсг1 с1а1:ас1азз Ггош ГурТпд гпрогС ОрПопа! @ба-Гас1азз с1азз Регзоп: "'Класс дгя представления человека............ папе: зСг аде: тпС сшаП: ОрТ1опа1[з1:г] = Иопе 1з_асТ1уе: Ьоо] = Тгие Фе-*1 регзоп_депегаСог() -> Са11аЫе[[], Регзоп]: Создает генератор объектов Регзоп со случайными, но реалистичными данными. II II II папез = ['АИсе', Воо ', ’ СЬагИе', 'Стана', ' Еуе', 'Ргапк', 'Стасе', 'Непгу ] с1ста1пз = [ ’дтаП.сот’, 'уа!тоо.сот', ’оиПоок.сот', сотрапу.сгд ] пате.деп = спо1се_депега1:ог(патез) аде_деп = гапде_депега1:ог(18, 80) ас1:1уе_деп = Ьоо1еап_депега1:ог сеГ депегаРе_епа11(папе. зГг) -> зГг: .......Генерирует правдоподобный етаП на основе имени.
сота1п = гапс1оп.с1'о1се(с1ота1пз) геГигп Г" {пате. 1о?/ег()}. {гапс1от. гапсИпТ( 1, 999) }@{с!ста1п}" Не! депега1:е_ре-5сп() -> Регзоп пате = пате_деп() аде = аде_деп() етаН = депега1е_ета11(пате) 1Т гапс!от. гапаоп() > 0.2 е1зе Мопе 1з_ас1:17е = ас!1уе_деп() гегигп Регзоп(пате=пате. аде=аде, етаП-етаП, 1з_аст1уе=1з_ асИуе) гебигп депега1:е_рег8оп # Создаем и используем генератор ретзоп_деп = регзоп..депега!ог() 1ез1_реср1е = [регзоп_деп() -Гог _ 1п тапде(5)] Гог регзоп 1п Тез1:_реср1е: рг1п1(Г Сгенерированный человек: {регзоп}") Функциональный подход к генерации данных также позволяет создавать ге- нераторы, которые учитывают взаимосвязи между данными. Например, мы мо- жем создать генератор транзакций, где сумма зависит от типа транзакции: Ггот епот иттрог! Епит Ггот с1аГе1:1те пирог! с1асе!1те, Т1тес1е1тэ с1азз Тгапзас1:1.опТуре(Епит): 0ЕР081Т = "дерозИ 1 И1ТН0кАИАЬ = "ипЛМгаиаГ ТРАМЗЕЕР = "РгапзГег" @Са!ас1азз с!азз Тгапзас!1оп: Сгапзас1:1эп_1с1: з!г 1гапзас1:1оп_1:уре: ТгагзасМопТуре атоип!: Г1оа! Итезсатэ: оагеИте скзсгГрИоп: з!г бе! 1гапзас1:1оп_депега1:ог() -> Са11ао1е[ [ ], ТгапзасНоп] : Создает генератор транзакций с логически связанными данными. !уре_деп = сИо1се_депегаГог(11з1:(Тга11яас1:1опТуре)) Не! атоил1:_Гог_1:уре(1:гап5_1:уре: ТгапзасРхсгТуре) -> Пса!: .......Генерирует сумму в зависимости от типа транзакции." 1! 1:гапз_1:уре == Тгапзас!1опТуре.0ЕРО81Т: геРигп гоипсЦгапс1от.ипИ'ОГ'Т|(50.0, 5000.0), 2) е11Г Ргапз^Руре == ТгапзасИопТуре.'ЛТТНЭРАЙАЕ: геРигп гоапсН г а пс!от. и п1'Го г гп (1 я. 0, 1000.0), 2) е!зе: # ТРАМЗ-ЕР
гебигп гоипс1(гапс1оп1.ип1Гогп1(1 .0, 10090.0), 2) СеГ с1езсг1р1:1оп_'Гог_1:уре(1:гап5_1:уре: ТгапзасСюпТуре, атоип!: Т1оа1:) -> з1г: ...Создает описание транзакции ...... 1Т Тгаг8_Туре == Тгаг.засГхспТуре.ОЕРСЗТТ; гебигп Т"0ероз1! о+ ${атоигт1:.2Г}1 еИТ !гапз_1уре == ТгапеасНопТуре .ЙТТНРКА'ЛА!..: геГигп Г'АТМ жИ±дга\*а1 о! 8{атоип!:,2Т}" е1зе гебигп Г'Тгапз/ег оТ ${атоип!:,2Г}1 Йе! депега1е_1гапзас1]оп() -> ТгапзасИоп: Ггапз_!уре = !уре_деп() эггоип! = апоир1_!ог_1:уре(1:гап8_!уре) ШтезСатр = баГеПте.по«() - Г1тес1е11а( Йауз=гапс1от. гапсИп1(0, 30), Ноигз=гапйот. гапа1п1:(0, 23), П11пи!ез=гапс1ог1. гапсНпГ(0, 59) ) 1гапзас11оп_1с1 = Г ТХМ{гапйот. гапс!1п1( 100000. 999999)} 1 с1езсг1р!1оп = с1езсг1р11оп_Гог_(:уре( 1гап8_1уре, апоип!) геГигп Тгапзас1юп( !гэпзас1:1оп_1с1=1:гапзас1:1оп_]с1, 1тэпзас1:1оп_1уре=1:гап8_1уре, атоип!=атоип1, 1:1те81:атр=11тез1:атр| Йезсг1р1:1оп-с1е8сг1р1:1оп ) гебигп депега1:е_1:гапзас1:1оп # Демонстрация генератора с взаимосвязанными данными !гапз_деп = 1гапзас11оп_депега1сг() са1тр1е_1:гапзасС1опз = [1гапз. деп() Гог _ 1п гапде(З)} Гог тгагзасГГоп 1п затр1е_тгэпзас1:1опз : рг!п1:(Т"Транзакиия: {ГгапзасПсп}") Такой подход к генерации данных обеспечивает согласованность и реали- стичность тестовых данных, что особенно важно при тестировании сложных бизкес-правил и интеграций.
Заключение Функциональное программирование на РуГЬ.оп представляет собой мощную парадигму разработки, которая предлагает элегантные решения для множества задач современного программирования В ходе изучения материала этой книги мы познакомились с фундаментальными концепциями, которые способны ра- дикально изменить подход к созданию программного обеспечения. Композиция функций и создание пайплайнов стали одними из самых важ- ных тем нашего изучения. Эти техники демонстрируют, как сложные задачи мо- гут быть разложены на простые переиспользуемые компоненты. Вместо созда- ния монолитных решений мы научились строить гибкие системы из небольших функций, каждая из которых выполняет одну четко определенную операцию. Такой подход не только повышает читаемость и тестируемость кода, но и значи тельно упрощает его поддеожку и развитие. Особую ценность представляет декларативный стиль программирования, ко- торый становится возможным благодаря функциональным пайплайнам. Мы фо- кусируемся на описании тою, что должно быть сделано с данными, а не на том, как именно это выполнить пошагово. Данный подход особенно эффективен при работе с потоками данных и создании систем ЕТЕ, где информация последо- вательно проходит через различные этапы обработки. Мемоизация оказалась незаменимым инструментом оптимизации произво- дительности. Понимание того, когда и как применять кеширование результатов вычислений, позволяет создавать эффективные программы без ущерба для чи- таемости кода. Встроенные возможности Руйгоп, такие как декоратор йИго-сасИе, предоставляют готовые решения для большинства практических задач, а зна- ние принципов создания собственных кеширующих механизмов открывает воз- можности для специализированных случаев использования. Рекурсивные алгоритмы и задачи динамического программирования особен- но выигрывают от применения мемоизации. Мы увидели, как временная слож- ность может снижаться с экспоненциальной до линейной или квадратичной благодаря умному кешированию промежуточных результатов. Это знание осо- бенно важно при работе с алгоритмически сложными задачами в области ма- шинного обучения, оптимизации и анализа данных. Параллельное и конкурентное программирование в функциональном сти- ле демонстрирует естественную синергию между неизменяемостью данных и безопасностью потоков. Отсутствие побочных эффектов в чистых функци- ях устраняет многие традиционные проблемы согсиггепТ ргодгатпипд. Мо- дуль сопсиг тетС.Гипотез предоставляет элегантные решения для распаралле- ливания вычислений, а архитектура Мар-Пебисе показывает путь к созданию масштабируемых систем обработки данных. Практическая значимость изученных концепций выходит далеко за рамки академического интереса. В современном мире разработки программного обе- спечения, где требования к масштабируемости, производительности и надеж- ности постоянно растут, функциональные подходы предлагают проверенные
временем решения. Микросервисная архитектура, системы потоковой обработ- ки данных, реактивное программирование — все эти современные тенденции имеют глубокие корни в функциональной парадигме. Важно отметить, что функциональное программирование ие является «сере- бряной пулей», которая решает все проблемы разработки. Скорее это мощный инструмент в арсенале разработчика который особенно эффективен в опреде- ленных контекстах. Умение распознавать ситуации, где функциональные под- ходы дают максимальную пользу, приходит с практикой и опытом. Изученные техники прекрасно интегрируются с другими парадигмами про граммирования. Руйюп как мультипарадигменный язык позволяет гармонично сочетать функциональные, объектно-ориентированные и процедурные подхо- ды в рамках одного проекта. Это дает разработчикам свободу выбирать наиболее подходящий инструмент для каждой конкретной задачи. Путешествие в мир функционального ирозраммирования на Руйтсп откры- вает двери в более глубокое понимание компьют ерных наук и алгоритмов. Мно- гие идеи, изученные в этой книге, применимы не только в Руйюп, но и в других языках программирования. Это делает полученные знания ценной инвестици- ей в профессиональное развитие. Для дальнейшего роста рекомендуется продолжить изучение специализиро- ванных библиотек и фреймворков, которые расширяют функциональные воз- можности Руйюп. Библиотеки для обработки больших данных, такие как йазк или РуБрагк, инструменты для реактивного программирования, системы пото- ковой обработки — все они основаны на принципах, заложенных в этой книге. Функциональное программирование — это не просто набор техник, а способ мышления о решении проблем Оно учит нас декомпозировать сложные задачи, думать о трансформациях данных и создавать переиспользуемые компоненты. Эти навыки остаются ценными независимо от конкретных технологий и языков программирования, с которыми мы работаем.
Справочное издание Анык,тамалык, басылым Серия «Программирование. Оффер на миллион» Дэвид Мерц Лекс Клаймен ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ НА РУТНОЫ Ответственный за выпуск: И. Резько О । ветст венный редактор' А. Ходякоеа Оформление обложки: С Дарсич Подписано в печать 10.10.2025. Формат 70x1001/16. Бумага офсетная. Печать офсетная. Гаонитура ЫоТо 5ег1ГЗет1Сопаеп5ес1 Усл. печ. л. 16,4 Тираж 1500 экз. Заказ \!е Общероссийский классификатор продукции ОК-034-2014 (КПЕС 2008); 58 11 1 — книги, брошюры печатные. Изготовлено з 2026 г Произведено в Российской Федерации Изготовитель: ООО «Издательство АСТ». 129085, Российская Федерация, г. Москва, Звездный бульвар, дом 21, строение 1, комната 705 пом. I, 7 этаж. Адрес места осуществление деятельности по изготовлению продукции: 123112, Российская Федерация, г. Москва, Пресненская набережная, д. 6, стр. 2, Деловой комплекс «Империя», 14,15 этаж. Наш электронный адрес. а5к@аь1.1и Наш сайт: имм.азТги Интернет-магазин: \лллм/.Ьсок24.ги Онц|рупл: «Издательство АСТ» ЖШ1^ 129085, Ресей Федерациясы, Мэскеу, Звёздный бульвары, 21-уй, 1-^урылыс, 705-белме, I уй-жай, 7-кабат. 0Н1М енд|ру к,ызмет1н жузеге асыру мекенжайы: 123112, Ресей Федерациясы, Маскеу, Пресненская жат., б-уй. 2-к,ур , «Империя» !скерл1к кешен1, 14, 15щабат. Б(зд1ц электрондык мекенжаймыз: туг/.азСги Е-таП: а5к@а5С.ги Интеонет-магазин: т™/.Ьоок24 ги Интернет-дукен: илллл/.Ьоок24.кг Импортер в Республику Казахстан, дистрибьютор и представитель по приёму претензий на продукцию в Республике Казахстан: ТОО «РДЦ-Алматы» г. Алматы, ул. Домбровского, 3«а», литер Б офис 1. ^азакдтан Республикасына импорттаушы дистрибьютор жэне К,аза^стан Республикасында еымге шатымдар к,абылдау женндеп еюл: «РДЦ-Алматы» ЖШС. Алматы к,., Домбровский кеш., 3«а». Б литер!, офис 1. Тел.: 8(727) 2 51 59 90,91 факс: 8 (727) 251 59 92 !шю 107; Е-та||; КОС-А1та1:у@ек5то.к2, \л/и/от.Ьоок24.к2 6нд|р!лген жылы. 2025 Он1мн|ц жарамдылык, мерз!м! шектелмеген. Сертификаттаута жат лайды. Ресей Федерациясында енд|р!лген.
Пишете на РуЙюп, но чувствуете, что ваш код можно сделать чище и выразительнее? Хотите мыслить как функциональный программист и строить программы, где все логично, прозрачно и предсказуемо? Эта книга покажет, как перейти от императив- ного стиля к функциональному мышлению — шаг за шагом, на понятных примерах и с применением реальных инструментов. От замыканий до декораторов, ог ленивых вычислений до асинхронных пайплайнов — откройте для себя функциональное про- граммирование на РуШоп! Основные преимущества данного издания: • От основ до продвинутых техник — от простых функций и итераторов до компо- зиции, мемоизации и асинхронных пайплайнов. • Практика вместо теории — примеры из реальных задач, разбор типичных оши- бок и готовые шаблоны функционального кода. • Современный РуТЬоп под новым углом — работа с йшсЮок, йеНоо18, орегайтг, азупсю, Рубаппс и другими ключевыми инструментами. • Фокус на чистом и читаемом коде — меньше побочных эффектов, больше логики и предсказуемости. Ключевые темы: • Принципы функционального программирования и их реализация в РуШоп. • Работа с функциями высшего порядка, замыканиями и декораторами. • Ленивые вычисления, композиции фушкций и построение пайплайнов обработки данных. • Асинхронное и параллельное программирование в функциональном стиле. • Мемоизация, кеширование и оптимизация производительности. • Функциональные подходы к валидации и тестированию данных. Для кого эта книга: • Для начинающих разработчиков, стремящихся понять логику функционального стиля и научиться писать «чистый» код. • Для опытных Ругйоп-разработчиков, желающих расширить арсенал и использо- вать все возможности языка. • Для инженеров и аналитиков, занимающихся обработкой данных и мечтающих писать компактные, устойчивые и легко тестируемые решения. 978-5-17-178630-4 КНИГИ ДЛЯ ЛЮБОГО НАСТРОЕНИЯ ЗДЕСЬ: уууууу.азГги / уу\длл/.Ьоок24.ги □ ук.сот/|2с!а1е151уоа51 0 ок.ги/|2с!а(е151уоа51 Перестаньте «бороться» с громоздким кодом — начните мыслить функционально и писать код на РуШоп, работающий четко, красиво и эффективно! ИЗДАТЕЛЬСКАЯ ГРУППА АСТ