Part 12

Функции как аргументы

Мы уже знакомы с методом 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)
Loading
Loading
Loading

Сортировка коллекций собственных объектов

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

Loading
Loading

Лямбда-выражения

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

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

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.

Loading

Функции как аргументы внутри собственных функций

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

# 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 независимо от входных данных. Поэтому поведение по умолчанию — копировать все строки. Как обычно, если для параметра со значением по умолчанию передано новое значение, оно заменяет значение по умолчанию.

Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

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