Программирование и научные вычисления на языке Python/§4

Термин функция в программировании означает несколько большее, чем в математике. Функция представляет законченную последовательность инструкций, которую вы можете вызывать в программе в любом месте и в любое время, когда программе известна ваша функция. Вы можете отсылать в функцию переменные, а функция в соответствии с инструкциями будет возвращать требуемое значение. Функции помогают избежать повторения одинаковых кусков программного кода, что в дальнейшем облегчает модифицирование программ. Функции также часто используются для разбиения программы на меньшие части так, чтобы более ясно понимать работу программы и допускать меньше ошибок. В Python имеется множество встроенных функций, что мы рассмотрели ранее (например, math.sqrt, range, len). Но вы и сами можете задавать свои собственные функции.

Функции одной переменной

править

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


def F(C):
    return (9.0/5)*C + 32


Задание функции в Python начинается со слова def, за ним следует имя функции и затем после двоеточия на новой строке с отступом начинаются инструкции функции. Слово return означает, что функция возвращает значение, которое находится после этого слова. Это значение связывается с именем функции. То есть передав в функцию аргумент (С), на выходе мы имеем значение, рассчитанное после слова return.

Строка с def, именем и аргументами функции называется заголовком функции, в то время как оставшаяся часть из самих инструкций - телом функции.

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


a = 10
F1 = F(a)
temp = F(15.5)
print F(a+1)
sum_temp = F(10) + F(20)


В нашем примере при вызове возвращается объект типа float, который может быть использован далее в любом месте кода, где может использоваться объект типа float. Например, в инструкции print. А вот так из списка градусов Цельсия можно создать соответствующий список по Фаренгейту:


Fdegrees = [F(C) for C in Cdegrees]


Немного поработав, получим и форматированный вывод, в котором уже возвращается объект строкового типа:


>>> def  F2(C):
...     F_value = (9.0/5)*C + 32
...     return '%.1f degrees Celsius  corresponds to '\
...            '%.1f degrees Fahrenheit' % (C, F_value)
...
>>> s1 = F2(21)
>>> s1
'21.0 degrees Celsius corresponds to 69.8 degrees Fahrenheit'

Локальные и глобальные переменные

править

Продолжим предыдущее рассмотрение в интерактивном режиме. Обратимся к функции F2(C), для которой F_value является локальной (local) переменной. Локальная переменная не может использоваться за пределами функции. Это легко продемонстрировать, продолжив:


>>> c1 = 37.5
>>> s2 = F2(c1)
>>> F_value
...
NameError: name 'F_value' is not defined


Точно так же локальной переменной является и аргумент функции C, который мы не можем использовать вне ее тела:


>>> C
...
NameError: name 'C' is not defined


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

>>> def F3(C):
	F_value = (9.0/5)*C + 32
	print 'Inside F3: C=%s F_value=%s r=%s' % (C, F_value, r)
	return '%.1f degrees Celsius corresponds to '\
	       '%.1f degrees Farenheit' % (C, F_value)

>>> C = 60
>>> r = 21
>>> s3 = F3(r)
Inside F3: C=21 F_value=69.8 r=21
>>> s3
'21.0 degrees Celsius corresponds to 69.8 degrees Farenheit'
>>> C
60


Этот пример иллюстрирует как одновременно используются две переменные с одним именем C: одна глобальная, определенная в самой программе значением 60 (int объектом), а другая локальная, представляющая собой аргумент функции F3. Значение последней, локальной переменной определяется передаваемым значением при вызове функции. Внутри F3 локальная переменная С "скрывает" глобальную переменную С.

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


print  sum  # sum встроенная функция
sum = 500   # sum ссылается на int
print sum   # sum глобальная переменная

def myfunc(n):
    sum = n + 1
    print sum  # sum локальная переменная
    return sum

sum = myfunc(2)+ 1  # новое значение глобальной переменной sum
print sum


В первой строке нет никаких локальных переменных, поэтому Python ищет значение sum как глобальной переменной, но ничего не находит и приступает к поиску среди встроенных функций, находит такую функцию и пишет что-нибудь вроде <built-in function sum>.

