10. Рекурсия и исключения

10.1. Кортежи и изменяемость

До сих пор мы встречались с двумя составными типами: строки, состоящие из символов, и списки, состоящие из элементов любого типа. Одно из отмеченных нами различий между этими типами состоит в том, что элементы списка можно изменять, а символы строки - нельзя. Другими словами, строки являются неизменяемыми, а списки - изменяемыми.

Кортеж (англ.: tuple), как и список, представляет собой последовательность элементов любого типа. Но, в отличие от списков, кортежи неизменяемы. Синтаксически, кортеж - это последовательность разделенных запятыми значений:

>>> tup = 2, 4, 6, 8, 10

Хотя это не является обязательным, принято заключать кортежи в скобки:

>>> tup = (2, 4, 6, 8, 10)

Для того, чтобы создать кортеж с единственным элементом, нужно поставить запятую после единственного элемента:

>>> tup = (5,)
>>> type(tup)
<type 'tuple'>

Без запятой Python считает, что (5) - это целое число в скобках:

>>> tup = (5)
>>> type(tup)
<type 'int'>

Оставляя в стороне синтаксис, кортежи поддерживают те же самые операции для последовательностей, что строки и списки. Оператор индекс выбирает элемент кортежа.

>>> tup = ('a', 'b', 'c', 'd', 'e')
>>> tup[0]
'a'

Оператор срез выбирает диапазон элементов.

>>> tup[1:3]
('b', 'c')

Однако, если попробовать выполнить присваивание элементу кортежа, то получим ошибку:

>>> tup[0] = 'X'
TypeError: 'tuple' object does not support item assignment

И, разумеется, хотя мы и не можем изменить отдельный элемент кортежа, мы можем заменить кортеж на другой кортеж:

>>> tup = ('X',) + tup[1:]
>>> tup
('X', 'b', 'c', 'd', 'e')

Или, вместо этого, мы могли бы сначала преобразовать кортеж в список, изменить список, и затем преобразовать его обратно в кортеж:

>>> tup = ('X', 'b', 'c', 'd', 'e')
>>> tup = list(tup)
>>> tup
['X', 'b', 'c', 'd', 'e']
>>> tup[0] = 'a'
>>> tup = tuple(tup)
>>> tup
('a', 'b', 'c', 'd', 'e')

10.2. Присваивание кортежей

Время от времени возникает необходимость обменять значения двух переменных. Пользуясь традиционными предложениями присваивания, мы должны использовать временную переменную. Например, чтобы обменять значения переменных a и b:

temp = a
a = b
b = temp

Если нам придется делать это часто, такой подход покажется громоздким. Python предоставляет возможность присваивания кортежей, которая элегантно решает эту задачу:

a, b = b, a

В левой части у нас кортеж из двух переменных; в правой - кортеж из двух значений. Каждое значение присваивается соответствующей ему переменной. Все выражения в правой части вычисляются до того, как выполняется первое присваивание. Это свойство делает присваивание кортежа очень полезным инструментом.

Само собой разумеется, что количество переменных в левой части и количество значений в правой должны быть одинаковы:

>>> a, b, c, d = 1, 2, 3
ValueError: need more than 3 values to unpack

10.3. Кортежи как возвращаемые значения

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

def swap(x, y):
    return y, x

После чего мы сможем присвоить возвращаемое значение кортежу из двух переменных:

a, b = swap(a, b)

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

def swap(x, y):      # incorrect version
     x, y = y, x

Если вызвать эту функцию так:

swap(a, b)

то a и x будут альтернативными именами для одного и того же значения. Изменение x в функции swap приводит к тому, что x указывает на другое значение, но это никак не отразится на переменной a в __main__. Аналогично, изменение y никак не повлияет на b.

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

10.4. Еще раз о чистых и модифицирующих функциях

Ранее в разделе Чистые и модифицирующие функции мы обсуждали чистые функции и модифицирующие функции в связи со списками. Поскольку кортежи неизменяемы, нельзя написать функцию, модифицирующую кортеж.

Пример модифицирующей функции, вставляющей новое значение в середину списка:

#
# seqtools.py
#

def insert_in_middle(val, lst):
    middle = len(lst)/2
    lst[middle:middle] = [val]

Убедимся, что она работает:

>>> from seqtools import *
>>> my_list = ['a', 'b', 'd', 'e']
>>> insert_in_middle('c', my_list)
>>> my_list
['a', 'b', 'c', 'd', 'e']

Но если мы попробуем вызвать ее с кортежем, то получим ошибку:

>>> my_tuple = ('a', 'b', 'd', 'e')
>>> insert_in_middle('c', my_tuple)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "seqtools.py", line 7, in insert_in_middle
    lst[middle:middle] = [val]
TypeError: 'tuple' object does not support item assignment
>>>

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

def insert_in_middle(val, tup):
    middle = len(tup)/2
    return tup[:middle] + (val,) + tup[middle:]

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

def encapsulate(val, seq):
    if type(seq) == type(""):
        return str(val)
    if type(seq) == type([]):
        return [val]
    return (val,)

Теперь можно написать версию insert_in_middle, работающую с последовательностями всех встроенных типов:

def insert_in_middle(val, seq):
    middle = len(seq)/2
    return seq[:middle] + encapsulate(val, seq) + seq[middle:]

Две последние версии insert_in_middle являются чистыми функциями. У них нет побочных эффектов. Добавив encapsulate и последнюю версию insert_in_middle в модуль seqtools.py, мы сможем протестировать функцию:

>>> from seqtools import *
>>> my_string = 'abde'
>>> my_list = ['a', 'b', 'd', 'e']
>>> my_tuple = ('a', 'b', 'd', 'e')
>>> insert_in_middle('c', my_string)
'abcde'
>>> insert_in_middle('c', my_list)
['a', 'b', 'c', 'd', 'e']
>>> insert_in_middle('c', my_tuple)
('a', 'b', 'c', 'd', 'e')
>>> my_string
'abde'

Значения my_string, my_list и my_tuple не изменились. Если мы захотим использовать insert_in_middle для их изменения, нам придется присваивать возвращаемое функцией значение обратно переменной:

>>> my_string = insert_in_middle('c', my_string)
>>> my_string
'abcde'

10.5. Рекурсивные структуры данных

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

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

Вложенный список чисел - это список, элементы которого относятся к одному из двух типов:

  1. числа
  2. вложенные списки чисел

Заметьте, что термин ‘вложенный список чисел’ использован в его собственном определении. Рекурсивные определения, подобные этому, довольно распространены в математике и программировании. Они предоставляют краткий и мощный способ описать рекурсивные структуры данных, частично состоящие из меньших и более простых экземпляров самих себя. Структура не циклична, поскольку рано или поздно мы достигаем списка, который не содержит других списков в качестве элементов.

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

>>> sum([1, 2, 8])
11
>>> sum((3, 5, 8.5))
16.5
>>>

Для нашего вложенного списка чисел, однако, функция sum не работает:

>>> sum([1, 2, [11, 13], 8])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'list'
>>>

Проблема в том, что третий элемент этого списка, [11, 13], сам является списком, а список не может быть просуммирован с целыми числами 1, 2 и 8.

10.6. Рекурсия

Для того, чтобы просуммировать все числа в нашем вложенном списке чисел, нам нужно обойти список, обработав каждый элемент его многоуровневой структуры. Числовые элементы будем складывать, а для списочных элементов повторять этот же обход.

Современные языки программирования, в основном, поддерживают рекурсию, что означает, функции могут вызывать себя внутри своего определения. Благодаря рекурсии, код на Python для суммирования всех чисел вложенного списка чисел оказывается на удивление коротким:

def recursive_sum(nested_num_list):
    sum = 0
    for element in nested_num_list:
        if type(element) == type([]):
            sum += recursive_sum(element)
        else:
            sum += element
    return sum

Тело функции recursive_sum содержит цикл for, который реализует обход nested_num_list. Если element оказывается числом (ветка else), он прибавляется к sum. Если element - список, то вновь вызывается recursive_sum с этим элементом в качестве аргумента. Предложение внутри определения функции, которое вызывает эту же функцию, называют рекурсивным вызовом функции.

Рекурсия поистине один из самых красивых и элегантных инструментов в программировании.

