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

Прежде чем я начну, мне нужно убедиться, что вы помните, что такое итерируемый объект, так как это один из тех терминов, который иногда заставляет людей остекленеть: время, которое затем позволяет циклу for или функции карты обрабатывать каждое из них по отдельности. Типичными примерами итерируемых объектов являются списки, строки, словари, кортежи и наборы. Что такое *не* итерации? Ответ: целые числа, числа с плавающей запятой, логические значения и комплексные числа.

Встроенная в Python функция map() заинтриговала меня с тех пор, как я впервые узнал о ней на базовом курсе. Что делает map(), так это берет список или другой итерируемый объект, выполняет операцию над каждым членом списка и затем возвращает результат. И он делает это, используя только функцию map(), поэтому цикл for не требуется. Я искал программу, для которой требовалась бы функция карты, и нашел ту, которая показалась мне идеальной.

Достаточно простой задачей было написать программу на Python, которая транспонировала бы песню — в данном случае список музыкальных нот — вверх или вниз по гамме. Почему вы хотите это сделать? Ну, еще в колледже я играл поющего кузена Амьена¹ в шекспировской «Как вам это понравится», и режиссер дал мне мелодии собственного сочинения, которыми он очень гордился. Проблема была в том, что мелодии были написаны для кого-то с солидным сопрано — если не квазиоперным диапазоном. Мне? Я альт, низкий альт. В том, что он написал, было всего несколько нот, которые я мог бы спеть.

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

В ночь нашего первого спектакля меня представили лютнисту («Разве это не чудесный сюрприз?» — сказал мне режиссер), и я испытал момент полнейшего ужаса. К счастью, мастер оказался настоящим профессионалом, и за короткое время мы соединили оригинальную музыку с моей транспозицией в более низком диапазоне, и все было готово. Шоу должно продолжаться!

Итак, вернемся к делу. Эта программа на Python должна была взять песню, вроде хита из топ-40 «Дуй, дуй, зимний ветер», написанную в типографской нотной записи (а не в виде нот). Начинается так: ["C", "Bb", "A", "G", "F", "D", "F", "E"].

Как работают западные музыкальные гаммы, в каждой октаве 12 нот: C, C#/Db, D, D#/Eb, E, F, F#/Gb, G, G#/Ab, A, A#/Bb, B. Когда мы транспонировать музыку, мы сдвигаем всю музыкальную композицию вверх или вниз на определенное количество шагов. Если бы мы поднимали предыдущую музыкальную строку на три ступени или тона вверх, мы бы получили: [D#, C#, C, A#, G#, F, G#, G]. (Это несколько упрощенное объяснение транспонирования. Некоторые инструменты натянуты и настроены в определенной тональности, и транспонирование музыки для оркестра, состоящего из разных инструментов, в разных тональностях может привести к другим соображениям.)

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

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

```

def adjust(note: list, interval: int):

    """Adjusts the note by the interval given."""
    
    scale = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E',
             'F', 'F#', 'G', 'G#', ]

    # determine the index of the note in the scale
    pointer = scale.index(note)

    # determine the new note
    new_note = scale[pointer + interval]

    return new_note
```

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

Вместо этого я получил это сообщение об ошибке:

```
 TypeError: 'int' object is not iterable
```

Ждать? Что? Моя песня была в виде правильно сформированного списка, который является правильной итерацией. Как это могло произойти? Глядя на свой код, я понял, что именно так я вызывал функцию map():

```
  ans = map(adjust, fixed_song, interval)

  new_song = list(ans)

  return new_song
```

Да, моя fixed_song была списком и, следовательно, итерируемой, но интервал транспонирования, который я включил, был выражен целым числом. Значит, это была проблема! Но ждать. Как я мог вызвать функцию настройки, не сообщая ей информацию о том, насколько песня должна быть транспонирована вверх или вниз? Я не мог просто запрограммировать интервал транспонирования в код. Это не сработает.

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

Я попытался поместить свой интервал в кортеж из одного, (interval,), и программа работала нормально, но транспонировала только первую ноту.

Я был уверен, что другие люди должны были столкнуться с этой проблемой. Итак, сначала я начал читать свои книги по Python, которых у меня немало. Базовые книги содержали одно и то же ограниченное объяснение, а также менее чем полезный пример вычисления квадратов целых чисел или преобразования целых чисел в строки. В нескольких книгах говорилось, что функцию map() можно использовать только с итерируемыми объектами в качестве аргументов, что предполагает, что функция map() не может использоваться для моей программы транспонирования. Хм…

