Функции как аргументы
Мы уже знакомы с методом sort и функцией sorted, которые используются для сортировки списков в их естественном порядке. Для чисел и строк это обычно работает вполне хорошо. Однако для чего-то более сложного то, что Python считает естественным порядком элементов, не всегда совпадает с тем, что мы, программисты, имели в виду.
Например, список кортежей по умолчанию сортируется на основе первого элемента каждого кортежа:
products = [("banana", 5.95), ("apple", 3.95), ("orange", 4.50), ("watermelon", 4.95)]
products.sort()
for product in products:
print(product)('apple', 3.95) ('banana', 5.95) ('orange', 4.5) ('watermelon', 4.95)
Но что, если мы хотим отсортировать список по цене?
Функции как аргументы
Метод или функция сортировки обычно принимает необязательный второй аргумент, который позволяет обойти стандартные критерии сортировки. Этот второй аргумент является функцией, которая определяет, как вычисляется значение каждого элемента списка. Когда список сортируется, Python вызывает эту функцию при сравнении элементов друг с другом.
Рассмотрим пример:
def order_by_price(item: tuple):
# Return the price, which is the second item within the tuple
return item[1]
if __name__ == "__main__":
products = [("banana", 5.95), ("apple", 3.95), ("orange", 4.50), ("watermelon", 4.95)]
# Use the function order_by_price for sorting
products.sort(key=order_by_price)
for product in products:
print(product)('apple', 3.95) ('orange', 4.5) ('watermelon', 4.95) ('banana', 5.95)
Теперь список отсортирован по ценам товаров, но что на самом деле происходит в программе?
Функция order_by_price на самом деле довольно проста. О на принимает один элемент в качестве аргумента и возвращает значение для этого элемента. Точнее, она возвращает второй элемент кортежа, который представляет цену. Но затем у нас есть такая строка кода, где вызывается метод sort:
products.sort(key=order_by_price)
Здесь метод sort вызывается с функцией в качестве аргумента. Это не ссылка на возвращаемое значение функции, а ссылка на саму функцию. Метод sort вызывает эту функцию несколько раз, по очереди используя каждый элемент списка как аргумент.
Если добавить дополнительный оператор вывода в определение функции order_by_price, можно убедиться, что функция действительно вызывается по одному разу для каждого элемента списка:
def order_by_price(item: tuple):
# Print the item
print(f"Function call: order_by_price({item})")
# Return the price, which is the second item within the tuple
return item[1]
products = [("banana", 5.95), ("apple", 3.95), ("orange", 4.50), ("watermelon", 4.95)]
# Use the function order_by_price for sorting
products.sort(key=order_by_price)
for product in products:
print(product)Function call: order_by_price(('banana', 5.95)) Function call: order_by_price(('apple', 3.95)) Function call: order_by_price(('orange', 4.5)) Function call: order_by_price(('watermelon', 4.95)) ('apple', 3.95) ('orange', 4.5) ('watermelon', 4.95) ('banana', 5.95)
Порядок можно развернуть с помощью еще одного именованного аргумента: reverse, который доступен и для метода sort, и для функции sorted:
products.sort(key=order_by_price, reverse=True)
t2 = sorted(products, key=order_by_price, reverse=True)Определение функции внутри определения функции
Мы также могли бы добавить именованную функцию для созданной нами новой сортировки по цене. Добавим функцию с именем sort_by_price:
def order_by_price(item: tuple):
return item[1]
def sort_by_price(items: list):
# use the order_by_price function here
return sorted(items, key=order_by_price)
products = [("banana", 5.95), ("apple", 3.95), ("orange", 4.50), ("watermelon", 4.95)]
for product in sort_by_price(products):
print(product)Если мы знаем, что вспомогательная функция order_by_price нигде не используется за пределами функции sort_by_price, можно поместить определение первой фун кции внутрь определения второй:
def sort_by_price(items: list):
# helper function defined within the function
def order_by_price(item: tuple):
return item[1]
return sorted(items, key=order_by_price)Сортировка коллекций собственных объектов
Используя тот же принцип, напишем программу, которая сортирует список объектов нашего собственного класса Student двумя разными способами:
class Student:
""" The class models a single student """
def __init__(self, name: str, id: str, credits: int):
self.name = name
self.id = id
self.credits = credits
def __str__(self):
return f"{self.name} ({self.id}), {self.credits} cr."
def by_id(item: Student):
return item.id
def by_credits(item: Student):
return item.credits
if __name__ == "__main__":
o1 = Student("Archie", "a123", 220)
o2 = Student("Marvin", "m321", 210)
o3 = Student("Anna", "a999", 131)
students = [o1, o2, o3]
print("Sort by id:")
for student in sorted(students, key=by_id):
print(student)
print()
print("Sort by credits:")
for student in sorted(students, key=by_credits):
print(student)Sort by id: Archie (a123), 220 cr. Anna (a999), 131 cr. Marvin (m321), 210 cr.
Sort by credits: Anna (a999), 131 cr. Marvin (m321), 210 cr. Archie (a123), 220 cr.
Как видно выше, сортировка по разным критериям работает именно так, как задумано. Если функции by_id и by_credits больше нигде не нужны, есть способы упростить реализацию. Мы вернемся к этой теме после следующих упражнений.
Лямбда-выражения
До сих пор мы в основном рассматривали функции с точки зрения модульности. Это правда, что функции играют важную роль в управлении сложностью программ и помогают избегать повторения кода. Функции обычно пишут так, чтобы их можно было использовать много раз.
Но иногда нужно нечто похожее на функцию, которую вы используете только один раз. Лямбда-выражения позволяют создавать небольшие анонимные функции, которые создаются (и отбрасываются) по мере необходимости в коде. Общий синтаксис выглядит так:
lambda <parameters> : <expression>
Сортировка списка кортежей по второму элементу в каждом кортеже при реализации через лямбда-выражение выглядела бы так:
products = [("banana", 5.95), ("apple", 3.95), ("orange", 4.50), ("watermelon", 4.95)]
# Function is created "on the fly" with a lambda expression:
products.sort(key=lambda item: item[1])
for product in products:
print(product)('apple', 3.95) ('orange', 4.5) ('watermelon', 4.95) ('banana', 5.95)
Выражение
lambda item: item[1]
эквивалентно определению функции
def price(item):
return item[1]за исключением того, что у лямбда-функции нет имени. Именно поэтому лямбда-функции называют анонимными функциями.
Во всех остальных отношениях лямбда-функция не отличается от любой другой функции, и ее можно использовать во всех тех же контекстах, что и эквивалентную именованную функцию. Например, следующая программа сортирует список строк в алфавитном порядке по последнему символу каждой строки:
strings = ["Mickey", "Mack", "Marvin", "Minnie", "Merl"]
for word in sorted(strings, key=lambda word: word[-1]):
print(word)Minnie Mack Merl Marvin Mickey
Также можно объединять генераторы списков, метод join и лямбда-выражения. Например, можно отсортировать строки только по гласным в них, игнорируя все остальные символы:
strings = ["Mickey", "Mack", "Marvin", "Minnie", "Merl"]
for word in sorted(strings, key=lambda word: "".join([c for c in word if c in "aeiou"])):
print(word)Mack Marvin Merl Mickey Minnie
Анонимные функции можно использовать и с другими встроенными функциями Python, не только с теми, которые применяются для сортировки. Например, функции min и max тоже принимают именованный аргумент key. Он используется как критерий для сравнения элементов при выборе минимального или максимального значения.
В следующем примере мы работаем с аудиозаписями. Сначала выбираем самую старую запись, а затем самую длинную:
class Recording:
""" The class models a single audio recording """
def __init__(self, name: str, performer: str, year: int, runtime: int):
self.name = name
self.performer = performer
self.year = year
self.runtime = runtime
def __str__(self):
return f"{self.name} ({self.performer}), {self.year}. {self.runtime} min."
if __name__ == "__main__":
r1 = Recording("Nevermind", "Nirvana", 1991, 43)
r2 = Recording("Let It Be", "Beatles", 1969, 35)
r3 = Recording("Joshua Tree", "U2", 1986, 50)
recordings = [r1, r2, r3]
print("The oldest recording:")
print(min(recordings, key=lambda rec: rec.year))
print("The longest recording:")
print(max(recordings, key=lambda rec: rec.runtime))The oldest recording: Let It Be (Beatles), 1969. 35 min. The longest recording: U2 (Joshua Tree), 1986. 50 min.
Функции как аргументы внутри собственных функций
Выше мы установили, что можно передавать ссылку на функцию в качестве аргумента другой функции. В завершение этого раздела напишем собственную функцию, которая принимает функцию как аргумент.
# the type hint "callable" refers to a function
def perform_operation(operation: callable):
# Call the function which was passed as an argument
return operation(10, 5)
def my_sum(a: int, b: int):
return a + b
def my_product(a: int, b: int):
return a * b
if __name__ == "__main__":
print(perform_operation(my_sum))
print(perform_operation(my_product))
print(perform_operation(lambda x,y: x - y))
15 50 5
Значение, возвращаемое функцией perform_operation, за висит от того, какая функция была передана аргументом. Подойдет любая функция, которая принимает два аргумента, независимо от того, анонимная она или именованная.
Передача ссылок на функции в качестве аргументов другим функциям может и не стать тем, что вы будете делать каждый день в своей карьере программиста, но это может быть полезным приемом. Следующая программа выбирает некоторые строки из одного файла и записывает их в другой файл. То, как выбираются строки, определяется функцией, которая возвращает True только для строк, которые нужно скопировать:
def copy_lines(source_file: str, target_file: str, criterion= lambda x: True):
with open(source_file) as source, open(target_file, "w") as target:
for line in source:
# Remove any whitespace from beginning and end of line
line = line.strip()
if criterion(line):
target.write(line + "\n")
# Some examples
if __name__ == "__main__":
# If the third parameter is not given, copy all lines
copy_lines("first.txt", "second.txt")
# Copy all non-empty lines
copy_lines("first.txt", "second.txt", lambda line: len(line) > 0)
# Copy all lines which contain the word "Python"
copy_lines("first.txt", "second.txt", lambda line: "Python" in line)
# Copy all lines which do not end in a full stop
copy_lines("first.txt", "second.txt", lambda line: line[-1] != ".")Определение функции содержит значение по умолчанию для именованного параметра criterion: lambda x: True. Эта анонимная функция всегда возвращает True независимо от входных да нных. Поэтому поведение по умолчанию — копировать все строки. Как обычно, если для параметра со значением по умолчанию передано новое значение, оно заменяет значение по умолчанию.
Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.