Несколько более сложной задачей является нахождение самого большого числа в нашем вложенном списке чисел:

def recursive_max(nested_num_list):
    """
      >>> recursive_max([2, 9, [1, 13], 8, 6])
      13
      >>> recursive_max([2, [[100, 7], 90], [1, 13], 8, 6])
      100
      >>> recursive_max([2, [[13, 7], 90], [1, 100], 8, 6])
      100
      >>> recursive_max([[[13, 7], 90], 2, [1, 100], 8, 6])
      100
    """
    largest = nested_num_list[0]
    while type(largest) == type([]):
        largest = largest[0]

    for element in nested_num_list:
        if type(element) == type([]):
            max_of_elem = recursive_max(element)
            if largest < max_of_elem:
                largest = max_of_elem
        else:                           # element is not a list
            if largest < element:
                largest = element

    return largest

Приведенные доктесты демонстрируют recursive_max в работе.

Дополнительная хитрость потребовалась для нахождения числового значения для инициализации переменной largest. Мы не можем просто использовать nested_num_list[0], так как это может быть либо число, либо список. Для решения этой задачи мы воспользовались циклом while, в котором присваиваем largest первое из найденных числовых значений, и неважно, насколько глубоко оно вложено.

Два приведенных примера оба имеют базовый случай который не приводит к рекурсивному вызову: случай, когда элемент является числом, а не списком. Без базового случая мы получили бы бесконечную рекурсию, и программа бы не работала. Python бы прекратил выполнение программы, достигнув максимально разрешенной глубины рекурсии, и возвратил ошибку выполнения.

Создайте файл infinite_recursion.py со следующим кодом:

#
# infinite_recursion.py
#
def recursion_depth(number):
    print "Recursion depth number %d." % number
    recursion_depth(number + 1)

recursion_depth(0)

Перейдя в каталог, где вы сохранили файл, введите в командной строке:

$ python infinite_recursion.py

После того, как промелькнут и остановятся сообщения, вы увидите конец длинной последовательности сообщений об ошибках - распечатки стека вызовов:

  ...
  File "infinite_recursion.py", line 3, in recursion_depth
    recursion_depth(number + 1)
RuntimeError: maximum recursion depth exceeded

Мы, конечно же, не хотим, чтобы что-то подобное получил пользователь, запустив одну из наших программ. Поэтому, прежде чем завершить обсуждение рекурсии, давайте посмотрим, как в Python обрабатываются ошибки, подобные этой.

10.7. Исключения

Каждый раз, когда происходит ошибка выполнения, создается исключение (англ.: exception). Программа прекращает выполняться и Python распечатывает стек вызовов, где в конце идет сообщение о возникшем исключении.

Например, деление на ноль создает исключение:

>>> print 55/0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
>>>

Так же, как и попытка прочитать несуществующий элемент списка:

>>> a = []
>>> print a[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>>

Или попытка присвоить значение элементу кортежа:

>>> tup = ('a', 'b', 'd', 'd')
>>> tup[2] = 'c'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>>

Во всех случаях сообщение об ошибке на последней строке состоит из двух частей: тип ошибки перед двоеточием, и конкретное описание ошибки после него.

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

Например, мы можем попросить пользователя ввести имя файла, и затем попробовать открыть его. Но мы не хотим, чтобы программа сломалась, если файл не существует; мы хотим обработать вероятное исключение:

filename = raw_input('Enter a file name: ')
try:
    f = open (filename, "r")
except:
    print 'There is no file named', filename

Предложение try выполняет предложения в первом блоке. Если исключения не происходит, предложение except игнорируется. Если же возникает исключение, то выполняются предложения блока except, после чего выполнение программы продолжается.

Можно встроить эту конструкцию в функцию exists (англ.: существует), которая принимает в качестве параметра имя файла и возвращает True, если файл существует, и False, если он не существует:

def exists(filename):
    try:
        f = open(filename)
        f.close()
        return True
    except:
        return False

Можно использовать несколько блоков except для того, чтобы обрабатывать различные исключения (см. урок Errors and Exceptions в учебнике Python Tutorial от создателя Python Гвидо ван Россума).

Если ваша программа обнаруживает ошибочную ситуацию, то она может возбудить (англ.: raise) исключение. Вот пример, в котором принимается ввод от пользователя и проверяется, что введенное число неотрицательно.

#
# learn_exceptions.py
#
def get_age():
    age = input('Please enter your age: ')
    if age < 0:
        raise ValueError, '%s is not a valid age' % age
    return age

Предложение raise принимает два аргумента: тип исключения, и конкретное описание ошибки. ValueError является встроенным типом исключения, который лучше всего подходит в данной ситуации. Полный список встроенных исключений можно найти в разделе 2.3 Справочника по библиотеке Python, также написанного создателем Python Гвидо ван Россумом.

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

>>> get_age()
Please enter your age: 42
42
>>> get_age()
Please enter your age: -2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "learn_exceptions.py", line 4, in get_age
    raise ValueError, '%s is not a valid age' % age
ValueError: -2 is not a valid age
>>>

Сообщение об ошибке включает тип ошибки и дополнительную информацию, которую вы указали.

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

#
# infinite_recursion.py
#
def recursion_depth(number):
    print "Recursion depth number %d." % number
    try:
        recursion_depth(number + 1)
    except:
        print "Maximum recursion depth exceeded."

recursion_depth(0)

Запустите эту версию и посмотрите на результат.

10.8. Хвостовая рекурсия

Когда рекурсивный вызов является последним предложением в определении функции, такую рекурсию называют хвостовой рекурсией.

Вот версия функции countdown из главы 6, написанная с использованием хвостовой рекурсии:

def countdown(n):
    if n == 0:
        print "Go!"
    else:
        print n
        countdown(n-1)

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

Рекурсивно определяются некоторые из хорошо известных математических функций. Например, факториал, для которого имеется собственный оператор, !, определяется так:

0! = 1
n! = n(n-1)

Можно легко закодировать это на Python:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

Другой пример, хорошо известный из математики — это последовательность Фибоначчи, определяемая так:

fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)

Это тоже можно с легкостью записать на языке Python:

def fibonacci (n):
    if n == 0 or n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

Обе функции, и factorial и fibonacci, демонстрируют хвостовую рекурсию.

Однако, хвостовая рекурсия считается плохой практикой в таких языках, как Python, так как она требует больше системных ресурсов, чем эквивалентное решение, использующее итерацию.

Вызов factorial(1000) превысит максимально допустимую глубину рекурсии. Также попробуйте выполнить fibonacci(35) и посмотрите, сколько времени на это понадобится (будьте терпеливы, вы получите результат).

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

10.9. Списочное включение

Списочное включение (англ.: list comprehension) - это синтаксическая конструкция, которая позволяет создавать списки из других списков, используя компактную математическую нотацию:

>>> numbers = [1, 2, 3, 4]
>>> [x**2 for x in numbers]
[1, 4, 9, 16]
>>> [x**2 for x in numbers if x**2 > 8]
[9, 16]
>>> [(x, x**2, x**3) for x in numbers]
[(1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]
>>> files = ['bin', 'Data', 'Desktop', '.bashrc', '.ssh', '.vimrc']
>>> [name for name in files if name[0] != '.']
['bin', 'Data', 'Desktop']
>>> letters = ['a', 'b', 'c']
>>> [n*letter for n in numbers for letter in letters]
['a', 'b', 'c', 'aa', 'bb', 'cc', 'aaa', 'bbb', 'ccc', 'aaaa', 'bbbb', 'cccc']
>>>

Обобщенный синтаксис для выражения, генерирующего список, таков:

[expr for  item1 in  seq1 for item2 in seq2 ... for itemx in seqx if condition]

Выражение списочного включения имеет тот же эффект, что и следующий код:

output_sequence = []
for item1 in seq1:
    for item2 in seq2:
        ...
            for itemx in seqx:
                if condition:
                    output_sequence.append(expr)

Как видите, код, использующий списковое включение, гораздо компактнее.

10.10. Маленький учебный пример: дерево

Следующий пример частично реализует поведение программы tree, имеющейся в операционных системах Unix.

#!/usr/bin/env python

import os
import sys


def getroot():
    if len(sys.argv) == 1:
        path = ''
    else:
        path = sys.argv[1]

    if os.path.isabs(path):
        tree_root = path
    else:
        tree_root = os.path.join(os.getcwd(), path)

    return tree_root


def getdirlist(path):
    dirlist = os.listdir(path)
    dirlist = [name for name in dirlist if name[0] != '.']
    dirlist.sort()
    return dirlist


def traverse(path, prefix='|--', s='.\n', f=0, d=0):
    dirlist = getdirlist(path)

    for num, file in enumerate(dirlist):
        lastprefix = prefix[:-3] + '``--'
        dirsize = len(dirlist)

        if num < dirsize - 1:
            s += '%s %s\n' % (prefix, file)
        else:
            s += '%s %s\n' % (lastprefix, file)
        path2file = os.path.join(path, file)

        if os.path.isdir(path2file):
            d += 1
            if getdirlist(path2file):
                s, f, d = traverse(path2file, '|   ' + prefix, s, f, d)
        else:
            f += 1

    return s, f, d


if __name__ == '__main__':
    root =  getroot()
    tree_str, files, dirs = traverse(root)

    if dirs == 1:
        dirstring = 'directory'
    else:
        dirstring = 'directories'
    if files == 1:
        filestring = 'file'
    else:
        filestring = 'files'

    print tree_str
    print '%d %s, %d %s' % (dirs, dirstring, files, filestring)

Ниже вам будет предложено самостоятельно исследовать эту программу в качестве упражнений.

10.11. Глоссарий

базовый случай
Ветка условного выполнения в рекурсивной функции, которая не выполняет рекурсивного вызова.
бесконечная рекурсия
Функция, которая вызывает саму себя рекурсивно, никогда не достигая базового случая. В конце концов бесконечная рекурсия приводит к ошибке выполнения.
возбуждение исключения
Создание исключения при помощи предложения raise.
исключение
Ошибка выполнения.
кортеж
Тип данных, значения которого - последовательности элементов любого типа. В отличие от списка, кортеж неизменяем. Кортежи можно использовать всякий раз, когда требуется неизменяемый тип, например, в качестве ключа в словаре (см. следующую главу).
неизменяемый тип данных
Значения такого типа нельзя изменить. Присваивания элементам или срезам неизменяемого типа вызывают ошибку выполнения.
обработка исключения
Предотвращение завершения программы в случае возникновения исключения, с помощью предложений try и except.
присваивание кортежа
Присваивание всем элементам кортежа, выполняемое одним предложением присваивания. Присваивание кортежу выполняется после вычисления всех значений в правой части присваивания, так что может использоваться для взаимного обмена значений переменных.
рекурсивное определение
Определение, которое определяет нечто в терминах этого самого нечто. Чтобы иметь практическую пользу, оно должно включать базовые случаи, которые не являются рекурсивными. Этим рекурсивное определение отличается от циклического определения. Рекурсивные определения часто предоставляют элегантный способ построения сложных структур данных.
рекурсия
Процесс вызова функцией самой себя, прямо или опосредованно.
рекурсивный вызов
Вызов функцией самой себя.
структура данных
Организация данных с целью с сделать их использование легче.
списочное включение
Синтаксическая конструкция, которая позволяет порождать список из другого списка, используя нотацию, похожую на математическую.
хвостовая рекурсия
Рекурсивный вызов в последнем предложении функции. Хвостовая рекурсия считается плохой практикой в программах Python, так как может быть написана логически эквивалентная функция, использующая итерацию, что более эффективно. См. статью в Википедии Хвостовая рекурсия.

10.12. Упражнения

  1. Выполните эту программу и объясните результаты:

    def swap(x, y):      # incorrect version
         print  "before swap statement: id(x):", id(x), "id(y):", id(y)
         x, y = y, x
         print  "after swap statement: id(x):", id(x), "id(y):", id(y)
    
    a, b = 0, 1
    print  "before swap function call: id(a):", id(a), "id(b):", id(b)
    swap(a, b)
    print  "after swap function call: id(a):", id(a), "id(b):", id(b)
    

    Объясните, почему эта версия swap не работает, как задумано. Какими будут значения a и b после вызова swap?

  2. Создайте модуль с именем seqtools.py. Поместите в него функции encapsulate и insert_in_middle из этой главы. Добавьте доктесты, проверяющие, что эти функции работают правильно со всеми тремя встроенными типами последовательностей.

  3. Добавьте следующие функции в seqtools.py:

    def make_empty(seq):
        """
          >>> make_empty([1, 2, 3, 4])
          []
          >>> make_empty(('a', 'b', 'c'))
          ()
          >>> make_empty("No, not me!")
          ''
        """
    
    def insert_at_end(val, seq):
        """
          >>> insert_at_end(5, [1, 3, 4, 6])
          [1, 3, 4, 6, 5]
          >>> insert_at_end('x', 'abc')
          'abcx'
          >>> insert_at_end(5, (1, 3, 4, 6))
          (1, 3, 4, 6, 5)
        """
    
    def insert_in_front(val, seq):
        """
          >>> insert_in_front(5, [1, 3, 4, 6])
          [5, 1, 3, 4, 6]
          >>> insert_in_front(5, (1, 3, 4, 6))
          (5, 1, 3, 4, 6)
          >>> insert_in_front('x', 'abc')
          'xabc'
        """
    
    def index_of(val, seq, start=0):
        """
          >>> index_of(9, [1, 7, 11, 9, 10])
          3
          >>> index_of(5, (1, 2, 4, 5, 6, 10, 5, 5))
          3
          >>> index_of(5, (1, 2, 4, 5, 6, 10, 5, 5), 4)
          6
          >>> index_of('y', 'happy birthday')
          4
          >>> index_of('banana', ['apple', 'banana', 'cherry', 'date'])
          1
          >>> index_of(5, [2, 3, 4])
          -1
          >>> index_of('b', ['apple', 'banana', 'cherry', 'date'])
          -1
        """
    
    def remove_at(index, seq):
        """
          >>> remove_at(3, [1, 7, 11, 9, 10])
          [1, 7, 11, 10]
          >>> remove_at(5, (1, 4, 6, 7, 0, 9, 3, 5))
          (1, 4, 6, 7, 0, 3, 5)
          >>> remove_at(2, "Yomrktown")
          'Yorktown'
        """
    
    def remove_val(val, seq):
        """
          >>> remove_val(11, [1, 7, 11, 9, 10])
          [1, 7, 9, 10]
          >>> remove_val(15, (1, 15, 11, 4, 9))
          (1, 11, 4, 9)
          >>> remove_val('what', ('who', 'what', 'when', 'where', 'why', 'how'))
          ('who', 'when', 'where', 'why', 'how')
        """
    
    def remove_all(val, seq):
        """
          >>> remove_all(11, [1, 7, 11, 9, 11, 10, 2, 11])
          [1, 7, 9, 10, 2]
          >>> remove_all('i', 'Mississippi')
          'Msssspp'
        """
    
    def count(val, seq):
        """
          >>> count(5, (1, 5, 3, 7, 5, 8, 5))
          3
          >>> count('s', 'Mississippi')
          4
          >>> count((1, 2), [1, 5, (1, 2), 7, (1, 2), 8, 5])
          2
        """
    
    def reverse(seq):
        """
          >>> reverse([1, 2, 3, 4, 5])
          [5, 4, 3, 2, 1]
          >>> reverse(('shoe', 'my', 'buckle', 2, 1))
          (1, 2, 'buckle', 'my', 'shoe')
          >>> reverse('Python')
          'nohtyP'
        """
    
    def sort_sequence(seq):
        """
          >>> sort_sequence([3, 4, 6, 7, 8, 2])
          [2, 3, 4, 6, 7, 8]
          >>> sort_sequence((3, 4, 6, 7, 8, 2))
          (2, 3, 4, 6, 7, 8)
          >>> sort_sequence("nothappy")
          'ahnoppty'
        """
    
    if __name__ == "__main__":
        import doctest
        doctest.testmod()
    

    Как и прежде, напишите тела функций, проверяя их работу с помощью доктестов по готовности каждой новой функции.

  4. Напишите функцию recursive_min, которая возвращает наименьшее число из вложенного списка чисел:

    def recursive_min(nested_num_list):
        """
          >>> recursive_min([2, 9, [1, 13], 8, 6])
          1
          >>> recursive_min([2, [[100, 1], 90], [10, 13], 8, 6])
          1
          >>> recursive_min([2, [[13, -7], 90], [1, 100], 8, 6])
          -7
          >>> recursive_min([[[-13, 7], 90], 2, [1, 100], 8, 6])
          -13
        """
    

    Ваша функция должна успешно пройти доктесты.

  5. Напишите функцию recursive_count, которая возвращает количество вхождений target в nested_number_list:

    def recursive_count(target, nested_num_list):
        """
          >>> recursive_count(2, [2, 9, [2, 1, 13, 2], 8, [2, 6]])
          4
          >>> recursive_count(7, [[9, [7, 1, 13, 2], 8], [7, 6]])
          2
          >>> recursive_count(15, [[9, [7, 1, 13, 2], 8], [2, 6]])
          0
          >>> recursive_count(5, [[5, [5, [1, 5], 5], 5], [5, 6]])
          6
        """
    

    Ваша функция должна успешно пройти доктесты.

  6. Напишите функцию flatten, которая возвращает (невложенный) список чисел, содержащий все числа из nested_number_list:

    def flatten(nested_num_list):
        """
          >>> flatten([2, 9, [2, 1, 13, 2], 8, [2, 6]])
          [2, 9, 2, 1, 13, 2, 8, 2, 6]
          >>> flatten([[9, [7, 1, 13, 2], 8], [7, 6]])
          [9, 7, 1, 13, 2, 8, 7, 6]
          >>> flatten([[9, [7, 1, 13, 2], 8], [2, 6]])
          [9, 7, 1, 13, 2, 8, 2, 6]
          >>> flatten([[5, [5, [1, 5], 5], 5], [5, 6]])
          [5, 5, 1, 5, 5, 5, 5, 6]
        """
    

    Убедитесь, что ваша функция успешно проходит доктесты.

  7. Напишите функцию readposint, которая просит пользователя ввести положительное целое число, и затем проверяет то, что ввел пользователь. Вот как можно работать с такой функцией:

    >>> num = readposint()
    Please enter a positive integer: yes
    yes is not a positive integer.  Try again.
    Please enter a positive integer: 3.14
    3.14 is not a positive integer.  Try again.
    Please enter a positive integer: -6
    -6 is not a positive integer.  Try again.
    Please enter a positive integer: 42
    >>> num
    42
    >>> num2 = readposint("Now enter another one: ")
    Now enter another one: 31
    >>> num2
    31
    >>>
    

    Используйте обработку исключений при проверке введенного пользователем значения.

  8. Предскажите, что выдаст Python в ответ на следующее:

    >>> nums = [1, 2, 3, 4]
    >>> [x**3 for x in nums]
    
    >>> nums = [1, 2, 3, 4]
    >>> [x**2 for x in nums if x**2 != 4]
    
    >>> nums = [1, 2, 3, 4]
    >>> [(x, y) for x in nums for y in nums]
    
    >>> nums = [1, 2, 3, 4]
    >>> [(x, y) for x in nums for y in nums if x != y]
    

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

  9. Воспользовавшись pydoc или онлайновой документацией на сайте http://docs.python.org/, выясните, что делают функции sys.getrecursionlimit() и sys.setrecursionlimit(n). Проведите несколько экспериментов, подобных тому, который мы поставили с помощью infinite_recursion.py, чтобы убедиться в том, как работают эти функции.

  10. Перепишите функцию factorial, используя итерацию вместо рекурсии. Вызовите вашу новую функцию с аргументом 1000 и заметьте, как быстро она отработает.

  11. Напишите программу litter.py, которая создает пустой файл с именем trash.txt в каждом каталоге дерева каталогов. Корень дерева каталогов программа получает в качестве аргумента (или использует текущий каталог по умолчанию). После этого напишите программу cleanup.py, которая удаляет все эти файлы. Подсказка: используйте программу tree из учебного примера в качестве основы для разработки этих двух рекурсивных программ.