ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 03.12.2023
Просмотров: 375
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Функциональное программирование
209
Функции высшего порядка
Функции высшего порядка (higher-order functions) могут получать другие функции в аргументах или использовать функции как возвращаемые значения. Например, определим функцию с именем callItTwice()
, которая вызывает заданную функ- цию дважды:
>>> def callItTwice(func, *args, **kwargs):
... func(*args, **kwargs)
... func(*args, **kwargs)
>>> callItTwice(print, 'Hello, world!')
Hello, world!
Hello, world!
Функция callItTwice()
работает с любой передаваемой функцией. В Python функ- ции являются первоклассными объектами (first-class objects); это означает, что они ничем не отличаются от других объектов: функции можно сохранять в переменных, передавать в аргументах или использовать как возвращаемые значения.
Лямбда-функции
Лямбда-функции (lambda functions), также называемые анонимными (anonymous) или безымянными (nameless) функциями, представляют собой упрощенные функ- ции, у которых нет имен, а код состоит из одной команды return
. Лямбда-функции часто используются для передачи функций как аргументов других функций.
Например, можно создать обычную функцию, которая получает список с шириной и высотой прямоугольника 4
× 10:
>>> def rectanglePerimeter(rect):
... return (rect[0] * 2) + (rect[1] * 2)
>>> myRectangle = [4, 10]
>>> rectanglePerimeter(myRectangle)
28
Эквивалентная лямбда-функция выглядит так:
lambda rect: (rect[0] * 2) + (rect[1] * 2)
Чтобы определить лямбда-функцию на Python, укажите ключевое слово lambda
, за которым следуют: список параметров (если они есть), разделенных запятыми, двоеточие и выражение, которое действует как возвращаемое значение. Так как функции являются первоклассными объектами, лямбда-функцию можно присвоить переменной, фактически повторяя то, что делает команда def
:
210
Глава 10.Написание эффективных функций
>>> rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2)
>>> rectanglePerimeter([4, 10])
28
Лямбда-функция присваивается переменной с именем rectanglePerimeter
, тем самым фактически создается функция rectanglePerimeter()
. Как видите, функ- ции, созданные командами lambda
, ничем не отличаются от функций, созданных командами def
1 ... 17 18 19 20 21 22 23 24 ... 40
ПРИМЕЧАНИЕ
В реальном коде лучше использовать команды def, вместо того чтобы присваивать лямб- да-функции неизменяемым переменным. Лямбда-функции специально создавались для ситуаций, в которых функции не нуждаются в имени.
Синтаксис лямбда-функций хорошо подходит для определения небольших функ- ций, которые служат аргументами для вызова других функций. Например, у функ- ции sorted()
есть ключевой аргумент key
, который позволяет задать функцию.
Вместо того чтобы сортировать элементы списка на основании значения элементов, она сортирует в зависимости от возвращаемого значения функции. В следующем примере sorted()
передается лямбда-функция, которая возвращает периметр заданного прямоугольника. В результате функция sorted()
сортирует по вычис- ленному периметру из списка
[width,
height]
, а не непосредственно по списку
[width,
height]
:
>>> rects = [[10, 2], [3, 6], [2, 4], [3, 9], [10, 7], [9, 9]]
>>> sorted(rects, key=lambda rect: (rect[0] * 2) + (rect[1] * 2))
[[2, 4], [3, 6], [10, 2], [3, 9], [10, 7], [9, 9]]
Вместо того чтобы сортировать, например, значения
[10,
2]
или
[3,
6]
, функция теперь выполняет сортировку на основании возвращаемых значений периметров
24
и
18
. Лямбда-функции являются удобным синтаксическим сокращением: вы можете задать одну маленькую лямбда-функцию вместо определения новой именованной функции командой def
Отображение и фильтрация со списковыми включениями
В более ранних версиях Python функции map()
и filter()
были обычными функци- ями высшего порядка, которые могли преобразовывать и фильтровать списки, часто при помощи лямбда-функций. Отображение способно строить списки значений на основании значений из другого списка. Фильтрация позволяет создать список, который содержит только те значения из другого списка, которые соответствуют некоторому критерию.
Функциональное программирование
211
Например, если вы хотите создать новый список, содержащий строки вместо це- лых чисел
[8,
16,
18,
19,
12,
1,
6,
7]
, можно передать функции map()
этот список и лямбда-функцию lambda n:
str(n)
:
>>> mapObj = map(lambda n: str(n), [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(mapObj)
['8', '16', '18', '19', '12', '1', '6', '7']
Функция map()
возвращает объект map
, который можно получить в форме списка, для чего он передается функции list()
. Отображенный список теперь содержит строковые значения на основании целых значений из исходного списка. Функ- ция filter()
работает аналогично, но в этом случае аргумент лямбда-функции определяет, какие элементы должны остаться в списке (если лямбда-функция возвращает
True
) или быть отфильтрованными (если она возвращает
False
).
Например, передача функции lambda n:
n
%
2
==
0
позволяет отфильтровать все нечетные числа:
>>> filterObj = filter(lambda n: n % 2 == 0, [8, 16, 18, 19, 12, 1, 6, 7])
>>> list(filterObj)
[8, 16, 18, 12, 6]
Функция filter()
возвращает объект-фильтр, который можно снова передать функции list()
. В отфильтрованном списке остаются только четные числа.
Однако создание отображенных или отфильтрованных списков функциями map()
и filter()
в Python считается устаревшим. Вместо этого рекомендуется создавать их при помощи списковых включений. Списковые включения не только освобожда- ют вас от необходимости писать лямбда-функции, но и работают быстрее функций map()
и filter()
Следующий код воспроизводит пример с функцией map(
) с использованием спи- скового включения:
>>> [str(n) for n in [8, 16, 18, 19, 12, 1, 6, 7]]
['8', '16', '18', '19', '12', '1', '6', '7']
Обратите внимание: часть спискового включения str(n)
похожа на lambda n: str(n)
А следующий фрагмент воспроизводит пример с функцией filter()
с использо- ванием спискового включения:
>>> [n for n in [8, 16, 18, 19, 12, 1, 6, 7] if n % 2 == 0]
[8, 16, 18, 12, 6]
Обратите внимание: часть спискового включения if n
%
2
==
0
похожа на lambda n:
n
%
2
==
0
212
Глава 10.Написание эффективных функций
Во многих языках существует концепция функций как первоклассных объектов, что делает возможным существование функций высшего порядка, включая функции отображения и фильтрации.
Возвращаемые значения всегда должны иметь
один тип данных
Python является языком с динамической типизацией; это означает, что функции и методы Python способны возвращать значения любого типа данных. Но чтобы ваши функции были более предсказуемыми, вы должны стремиться к тому, чтобы они возвращали значения только одного типа данных.
Например, следующая функция в зависимости от случайного числа возвращает целое число или строковое значение:
>>> import random
>>> def returnsTwoTypes():
... if random.randint(1, 2) == 1:
... return 42
... else:
... return 'forty two'
Когда вы пишете код с вызовом этой функции, легко забыть, что вам нужно обра- батывать разные типы данных. Продолжим этот пример: допустим, что вы вызвали returnsTwoTypes()
и хотите преобразовать возвращенное число в шестнадцатерич- ную форму:
>>> hexNum = hex(returnsTwoTypes())
>>> hexNum
'0x2a'
Встроенная функция Python hex()
возвращает строку с шестнадцатеричным представлением переданного ей числа. Этот код работает при условии, что returnsTwoTypes()
вернет целое число; возникает впечатление, что этот код сво- боден от ошибок. Но когда returnsTwoTypes()
возвращает строку, выдается ис- ключение:
>>> hexNum = hex(returnsTwoTypes())
Traceback (most recent call last):
File "
TypeError: 'str' object cannot be interpreted as an integer
(Объект 'str'
не может быть интерпретирован как integer
.)
Конечно, вы постоянно должны помнить о необходимости обрабатывать все воз- можные типы данных возвращаемого значения. Но в реальности об этом легко
Возвращаемые значения всегда должны иметь один тип данных
213
забыть. Чтобы предотвратить такие ошибки, всегда стремитесь к тому, чтобы ваши функции возвращали значения только одного типа данных. Это не является жест- ким требованием, и иногда просто невозможно предотвратить возвращение функ- цией значений разных типов данных. Но чем ближе вы подходите к возвращению только одного типа, тем проще и надежнее будут ваши функции.
Есть один случай, на который необходимо обратить особое внимание: не возвра- щайте
None из функций (единственное исключение — если ваша функция всегда возвращает
None
). Значение
None
— единственное значение типа данных
NoneType
Появляется искушение написать функцию, которая возвращает
None
, сообщая тем самым о возникшей ошибке (эта практика рассматривается в следующем разделе «Выдача исключений и возвращение кодов ошибок»), но возвращение
None лучше зарезервировать для функций, у которых возвращаемое значение не имеет смысла.
Дело в том, что возвращение
None как признака ошибки становится распространен- ным источником неперехваченных исключений 'NoneType'
object has no attribute
(Объект 'NoneType
' не имеет атрибута…):
>>> import random
>>> def sometimesReturnsNone():
... if random.randint(1, 2) == 1:
... return 'Hello!'
... else:
... return None
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
'HELLO!'
>>> returnVal = sometimesReturnsNone()
>>> returnVal.upper()
Traceback (most recent call last):
File "
AttributeError: 'NoneType' object has no attribute 'upper'
Сообщение об ошибке выглядит довольно туманно. Вероятно, вы не сразу отследите его происхождение до функции, которая обычно возвращает ожидаемый результат, но также может вернуть
None при возникновении ошибки. Проблема возникла из-за того, что sometimesReturnsNone()
возвращает значение
None
, которое затем присваивается переменной returnVal
. Но сообщение об ошибке заставляет думать, что проблема возникла при вызове метода upper()
В своем докладе на конференции в 2009 году компьютерный теоретик Тони Хоар
(Tony Hoare) извинился за то, что он изобрел null
-ссылку (общий аналог значения
None в Python) в 1965 году. Он сказал: «Я называю это своей ошибкой на милли- ард долларов. <…> Я не устоял перед искушением включить null
-ссылки просто
214
Глава 10.Написание эффективных функций потому, что их было так легко реализовать. Это привело к неисчислимым ошибкам, уязвимостям и системным сбоям, которые за последние 40 лет, вероятно, создали проблемы и неприятности на миллиард долларов». Полное выступление можно просмотреть на https://autbor.com/billiondollarmistake.
Выдача исключений и возвращение
кодов ошибок
В Python термины «исключение» (exception) и «ошибка» (error) имеют прибли- зительно одинаковый смысл: аномальная ситуация в программе, которая обычно указывает на возникшую проблему. Исключения стали популярным языковым механизмом в 1980-е и 1990-е годы в C++ и Java. Ими заменили коды ошибок (error codes) — значения, возвращаемые функциями для обозначения проблемы. Пре- имущество исключений состоит в том, что возвращаемые значения связаны только с предназначением функции и не указывают на присутствие ошибки.
Коды ошибок также способны создавать проблемы в ваших программах. Напри- мер, метод строк Python find()
обычно возвращает индекс, по которому была найдена подстрока, а если найти подстроку не удалось, возвращается код ошиб- ки
-1
. Но поскольку индекс
-1
также может использоваться для отсчета индекса от конца строки, случайное использование
-1
в качестве кода ошибки создаст ошибку. Чтобы понять, как это происходит, введите следующий фрагмент в ин- терактивной оболочке:
>>> print('Letters after b in "Albert":', 'Albert'['Albert'.find('b') + 1:])
Letters after b in "Albert": ert
>>> print('Letters after x in "Albert":', 'Albert'['Albert'.find('x') + 1:])
Letters after x in "Albert": Albert
Часть кода 'Albert'.find('x')
при вычислении возвращает код ошибки
-1
В результате выражение 'Albert'['Albert'.find('x')
+
1:]
преобразуется в 'Albert'[-1
+
1:]
, что далее дает результат 'Albert'[0:]
и, наконец,
'Albert'
Очевидно, это не то поведение, которое ожидалось от кода. При вызове index()
вместо find()
, как в 'Albert'['Albert'.index('x')
+
1:]
, возникло бы исключение.
Проблема становится очевидной, и проигнорировать ее не удастся.
С другой стороны, метод строк index()
выдает исключение
ValueError
, если он не может найти подстроку. Если исключение не будет обработано, оно приведет к аварийному завершению программы — обычно это лучше, чем ошибка, которая осталась незамеченной.
Имена классов исключений часто завершаются словом
Error
, когда исключение ука- зывает на фактическую ошибку — такую как
ValueError
,
NameError или
SyntaxError
Итоги
215
К категории классов исключений, представляющих аномальные ситуации, которые не обязательно являются ошибками, относятся
StopIteration
,
KeyboardInterrupt и
SystemExit
Итоги
Функции предоставляют популярный механизм группировки кода наших про- грамм; при их написании необходимо принимать ряд решений: какое имя им при- своить, какой размер они должны иметь, сколько у них должно быть параметров и сколько аргументов должно передаваться для этих параметров. Синтаксисы
*
и
**
в командах def позволяют функциям получать переменное количество параметров; такие функции называются вариадическими.
Хотя Python не является языком функционального программирования, в нем реализованы многие возможности, используемые в языках функционального про- граммирования. Функции являются первоклассными объектами; это означает, что их можно сохранять в переменных и передавать как аргументы других функций
(которые в этом случае называются функциями высшего порядка). Лямбда-функ- ции предоставляют короткий синтаксис для определения анонимных функций как аргументов функций высшего порядка. Самые распространенные функции высшего порядка в Python — map()
и filter()
, хотя предоставляемая ими функциональность быстрее реализуется списковыми включениями.
Возвращаемые значения функций всегда должны иметь постоянный тип данных.
Возвращаемые значения не должны использоваться как коды ошибок; для этой цели следует использовать исключения. В частности, значение
None часто определяется как код ошибки.
11
Комментарии, doc-строки
и аннотации типов
Комментарии и документация в исходном коде не менее важны, чем сам код. Причина в том, что программный про- дукт никогда не бывает полностью готовым; всегда прихо- дится вносить в него изменения — как при добавлении новых возможностей, так и при исправлении ошибок. Как однажды заметили специалисты по теории вычислений Гарольд Абельсон
(Harold Abelson), Джеральд Джей Зюссман (Gerald Jay Sussman) и Джулия Зюсс- ман (Julie Sussman), «программы пишутся для того, чтобы их читали люди, и лишь изредка для того, чтобы они выполнялись машинами».
Комментарии, doc-строки и аннотации типов помогают поддерживать код в работо- способном состоянии. Комментарии представляют собой короткие объяснения на естественном языке, которые записываются прямо в исходном коде; компьютер их игнорирует. Комментарии содержат полезные заметки, предупреждения и напоми- нания для сторонних читателей кода, а иногда и для самого автора кода в будущем.
Почти каждому программисту доводилось задавать себе вопрос: «Кто написал это нечитаемое месиво?», только чтобы вспомнить ответ: «Это я».
Doc-строки представляют собой форму документирования функций, методов и модулей, специфическую для Python. Когда вы задаете комментарии в формате doc-строки, автоматизированные средства (такие как генераторы документации или встроенный модуль Python help()
) позволяют разработчикам легко найти информацию о вашем коде.
Аннотации типов (type hints) представляют собой директивы, которые можно до- бавить в исходный код Python для указания типов данных переменных, параметров и возвращаемых значений. Это позволяет средствам статического анализа кода
Комментарии
217
убедиться в том, что ваш код не сгенерирует исключений, обусловленных непра- вильными типами значений. Аннотации типов впервые появились в Python 3.5, но так как они создаются на основе комментариев, их можно использовать в любой версии Python.
Итак, в этой главе я расскажу о трех упомянутых способах встраивания докумен- тации в код с целью улучшения его читабельности. Внешняя документация — ру- ководства пользователя, электронные учебники и справочные материалы — важна, но в этой книге она не рассматривается. Если вы захотите узнать больше о внешней документации, поищите информацию о генераторе документации Sphinx (https://
www.sphinx-doc.org/).
Комментарии
Как и большинство языков программирования, Python поддерживает одностроч- ные и многострочные комментарии. Любой текст, заключенный между знаком
#
и концом строки, является однострочным комментарием. Хотя в Python нет спе- циального синтаксиса для многострочных комментариев, в этом качестве можно использовать многострочный текст в тройных кавычках. В конце концов, строковое значение само по себе еще не заставляет интерпретатор Python что-либо сделать.
Рассмотрим пример:
# Это однострочный комментарий.
"""А это многострочный текст, который также работает как многострочный комментарий. """
Если комментарий занимает несколько строк, лучше использовать один много- строчный блок, чем несколько последовательных однострочных комментариев.
Второй вариант хуже читается, как видно из следующего примера:
"""Хороший способ записи комментариев,
занимающих несколько строк. """
# А это плохой способ
# записи комментариев,
# занимающих несколько строк.
Комментарии и документацию программисты зачастую включают в код задним числом, а некоторые даже считают, что от них больше вреда, чем пользы. Но как я уже объяснял в подразделе «Миф: комментарии излишни» на с. 111, коммен- тарии абсолютно обязательны, если вы хотите писать профессиональный, удо- бочитаемый код. В этом разделе вы научитесь писать полезные комментарии, которые предоставляют дельную информацию читателю без ущерба для удобо- читаемости программы.