Во второй строке создается глобальная переменная sum соответствующая объекту типа int. Теперь, когда мы используем sum в инструкции print, Python уже находит одну переменную среди глобальных. При вызове myfunc(2) sum локальная переменная, поэтому инструкция print sum приводит к тому, что Python в первую очередь стремиться использовать локальную переменную, равную 3 (а не значение глобальной переменной, равное 500). В то же время кроме инструкции print, выполняется инструкция return и внешней глобальной переменной sum придается значение 4, которое мы и видим, когда выполняем последнюю строчку.

Значение глобальной переменной может быть задано и внутри функции, но не может быть изменено пока переменная обозначена как global:


a = 20; b = -2.5    # глобальные переменные

def f1(x):
    a = 21          # новая локальная переменная
    return a*x + b  # 21*x - 2.5

print a             # выводится 20

def  f2(x):
    global a
    a = 21          # глобальная a изменена
    return a*x + b  # 21*x - 2.5

f1(3); print a      # 20
f2(3); print a      # 21


Отметим что в функции f1, выражение a = 21 создает локальную переменную a. Хотя можно было бы подумать, что так вы изменяете глобальную переменную a, но как показывает опыт это не так. Намотайте это на ус, это частая причина ошибок.

Несколько аргументов

править

Предыдущие функции F(C) и F2(C) функции одной переменной, С, то есть функции принимают только один аргумент. Но вообще функции могут принимать сколь угодно много аргументов. Например, задачу из нашего первого урока в виде функции двух аргументов можно записать так:


def yfunc(t, v0):
    g = 9.81
    return v0*t - 0.5*g*t**2


Отметим, что g локальная переменная с фиксированным значением, в то время как t и v0 аргументы функции, также являющиеся ее локальными переменными. Как можно вызвать заданную функцию нескольких аргументов:


y = yfunc(0.1, 6)
y = yfunc(0.1, v0=6)
y = yfunc(t=0.1, v0=6)
y = yfunc(v0=6, t=0.1)


Возможность при вызове передавать аргументы в виде аргумент=значение делает чтение кода более простым для понимания. При этом, если использовать данную конструкцию для всех аргументов, можно не думать об их очередности. Заметим, что если данный синтаксис выполняется не для всех аргументов, то требуется соблюдать последовательность: вначале значения "безымянных" аргументов, потом в виде аргумент=значение. То есть вызов yfunc(t=0.1, 6) будет некорректным.

Независимо от того пишем ли мы yfunc(0.1, 6) или yfunc(v0=6, t=0.1), аргументы рассматриваются как локальные переменные подобно присваиванию переменным значений:


t = 0.1
v0 = 6


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

Можно указать на то, что координату y привычнее считать функцией времени t и писать y(t). Замечательно, что Python позволяет переписать код так, чтобы он выглядел подходящим образом:


def yfunc(t):
    g = 9.81
    return v0*t - 0.5*g*t**2


Главное отличие в том, что теперь v0 просто обязана быть глобальной переменной, иначе мы не можем вызвать yfunc:


>>> def yfunc(t):
...     g = 9.81
...     return v0*t - 0.5*g*t**2
...
>>> yfunc(0.6)
...
NameError: global name 'v0' is not defined


Решением служит предопределение v0 как глобальной переменной перед вызовом yfunc:


>>> v0 = 5
>>> yfunc(0.6)
1.2342


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


def makelist(start, stop, inc):
    value = start
    result = []
    while value <= stop:
        result.append(round(value, 1))  
        value = value + inc
    return result

mylist =  makelist(0, 100, 0.2)
print mylist  #  выведет 0, 0.2, 0.4, 0.6, ... 99.8, 100

Несколько возвращаемых значений

править

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


 .


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


def yfunc(t, v0):
    g = 9.81
    y = v0*t - 0.5*g*t**2
    dydt = v0 - g*t
    return  y, dydt


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


position, velocity = yfunc(0.6, 3)


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


t_values = [0.05*i for i in range(10)]
for t in t_values:
    pos, vel = yfunc(t, v0=5)
    print 't=%-10g position=%-10g velocity=%-10g' % (t, pos, vel)


Форматирование %-10g выводит числа в наиболее компактной форме (десятичного числа или научного представления) в поле из десяти знаков. Знак минус после процента означает что выравнивание в отведенном поле происходит по левому краю.


t=0         position=0          velocity=5
t=0.05      position=0.237737   velocity=4.5095
t=0.1       position=0.45095    velocity=4.019
t=0.15      position=0.639638   velocity=3.5285
t=0.2       position=0.8038     velocity=3.038
t=0.25      position=0.943437   velocity=2.5475
t=0.3       position=1.05855    velocity=2.057
t=0.35      position=1.14914    velocity=1.5665
t=0.4       position=1.2152     velocity=1.076
t=0.45      position=1.25674    velocity=0.5855


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


>>> def f(x):
...        return  x,  x**2,  x**4
...
>>> s = f(2)
>>> s
(2, 4, 16)
>>> type(s)
<type  'tuple'>
>>> x, x2, x4 = f(2)


Наш следующий пример — о функции суммы:


 


Можно показать, что L(x, n) аппроксимирует функцию ln(1+x) при конечном n и x ≥ 1. Для того чтобы записать сумму в программе, нам потребуется цикл и некоторая переменная в цикле, к которой будут добавляться новые члены. В общем случае когда слагаемые представляются как c(i), такую ситуацию можно реализовать так:


s = 0
for i in range (1, n+1):
    s += c(i)


В нашем конкретном случае c(i) представлено как (1/i)(x/(1 + x))i:


s = 0
for i in range (1, n+1):
    s += (1.0/i)*(x/(1.0+x))**i


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


def L(x, n):
    s = 0
    for i in range (1, n+1):
        s += (1.0/i)*(x/(1.0+x))**i
    return s


Поскольку заданная математическая функция L(x, n) аппроксимирует точную функцию ln(1+x), мы могли бы определить как это качественно происходит. Новая версия будет выглядеть так:


def L(x, n):
    s = 0
    for i in range (1, n+1):
        s += (1.0/i)*(x/(1.0+x))**i
    value_of_sum = s
    first_neglected_term = (1.0/(n+1))*(x/(1.0+x))**(n+1)
    from math import log
    exact_error = log(1+x) - value_of_sum
    return value_of_sum, first_neglected_term, exact_error

value, approximate_error, exact_error = L(x, 100)

Функции без ответа

править

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


def table(x):
    print '\nx=%g, ln(1+x)=%g' % (x, log(1+x))
    for n in [1, 2, 10, 100, 500]:
        value, next, error = L(x, n)
        print 'n=%-4d %-10g (next term: %8.2e  '\
            'error: %8.2e)' % (n, value, next, error)


Вызов функции:

table(10)
table(1000)

дает на выходе:

x=10, ln(1+x)=2.3979
n=1      0.909091       (next term: 4.13e-01  error: 1.49e+00)
n=2      1.32231        (next term: 2.50e-01  error: 1.08e+00)
n=10     2.17907        (next term: 3.19e-02  error: 2.19e-01)
n=100    2.39789        (next term: 6.53e-07  error: 6.59e-06)
n=500    2.3979         (next term: 3.65e-24  error: 6.22e-15)

x=1000, ln(1+x)=6.90875
n=1      0.999001       (next term: 4.99e-01   error:  5.91e+00)
n=2      1.498          (next term: 3.32e-01   error:  5.41e+00)
n=10     2.919          (next term: 8.99e-02   error:  3.99e+00)
n=100    5.08989        (next term: 8.95e-03   error:  1.82e+00)
n=500    6.34928        (next term: 1.21e-03   error:  5.59e-01)


Отсюда мы видим, что сумма сходится гораздо медленнее при больших x, чем при малых. Также мы видим, что начальные ошибки для малого n по порядку могут превышать само значение.

Когда мы явно не указываем инструкции return, это еще не означает, что таковой не имеется, в этом случае Python вставляет "невидимую" инструкцию return None. None — специальный объект Python, который представляет собой "ничто". Он преследует своим существованием те же цели, что в математике ноль обозначает отсутствие соответствующего количества единиц или десятков, сотен и так далее. В других языках программирования, таких как C, C++ и Java схожий смысл имеет слово void. Таким образом, функция table кроме того что выполняет записанные в ней инструкции, будет возвращать и объект особого типа, объект None. Например, при запросе result = table(500), переменная result будет ссылаться на объект None.

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


if obj is None:
   ...
if obj is not None:
   ...


Также можно использовать конструкцию obj == None, который сравнивает значения. Напомним, что оператор is определяет ссылаются ли имена на один и тот же объект:


>>> a = 1
>>> b = a
>>> a is  b  #  a и b ссылаются на один объект
True
>>> c = 1.0
>>> a is c
False
>>> a == c   # a и c математически равны
True


Аргументы по умолчанию

править

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

>>> def somefunc(arg1, arg2, kwarg1=True, kwarg2=0):
    print arg1, arg2, kwarg1, kwarg2


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


>>> somefunc('Hello', [1, 2])
Hello [1, 2] True 0
>>> somefunc('Hello', [1, 2], kwarg1='Hi')
Hello [1, 2] Hi 0
>>> somefunc('Hello', [1, 2], kwarg2='Hi')
Hello [1, 2] True Hi
>>> somefunc('Hello', [1, 2], kwarg2='Hi', kwarg1=6)
Hello [1, 2] 6 Hi


Последовательность для keywords не имеет значения. Можно даже и не думать о порядке и смешивать positional и keywords, если их всех при вызове записывать в формате имя=значение:


>>> somefunc(kwarg2='Hello', arg1='Hi', kwarg1=6,  arg2=[1, 2])
Hi [1,  2] 6 Hello


Пример

править

Рассмотрим функцию от t, которая также содержит некоторые параметры, а именно A, a и w :  . Мы можем сопоставить математической функции f функцию Python, в которой параметры A, a и w будут обладать некоторыми значениями по умолчанию:

from math import pi, exp, sin

def f(t, A=1, a=1, omega=2*pi):
     return A*exp(-a*t)*sin(omega*t)


Теперь мы можем вызывать функцию любым интересным нам способом:


v1 = f(0.2)
v2 = f(0.2, omega=1)
v3 = f(1, A=5, omega=pi, a=pi**2)
v4 = f(A=5, a=2, t=0.01, omega=0.1)
v5 = f(0.2, 0.5, 1, 1)


В последнем варианте показано, что keyword аргументы могут рассматриваться как positional, то есть естественно использовать их и без обозначения имен, но помня их очередность при определении функции.


Doc strings

править

В Python имеется договоренность о том, что строки документации (doc strings) вставляются сразу после заголовка функции. Doc strings содержат краткое описание цели функции и объясняют смысл аргументов и возвращаемых значений. Doc strings заключаются в тройные кавычки """, которые позволяют разбивать текст между ними в несколько строк. Вот два примера использования строк документации в функциях, короткий и длинный:


def C2F(C):
    """Convert Celsius degrees (C) to Fahrenheit."""
    return (9.0/5)*C + 32

def line(x0, y0, x1, y1):
    """
    Compute the  coefficients a and b in the mathematical
    expression for a straight line y = a*x + b that goes
    through two points (x0, y0) and (x1, y1).

    x0, y0: a point on the line (floats).
    x1, y1: another point on the line (floats).
    return: coefficients a, b (floats) for the  line  (y=a*x+b).
    """
    a = (y1 - y0)/float(x1 - x0)
    b = y0 - a*x0
    return a, b


Запомните, что строки документации должны располагаться до всех инструкций функции. Doc strings это не просто комментарии, они обладают большими возможностями. Например, для выше описанной функции line выполняется команда:


print line.__doc__


в результате которой (как вы можете проверить сами) выводится все заключенное содержимое.


Функции в качестве аргументов

править

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


 


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


def diff2(f, x, h=1E-6):
    r = (f(x-h) - 2*f(x) + f(x+h))/float(h*h)
    return r


Аргумент f здесь выступает как любой другой аргумент. Применение diff2 выглядит, например, так:


def g(t):
    return t**(-6)

t = 1.2
d2g = diff2(g, t)
print "g''(%f)=%f" % (t, d2g)

Lambda-функции

править

Существует однострочная конструкция, называемая lambda-функцией, которая иногда бывает очень удобна. Начнём с примера:


f = lambda x: x**2 + 4


то же самое, что


def f(x):
    return x**2 + 4


В общем случае любая конструкция вида


def g(arg1, arg2, arg3, ...):
    return expression


может быть записана как


g = lambda arg1, arg2, arg3, ...: expression


Lambda-функции часто используются для быстрого определения функции как аргумента другой функции, как, например, недавно для функции diff2 можно было определять дифференцируемую g(t) тут же на месте при вызове diff2:


d2 = diff2(lambda t: t**(-6), 1, h=1E-4)


Lambda-функции очень удобны для того, чтобы определять небольшие функции "на лету" и потому очень популярны среди многих программистов.

Чему мы научились

править

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