Затем я открыл свои заметки о занятиях и курсах со всех пройденных мною курсов по Python, но и там не нашел ничего полезного.

Так что я оказался там, где приземляется каждый отчаявшийся молодой программист, на Переполнении стека. Каким бы разочаровывающим иногда ни был поиск в Stack Overflow, на этот раз я сорвал джекпот.

Чуть больше года назад другой программист столкнулся с тем же ограничением map(), что и я, и у человека с именем пользователя trincot был ответ. Вы можете использовать каррирование, например, с partial из functools, — сказал он.

Карри? частично? Я слышал о functools, но я никогда не слышал об этих вещах. Его версия будет работать в моем коде так:

```

from functools import partial

ans = map(partial(adjust, interval=interval), fixed_song)

```

Я попробовал, и, к моему удовольствию и удивлению, это сработало!

Но как? Что это за каррирование и этот парциал?

По данным GeeksforGeeks:

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

Хорошо, часть грамматики оставляла желать лучшего, но идея, похоже, заключается в том, что каррирование — это способ обойти ограничение map только на итерации, вложив неинтерактивный объект (в данном случае интервал) внутри функции функции карты (adjust, в этом случае.)

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

Я копнул глубже, ища какое-нибудь объяснение того, как partial() творит чудеса. Согласно документации Python, partial() будет «возвращать новый частичный объект, который при вызове будет вести себя как func, вызванный с позиционными аргументами args и ключевыми словами arguments».

Если я понимаю, что я читаю, это звучит так, как если бы partial вернул свой частичный объект, который затем действовал как функция, которую могла бы использовать карта. Только этот неполный объект включал (и в некотором смысле скрывал) переменную интервала, необходимую мне для того, чтобы все это заработало.

Это очень ловкий трюк, который я зарегистрировал на случай, если снова столкнусь с подобной ситуацией. Тем не менее, я задавался вопросом, возможно, используя functools.partial(), я искажал map() во что-то, чем она не должна была быть. Действительно ли map() была лучшей функцией для этой программы? Могло ли какое-то понимание списка или выражение генератора сделать работу лучше?

Во-первых, я переписал свою функцию транспонирования как понимание списка:

```

ans = [adjust(note, interval) for note in fixed_song]

return ans

```

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

Я также мог бы переписать функцию транспонирования с помощью Genexpr, например:

```

ans = (adjust(note, interval) for note in fixed_song)

new_song = list(ans)

return new_song

```

Все три решения работают. Есть ли способ оценить, какое из них является лучшим решением?

Я запустил tracemalloc для каждой версии программы и обнаружил, что среднее выделение памяти для версии listcomp было почти в три раза выше, чем для map() или Genexpr. (Средний размер для listcomp составил 152 байта, а для двух других — всего 56 байт). Теперь, по общему признанию, это маленькая программа, и я сомневаюсь, что разница здесь кого-то волнует. Но что, если набор данных был намного больше? В этом случае Genexpr или map() могут быть более разумным выбором.

[1]: Те из вас, кто действительно знает своего Шекспира, возможно, помнят, что кузен Амьен играл мужскую роль. Я училась в женском колледже, и мы ставили «Как вам это понравится» с почти полностью женским составом. Таково было начало 70-х в католической школе для девочек.

Рекомендации

Iterables, Python, как вы это понимаете

функция map(), Встроенные функции, Стандартная библиотека Python, 3.11.4 Документация

Python’s map(): обработка итерируемых объектов без цикла, Леоданис Посо Рамос, 30 сентября 2020 г., Real Python

Music Transposition: The Ultimate Guide, Кейт Бруноттс, блог eMastered

Как вам это понравится Уильяма Шекспира, шекспировская библиотека Фолгера.

"Переполнение стека"

functools, Модули функционального программирования, Стандартная библиотека Python, 3.11.4 Документация

Когда использовать понимание списка в Python, Джеймс Тимминс, 6 ноября 2019 г., Real Python

Функция map() против выражений генератора, Дон Бадер, Real Python.

tracemalloc, Отладка и профилирование, Стандартная библиотека Python, 3.11.4 Документация

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .