Программирование на Python

Функции и процедуры

Разбить на страницы
Показывать лекцию целиком

Проект для лекции Lecture4.rar.

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

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

Функции - один из основных инструментов математиков - предмет математического (функционального) анализа. И для программистов функция - базисный элемент построения программ.

В программировании принято рассматривать два понятия - функция и процедура. В некоторых языках ООП используется термин метод класса, который может быть реализован либо как функция, либо как процедура.

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

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

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

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

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

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

Функции и процедуры языка Python

Формально, в языке Python все, что задается определением def, является функцией (class function).

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

Все параметры метода - это входные параметры. Выходных параметров методы Python не имеют. Оператор return в теле метода, как и положено, возвращает результат вычисления функции. Как же быть, когда в методе вычисляется несколько результатов?

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

return result1 [, result2, … result_n]

Методы Python могут использоваться как классические функции, так и как процедуры.

Классическая функция Python обязательно содержит в теле функции оператор return. Вызов такой функции является первичным выражением и может быть сделан всюду, где допускаются по синтаксису выражения, например, $$y = sin(x) + cos(2 * x)$$. Здесь $$sin$$ и $$cos$$ - это имена функций, определенных в классе math.

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

В предыдущих главах все примеры использовали соответствующие методы test, являющиеся фактически процедурами и вызываемые как процедуры. Эти методы без входных параметров использовали в качестве входных данных локальные переменные, значения которых вычислялись непосредственно в теле функции или задавались пользователем с консоли. Результаты работы выводились на консоль вызовом функции print. В некоторых вариантах тестовых процедур входные данные брались из входного файла, а результаты записывались в выходной файл.

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

Описание классической функции. Вызов функции

Синтаксис описания функции в наиболее употребительном варианте достаточно прост:

def <имя функции> (<список имен аргументов>):
		<Операторы тела функции>

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

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

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

В отличие от привычного для меня стиля, когда заголовочный комментарий предшествует заголовку функции, в языке Pythonзаголовочный комментарий располагается сразу после заголовка функции (метода). Заголовочный комментарий является документированным комментарием и дает подсказку при вызове метода. Синтаксически заголовочный комментарий представляет текст, взятый в тройные кавычки, занимающий возможно несколько строчек.

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

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

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

$$P(x) = a_n * x^n + a_{n-1} * x^{n-1} + \dots + a_1 * x^1 + a_0$$

Входными параметрами (аргументами) являются два параметра - список коэффициентов полинома и число x. У функции один результат - значение полинома в точке x. Для вычисления результата будет использоваться классическая схема Горнера, требующая выполнения только n умножений и n сложений.

\def Gorner(a, x):
"""
    Вычисление полинома P(x) = Gorner(a, x)
    a - список коэффициентов полинома, первый в списке - старший коэффициент
    x - точка (число), в которой вычисляется значение полинома.
"""

    v = 0
    for coef in a:
        v = v * x + coef
    return v    

Выполним два вызова этой функции:

def test1():
    print("Polinom P(x) - Gorner Shema")
    p1 = Gorner([1, 2, 3], 2)
    print( "Gorner([1, 2, 3], 2) = ", p1)
    p2 = Gorner([3, 2, 1], 2)
    print( "Gorner([3, 2, 1], 2) = ", p2)
test1()

Вот результаты вычислений:

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

def Max_Min(a): 
"""
Вычисление максимального и минимального элементов списка a
"""
   
    max = min = a[0]
    for item in a:
        if item > max:
           max = item
        elif item < min:
           min = item
    res = [max, min]
    return res

Вызовем функцию на выполнение:

def test2():
    print ("Вычисление Max, Min списка ")
    a  = [5, 7, -2, 1, 8, 6]
    maxmin = Max_Min(a)
    print("max([5, 7, -2, 1, 8, 6]) = ", maxmin[0])
    print("min([5, 7, -2, 1, 8, 6]) = ", maxmin[1])
test2()

Результаты выполнения:

Аннотированный заголовок

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

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

Аннотированные заголовки для выше приведенных методов имеют вид:

def Gorner(a: list, x: float)->float:
def Max_Min(a: list)-> list: 
def test()->None:

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

def sum(a: int, b: int)->int :
    """ Возвращает сумму двух целочисленных операндов a и b
    """
    return a + b

Судя по заголовочному комментарию и аннотациям, функция sum вычисляет сумму двух целых чисел. Однако эта функция успешно работает для операндов самых разных типов, для которых определена операция "+". Операндами могут быть строки, списки, кортежи, а результатом является конкатенация операндов. Приведем тестовую процедуру:

def test0()->None:
    
    r = sum(5.5, 7.7)
    print (r)
    r = sum('a', 'b')
    print (r)
    r = sum([5, 5], [7, 7])
    print (r)
    r = sum(('five', 'seven'), ('one', 'two'))
    print (r)
    #r = sum({5, 7}, {1, 3})
    #print(r)
test0()

Результаты работы:

Как видите, функция, предназначенная для работы с целочисленными результатами, прекрасно работает и для операндов других типов, для которых определена операция сложения. Заметьте, два последних оператора в тестовой процедуре закомментированы, поскольку при попытке вызвать функцию с операндами типа "множество" (set) возникает ошибка периода выполнения. Конечно, ошибки на этапе выполнения - неприятные ошибки, гораздо лучше обнаруживать ошибки на этапе компиляции, что делается во многих языках программирования, допускающих статический контроль типов. Но создатели языка Python сознательно отказались от статического контроля типов в пользу имен, представляющих нетипизированные ссылки, - имен, которым можно присваивать объекты любых типов. Некоторое преимущество такого подхода демонстрирует метод sum, область применения которого гораздо шире в сравнении с ситуацией, когда формальные параметры имеют фиксированный тип.

Заметьте, при аннотировании типа формальных параметров и типа результата допустимо задать перечисление возможных типов. Так что определение функции sum может выглядеть так:

def sum(a: (int, float, str, list, tuple), b: (int, float, str, list, tuple))->(int, float, str, list, tuple) :
    """ Возвращает сумму двух операндов a и b
    """
    return a + b

Функции с переменным числом аргументов. Аргументы, имеющие значение по умолчанию

В приведенном выше синтаксисе заголовка функции списки формальных и фактических параметров должны соответствовать по числу параметров и их положению. При вызове функции k-му формальному параметру списка ставится в соответствие k- й фактический параметр. Язык Pythonпредлагает более гибкую стратегию соответствия списков фактических и формальных параметров. Во-первых, для некоторых параметров в момент объявления функции могут быть заданы значения по умолчанию. Для таких параметров в момент вызова фактические параметры могут не задаваться, если устраивают значения по умолчанию. Конечно, в момент вызова допускается изменение значений по умолчанию. Еще одно расширение связано с тем, что некоторому параметру может ставиться в соответствие не один фактический параметр, а целый кортеж. Понятно, что такой формальный параметр может существовать в единственном экземпляре.

Синтаксис заголовка функции в таком расширенном варианте выглядит следующим образом:

def <имя функции>(<список имен7gt;, *<имя>, <список имен со значениями по умолчанию>):

Список фактических параметров при вызове функции должен содержать не меньше элементов чем число элементов в первом списке имен. Пусть число формальных параметров в этом списке равно n, а после следует параметр со звездочкой. Пусть число фактических параметров равно m > n. Тогда размер кортежа фактических параметров, который ставится в соответствие параметру со звездочкой, равен m - n.

Для формальных параметров, входящих в список имен с заданными значениями по умолчанию, фактические параметры не задаются, если значения по умолчанию не требуют переопределения. Фактический параметр, имеющий форму: <имя> = <значение> при условии, что имя входит в список имен по умолчанию, заменяет значение, заданное по умолчанию, значением, заданным фактическим параметром.

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

def CreateList(n, *e, min = 0, max = 100):
    """
    Первоначально создается список из n элементов,
    заполненных случайными числами в диапазоне min ... max
    Затем список расширяется включением аргументов,
    число которых может быть произвольным.
    Значения аргументов min и max задаются по умолчанию
    """
    import random
    a = []
    for i in range(n):
        a.append(random.randint(min, max+1))
    for item in e:
        a.append(item)
    return a

Заметьте, параметр е, задаваемый со звездочкой, интерпретируется как коллекция элементов. Получить доступ ко всем фактическим параметрам, связанным с этим формальным параметром, можно итерированием по этой коллекции, используя оператор for.

Вот тест, в котором эта функция вызывается:

def test3():
   
    print ("Создание массива со случайными и неслучайными элементами")
    mas = CreateList(5, 77, 33, 11, min = -100)
    print("CreateList(5, 77, 33, 11, min = -100) = ", mas) 
test3()

Результаты работы:

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

import random
def CreateDepartment(p1, p2, p3, *p, director = "Висицкий", secretary = "Балабенкова"):
    """
    Формирование состава отдела
    p1, p2, p3 - фиксированные сотрудники
    *p - группа сотрудников произвольного размера
    director - директор - задан по умолчанию
    secretary - секретарь - задан по умолчанию
    """
    dep_state = []
    dep_state.append(p1)
    dep_state.append(p2)
    dep_state.append(p3)
    for item in p:
        dep_state.append(item)
    department = [director, secretary, dep_state]
    return department

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

def test4():
    #Случайный выбор сотрудников из фиксированного множества
    rand_set = ["Свердлов", "Суриков", "Тимофеев", "Фадеев", "Жукова", "Иванов"]
    p1 = rand_set[random.randint(0, 5)]
    rand_set.remove(p1)
    p2 = rand_set[random.randint(0, 4)]
    rand_set.remove(p2)
    p3 = rand_set[random.randint(0, 3)]
    rand_set.remove(p3)
    department = CreateDepartment(p1, p2, p3, "Седнев", "Ковальский", "Богун")
    print("директор = ", department[0])
    print("секретарь = ", department[1])
    print("сотрудники = ", department[2])
test4()

Результаты работы:

Функции, ссылки и объекты

Как уже говорилось, формальные параметры функции - это имена. Как всякие имена в Python, это ссылки, которые в момент присваивания связываются с объектом и получают его тип. Семантика замены формальных параметров (имен) на фактические аналогична семантике присваивания, так что при вызове функции имена формальных параметров связываются с объектами, заданными фактическими параметрами. При выполнении тела функции операции выполняются над объектами, заданными фактическими параметрами.

Что происходит, если в теле функции предпринимается попытка присвоить имени формального параметра некоторое значение? Изменяется ли объект, заданный фактическим параметром? Ответ - нет!

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

Рассмотрим пример:

def changeless(x):
    """
    Пример функции, изменяющей входной параметр
    """
    x = x + 2
    return 2 * x + 1

Рассмотрим тест, в котором вызывается эта функция:

def test5():
    y = 5
    print ("y = ", y )
    z = changeless(y)
    print ("y = ", y )
    print ("z = ", z )
test5()

Результаты работы:

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

Важно, однако, понимать, что утверждение о неизменности входных параметров - это лишь часть правды. Как утверждал поэт: "Не продается вдохновенье, но можно рукопись продать! " Изменить в теле функции связь между именем фактического параметра и объектом, с которым это имя связано, невозможно. Невозможно, чтобы в результате вызова функции имя указывало на другой объект. Но! можно изменить объект, связанный с этим именем. Такая ситуация встречается сплошь и рядом. В качестве примера рассмотрим функцию сортировки массива - пузырьковую сортировку списка.

def BubbleSort(ar):
    """
    Пузырьковая сортировка списка
    """

    n = len(ar) - 1   
    for i in range(n):
        for j in range(n, i, -1 ):
            if ar[j] < ar[j-1]:
                ar[j], ar[j-1] = ar[j-1], ar[j]
   return True 

Заметьте, функция возвращает формальный результат True. Главный эффект ее выполнения побочный - сортировка элементов списка, передаваемого функции в качестве аргумента. Функция не меняет ссылку на список, но изменяет список, сортируя его элементы.

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

def test6():
    ar = [5, 7, -3, 8, 16, 4]
    print ("ar before sorting :", ar)
    print("Вызывается как функция")
    p = BubbleSort(ar)
    print ("ar after sorting :", ar)
    ar = [15, 17, -13, 18, 16, 4]
    print ("ar before sorting :", ar)
    print("Вызывается как процедура")
    BubbleSort(ar)
    print ("ar after sorting :", ar)
test6()

Результат выполнения:

Страницы:

Проект для лекции Lecture4.rar.

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

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

Функции - один из основных инструментов математиков - предмет математического (функционального) анализа. И для программистов функция - базисный элемент построения программ.

В программировании принято рассматривать два понятия - функция и процедура. В некоторых языках ООП используется термин метод класса, который может быть реализован либо как функция, либо как процедура.

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

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

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

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

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

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

Функции и процедуры языка Python

Формально, в языке Python все, что задается определением def, является функцией (class function).

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

Все параметры метода - это входные параметры. Выходных параметров методы Python не имеют. Оператор return в теле метода, как и положено, возвращает результат вычисления функции. Как же быть, когда в методе вычисляется несколько результатов?

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

return result1 [, result2, … result_n]

Методы Python могут использоваться как классические функции, так и как процедуры.

Классическая функция Python обязательно содержит в теле функции оператор return. Вызов такой функции является первичным выражением и может быть сделан всюду, где допускаются по синтаксису выражения, например, $$y = sin(x) + cos(2 * x)$$. Здесь $$sin$$ и $$cos$$ - это имена функций, определенных в классе math.

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

В предыдущих главах все примеры использовали соответствующие методы test, являющиеся фактически процедурами и вызываемые как процедуры. Эти методы без входных параметров использовали в качестве входных данных локальные переменные, значения которых вычислялись непосредственно в теле функции или задавались пользователем с консоли. Результаты работы выводились на консоль вызовом функции print. В некоторых вариантах тестовых процедур входные данные брались из входного файла, а результаты записывались в выходной файл.

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

Описание классической функции. Вызов функции

Синтаксис описания функции в наиболее употребительном варианте достаточно прост:

def <имя функции> (<список имен аргументов>):
		<Операторы тела функции>

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

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

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

В отличие от привычного для меня стиля, когда заголовочный комментарий предшествует заголовку функции, в языке Pythonзаголовочный комментарий располагается сразу после заголовка функции (метода). Заголовочный комментарий является документированным комментарием и дает подсказку при вызове метода. Синтаксически заголовочный комментарий представляет текст, взятый в тройные кавычки, занимающий возможно несколько строчек.

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

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

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

$$P(x) = a_n * x^n + a_{n-1} * x^{n-1} + \dots + a_1 * x^1 + a_0$$

Входными параметрами (аргументами) являются два параметра - список коэффициентов полинома и число x. У функции один результат - значение полинома в точке x. Для вычисления результата будет использоваться классическая схема Горнера, требующая выполнения только n умножений и n сложений.

\def Gorner(a, x):
"""
    Вычисление полинома P(x) = Gorner(a, x)
    a - список коэффициентов полинома, первый в списке - старший коэффициент
    x - точка (число), в которой вычисляется значение полинома.
"""

    v = 0
    for coef in a:
        v = v * x + coef
    return v    

Выполним два вызова этой функции:

def test1():
    print("Polinom P(x) - Gorner Shema")
    p1 = Gorner([1, 2, 3], 2)
    print( "Gorner([1, 2, 3], 2) = ", p1)
    p2 = Gorner([3, 2, 1], 2)
    print( "Gorner([3, 2, 1], 2) = ", p2)
test1()

Вот результаты вычислений:

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

def Max_Min(a): 
"""
Вычисление максимального и минимального элементов списка a
"""
   
    max = min = a[0]
    for item in a:
        if item > max:
           max = item
        elif item < min:
           min = item
    res = [max, min]
    return res

Вызовем функцию на выполнение:

def test2():
    print ("Вычисление Max, Min списка ")
    a  = [5, 7, -2, 1, 8, 6]
    maxmin = Max_Min(a)
    print("max([5, 7, -2, 1, 8, 6]) = ", maxmin[0])
    print("min([5, 7, -2, 1, 8, 6]) = ", maxmin[1])
test2()

Результаты выполнения:

Аннотированный заголовок

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

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

Аннотированные заголовки для выше приведенных методов имеют вид:

def Gorner(a: list, x: float)->float:
def Max_Min(a: list)-> list: 
def test()->None:

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

def sum(a: int, b: int)->int :
    """ Возвращает сумму двух целочисленных операндов a и b
    """
    return a + b

Судя по заголовочному комментарию и аннотациям, функция sum вычисляет сумму двух целых чисел. Однако эта функция успешно работает для операндов самых разных типов, для которых определена операция "+". Операндами могут быть строки, списки, кортежи, а результатом является конкатенация операндов. Приведем тестовую процедуру:

def test0()->None:
    
    r = sum(5.5, 7.7)
    print (r)
    r = sum('a', 'b')
    print (r)
    r = sum([5, 5], [7, 7])
    print (r)
    r = sum(('five', 'seven'), ('one', 'two'))
    print (r)
    #r = sum({5, 7}, {1, 3})
    #print(r)
test0()

Результаты работы:

Как видите, функция, предназначенная для работы с целочисленными результатами, прекрасно работает и для операндов других типов, для которых определена операция сложения. Заметьте, два последних оператора в тестовой процедуре закомментированы, поскольку при попытке вызвать функцию с операндами типа "множество" (set) возникает ошибка периода выполнения. Конечно, ошибки на этапе выполнения - неприятные ошибки, гораздо лучше обнаруживать ошибки на этапе компиляции, что делается во многих языках программирования, допускающих статический контроль типов. Но создатели языка Python сознательно отказались от статического контроля типов в пользу имен, представляющих нетипизированные ссылки, - имен, которым можно присваивать объекты любых типов. Некоторое преимущество такого подхода демонстрирует метод sum, область применения которого гораздо шире в сравнении с ситуацией, когда формальные параметры имеют фиксированный тип.

Заметьте, при аннотировании типа формальных параметров и типа результата допустимо задать перечисление возможных типов. Так что определение функции sum может выглядеть так:

def sum(a: (int, float, str, list, tuple), b: (int, float, str, list, tuple))->(int, float, str, list, tuple) :
    """ Возвращает сумму двух операндов a и b
    """
    return a + b

Функции с переменным числом аргументов. Аргументы, имеющие значение по умолчанию

В приведенном выше синтаксисе заголовка функции списки формальных и фактических параметров должны соответствовать по числу параметров и их положению. При вызове функции k-му формальному параметру списка ставится в соответствие k- й фактический параметр. Язык Pythonпредлагает более гибкую стратегию соответствия списков фактических и формальных параметров. Во-первых, для некоторых параметров в момент объявления функции могут быть заданы значения по умолчанию. Для таких параметров в момент вызова фактические параметры могут не задаваться, если устраивают значения по умолчанию. Конечно, в момент вызова допускается изменение значений по умолчанию. Еще одно расширение связано с тем, что некоторому параметру может ставиться в соответствие не один фактический параметр, а целый кортеж. Понятно, что такой формальный параметр может существовать в единственном экземпляре.

Синтаксис заголовка функции в таком расширенном варианте выглядит следующим образом:

def <имя функции>(<список имен7gt;, *<имя>, <список имен со значениями по умолчанию>):

Список фактических параметров при вызове функции должен содержать не меньше элементов чем число элементов в первом списке имен. Пусть число формальных параметров в этом списке равно n, а после следует параметр со звездочкой. Пусть число фактических параметров равно m > n. Тогда размер кортежа фактических параметров, который ставится в соответствие параметру со звездочкой, равен m - n.

Для формальных параметров, входящих в список имен с заданными значениями по умолчанию, фактические параметры не задаются, если значения по умолчанию не требуют переопределения. Фактический параметр, имеющий форму: <имя> = <значение> при условии, что имя входит в список имен по умолчанию, заменяет значение, заданное по умолчанию, значением, заданным фактическим параметром.

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

def CreateList(n, *e, min = 0, max = 100):
    """
    Первоначально создается список из n элементов,
    заполненных случайными числами в диапазоне min ... max
    Затем список расширяется включением аргументов,
    число которых может быть произвольным.
    Значения аргументов min и max задаются по умолчанию
    """
    import random
    a = []
    for i in range(n):
        a.append(random.randint(min, max+1))
    for item in e:
        a.append(item)
    return a

Заметьте, параметр е, задаваемый со звездочкой, интерпретируется как коллекция элементов. Получить доступ ко всем фактическим параметрам, связанным с этим формальным параметром, можно итерированием по этой коллекции, используя оператор for.

