пятница, 12 сентября 2014 г.

Исследовательский конструктор. Часть II. Функции

ABSTRACT

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

Так как вышло довольно много кода сделал ipython notebook версию на своём канале в Github.




I. Необязательные параметры или параметры по умолчанию

В отличие от "жёстких" процедурных языков научного программирования, таких как fortran, в python функции могут иметь необязательные параметры. То есть описание функции может содержать 10 параметров, а для запроса достаточно указать только 2 или вообще ни одного. При этом у остальных параметров в определении функции должны быть указаны начальные значения. Выглядит это примерно так:

def my_func1(x,y,z=10):
    xyz = x*x + y*y - z
    return xyz

print my_func(2.5, -3.9)

Если сделать такой вариант,

def circ(R=6371, pin=3.14):
    z = 2*R*pin
    return z

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

c = circ()
print 'The circumference of the Earth is %.1f km-s' % (c)

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

def circ(R=6371, pin=3.14):
    z = 2*pin*R
    return z

Надо же! Ничего переделывать не пришлось! Нужно лишь по-другому обратиться к функции, а именно передать другой радиус:

c = circ(R=3397) # можно так
с = circ(3397) # а можно и вот так!
с = circ(pin=3.14, R=3397) # а можно ещё и так!
с = circ(3.14, 3397) # а вот так не получится! Ошибка!

print 'The circumference of the Mars is %.1f km-s' % (c)

Если не указать ключ (R=бла-бла-бла), то нужно вводить значения в том же порядке, что и указано в описании функции, то есть сначала радиус, а потом число Пи. Иначе параметры будут переданы неправильно, не в том порядке.
Если нужно больше чёткости и мы хотим обезопасить пользователя (и себя, конечно, тоже) от ошибок ввода или считаем, что пользователь забудет изменить радиус планеты и выдаст длину окружности Земли за длину окружности Марса, тогда надо делать так:

def circ(R, pin=3.14):
    z = 2*R*pin
    return z

Тогда при вызове функции нужно обязательно передать первый параметр R: либо по ключу "R=", либо указать значение первым в описании функции, когда мы хотим её вызвать:

с = circ(R=3397) # указали радиус Марса
с = circ(3397) # это тоже Марс
с = circ(pi=3.14, 3397) # а вот так не получится! Ошибка!

print 'The circumference of the Mars is %.1f km-s' % (c)

II. Строка комментария и описание назначения функции

Одно из главных правил хорошего программиста гласит, что после создания программы необходимо сделать к ней документацию. Некоторые люди, вообще-то, ценят своё время и не будут даже смотреть на ваш код, если нет описания к нему. Да и для самого творца назначение функции с названием "my_func" через месяц отпуска или недели работы над другим проектом становится малопонятным. А что уж говорить про соседа, пользователя или читателя блога? Наступает тяжкое, неблагодарное время, когда нужно стиснув зубы разбираться в том, как работает конкретная функция. Это лишние десятки минут, а если программа или функция сложные, то время на её разбор может занять целые дни! И все эти усилия будут направлены по сути лишь на поиск ответа на скромный вопрос: а для моей задачи, проекта, это программа/функция вообще нужна?
Поэтому будем стремиться к тому, чтобы КАЖДАЯ наша функция имела описание. Пусть маленькое, в одну строчку, но оно должно быть. Плюс, нужны метаданные, то есть описание того, какие данные, какого типа, являются входными, сколько обязательных параметров и какие варианты предлагаются для управляющих параметров (об управляющих параметрах подробнее будет далее). Это ОЧЕНЬ ВАЖНО, ведь описание типа "Эта функция вычисляет среднюю температуру воздуха на Земном шаре" совсем не помогает, когда нужно определить тип и формат входящих данных, которые у вас имеются.
Таким образом, будем ориентироваться на следующую структуру описания функции/программы:
а) Общее описание: цель работы функции
б) метаданные: входящие, исходящие данные, их типы, и др.
Перейдём к практике. В python у функций есть строка комментария. Это первая строка после описания функции (def my_func(x,y,z)). Эта строка помещается в многострочный комментарий (три апострофа - одиночных или двойных без разницы) и может быть вызвана следующим образом:

def my_func():
    ''' This string is a comment '''
    pass # ничего не делает, но без него тело функции будет пустым

print(my_func.__doc__)

Если вы работаете в редакторе spyder, то в окне object inspector будет видна именно строка документации. Если взглянуть на описания функций пакета numpy, то будет видно насколько информативны и выразительны бывают строки комментария. Для настройки строки комментария нужно использовать специальный облегчённый язык разметки текста, который называется Markdown. Википедия поможет разобраться в его основах.
пример разметки строки комментария на языке Markdown:

    def circ(R, pin=3.14):
    ''' 
    Subroutine computes the planet circumference with given radii \n
    
    **INPUT:** \n
           `R [float]` - planet radii  \n
           `pin [float]` - pi number. Initial pin=3.14 \n 
    \v
    **OUTPUT:** \n
            `z [float]` -  the planet circumference \n

    **Author:** Pavel Shabanov \n
    Blog: http://geofortran.blogspot.ru \n
    '''
    z = 2*R*pin
    return z

Набрав в console или IPython console circ( (открытая скобка важна!) для этого файла (возможно потребуется запуск программы) , в object inspector будет видна созданная строка комментария из которой ясно:

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

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

III. Настройка пользовательского вывода функций

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

    def circ(R, pin=3.14, out='R'):
    ''' 
    Subroutine computes the planet circumference with given radii \n
    
    **INPUT:** \n
           `R [float]` - planet radii  \n
           `pin [float]` - pi number. Initial pin=3.14 \n 
           `out [str]` - output parameter. Initial out='R' \n 
    \v
    **OUTPUT:** \n
            out='R': `z [float]` -  the surface area of a planet \n
    \v
            out='RV': 1. `z [float]` -  the planet circumference  \n
                           2. `v [float]` -  the surface area of a planet \n
    \v
            out='V': `v [float]` -  the surface area of a planet \n

    **Author:** Pavel Shabanov \n
    Blog: http://geofortran.blogspot.ru \n
    '''
    z = 2*R*pin
    if (out == 'R'):   
        return z
    elif (out == 'RV'):
        v = 4*R*R*pin
        return z, v 
    elif (out == 'V'):
        return 4*R*R*pin 
    else:
        return z

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

R, V = circ(R=3397, out='V') # ошибка (в выводе стоит один параметр - V (см. описание), а                                                              запросили два - R и V)
print type(RV), RV

или

RV = circ(R=3397, out='RV') # RV - не 2 числа, а кортеж чисел R и V
print type(RV), RV

IV. Заключение

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


Как перевести UV в направление и скорость ветра? How to convert wind UV-components to direction and velocity?

 Всё просто.  def uv2dir(u, v):     '''     Источник:     https://github.com/blaylockbk/Ute_WRF/blob/master/functions/wind_calc...