Вот тест, в котором эта функция вызывается:

def test3():
   
    print ("Создание массива со случайными и неслучайными элементами")
    mas = CreateList(5, 77, 33, 11, min = -100)
    print("CreateList(5, 77, 33, 11, min = -100) = ", mas) 
test3()

Результаты работы:

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

import random
def CreateDepartment(p1, p2, p3, *p, director = "Висицкий", secretary = "Балабенкова"):
    """
    Формирование состава отдела
    p1, p2, p3 - фиксированные сотрудники
    *p - группа сотрудников произвольного размера
    director - директор - задан по умолчанию
    secretary - секретарь - задан по умолчанию
    """
    dep_state = []
    dep_state.append(p1)
    dep_state.append(p2)
    dep_state.append(p3)
    for item in p:
        dep_state.append(item)
    department = [director, secretary, dep_state]
    return department

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

def test4():
    #Случайный выбор сотрудников из фиксированного множества
    rand_set = ["Свердлов", "Суриков", "Тимофеев", "Фадеев", "Жукова", "Иванов"]
    p1 = rand_set[random.randint(0, 5)]
    rand_set.remove(p1)
    p2 = rand_set[random.randint(0, 4)]
    rand_set.remove(p2)
    p3 = rand_set[random.randint(0, 3)]
    rand_set.remove(p3)
    department = CreateDepartment(p1, p2, p3, "Седнев", "Ковальский", "Богун")
    print("директор = ", department[0])
    print("секретарь = ", department[1])
    print("сотрудники = ", department[2])
test4()

Результаты работы:

Функции, ссылки и объекты

Как уже говорилось, формальные параметры функции - это имена. Как всякие имена в Python, это ссылки, которые в момент присваивания связываются с объектом и получают его тип. Семантика замены формальных параметров (имен) на фактические аналогична семантике присваивания, так что при вызове функции имена формальных параметров связываются с объектами, заданными фактическими параметрами. При выполнении тела функции операции выполняются над объектами, заданными фактическими параметрами.

Что происходит, если в теле функции предпринимается попытка присвоить имени формального параметра некоторое значение? Изменяется ли объект, заданный фактическим параметром? Ответ - нет!

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

Рассмотрим пример:

def changeless(x):
    """
    Пример функции, изменяющей входной параметр
    """
    x = x + 2
    return 2 * x + 1

Рассмотрим тест, в котором вызывается эта функция:

def test5():
    y = 5
    print ("y = ", y )
    z = changeless(y)
    print ("y = ", y )
    print ("z = ", z )
test5()

Результаты работы:

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

Важно, однако, понимать, что утверждение о неизменности входных параметров - это лишь часть правды. Как утверждал поэт: "Не продается вдохновенье, но можно рукопись продать! " Изменить в теле функции связь между именем фактического параметра и объектом, с которым это имя связано, невозможно. Невозможно, чтобы в результате вызова функции имя указывало на другой объект. Но! можно изменить объект, связанный с этим именем. Такая ситуация встречается сплошь и рядом. В качестве примера рассмотрим функцию сортировки массива - пузырьковую сортировку списка.

def BubbleSort(ar):
    """
    Пузырьковая сортировка списка
    """

    n = len(ar) - 1   
    for i in range(n):
        for j in range(n, i, -1 ):
            if ar[j] < ar[j-1]:
                ar[j], ar[j-1] = ar[j-1], ar[j]
   return True 

Заметьте, функция возвращает формальный результат True. Главный эффект ее выполнения побочный - сортировка элементов списка, передаваемого функции в качестве аргумента. Функция не меняет ссылку на список, но изменяет список, сортируя его элементы.

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

def test6():
    ar = [5, 7, -3, 8, 16, 4]
    print ("ar before sorting :", ar)
    print("Вызывается как функция")
    p = BubbleSort(ar)
    print ("ar after sorting :", ar)
    ar = [15, 17, -13, 18, 16, 4]
    print ("ar before sorting :", ar)
    print("Вызывается как процедура")
    BubbleSort(ar)
    print ("ar after sorting :", ar)
test6()

Результат выполнения:

Вернуться к учебному плану