Автоматизирай Това Начало на книгата

Функции

В предишните глави се обсъдиха функциите print(), input() и len(). Python предоставя и други вградени функции, но могат да се пишат и собствени функции. Функцията е като минипрограма в програмата.

Следва пример за по-лесно разбиране как работят функциите. Кодът на програмата helloFunc.py :

➊ def hello():
    ➋ print('Здравей!')
    print('Здравей!!!')
    print('Как си ?')

➌ hello()
hello()
hello()

Първият ред е конструкция def ➊, който дефинира функция с име hello(). Следва блок от код ➋, който е тялото на функцията. Този код се изпълнява при извикване на функцията, а не при дефинирането ѝ.

Редовете hello() ➌ са извиквания на функцията. В кода функцията се извиква с написване на името ѝ, последвано от скоби (евентуално може да има аргументи между скобите). Когато достигне до извикване на функция, изпълнението на програмата се прехвърля към първия ред на функцията и започва да изпълнява инструкциите там. Когато достигне до края функцията, изпълнението се връща до реда, в който е извикването, и продължава надолу.

Тази програма извиква hello() три пъти, така че кодът на функцията hello() се изпълнява три пъти. При изпълнението на програмата се получава следното :

Здравей!
Здравей!!!
Как си ?
Здравей!
Здравей!!!
Как си ?
Здравей!
Здравей!!!
Как си ?

Основна цел на функциите е да групират код, който ще се изпълнява многократно. Без дефиниране на функции ще трябва един и същи код да се копира на няколко места и програмата ще изглежда така :

print('Здравей!')
print('Здравей!!!')
print('Как си ?')
print('Здравей!')
print('Здравей!!!')
print('Как си ?')
print('Здравей!')
print('Здравей!!!')
print('Как си ?')

Принципно повторението на код трябва да се избягва, защото при необходимост от промяна (например за оправяне на грешка), кодът трябва да се промени навсякъде, където е копиран.

С придобиването на опит често се прави изнасяне на код - премахването на повторяем или копиран код. Изнасянето на код прави програмите по-кратки, по-лесни за четене и по-лесни за променяне.

def конструкция с параметри

При извикване на функцията print() или len() се подават стойности, наричани аргументи. Аргументите се подават, като се пишат между скобите на извикването. Потребителските функции също могат да приемат аргументи. Ето примерна програма, записана във файл helloFunc2.py :

➊ def hello(name):
    ➋ print('Здравей, ' + name)

➌ hello('Ана')
hello('Боби')

При изпълнението на програмата се получава следното :

Здравей, Ана
Здравей, Боби

В тази програма дефиницията на функцията hello() съдържа параметър, с име name ➊. Параметрите са променливи, които приемат аргументите. Функцията се извиква с аргументи, които се запазват в параметрите. Първото извикване на функцията hello() е с аргумент 'Ана' ➌. Изпълнението на програмата влиза във функцията и параметърът name автоматично получава стойността 'Ана', която се принтира от израза print() ➋.

Нещо специално за отбелязване за параметрите е ограниченият им живот. Ако се добави print('name') след реда hello('Боби') програмата ще даде NameError, защото няма променлива, наречена name. Променливата се унищожава след връщането от извикване hello('Боби'), така че print(name) ще използва променлива name, която не съществува.

По подобен начин променливите на програмата се забравят след приключването ѝ. По надолу е обяснено защо е така.

Дефиниране, Извикване, Подаване, Аргумент, Параметър

Термините дефиниране, извикване, аргумент и параметър могат да предизвикат объркване. Следващият пример служи за изясняването им :

➊ def sayHello(name):
    print('Hello, ' + name)
➋ sayHello('Станимир')

Дефинирането на функция я създава, както присвояването на стойност spam = 42 създава променливата spam. Конструкцията def дефинира функцията sayHello() ➊. Редът sayHello('Станимир')извиква вече създадената функция, като изпълнението продължава в началото на тялото на функцията. При извикването на функцията се подава низовата стойност 'Станимир'. Аргумент е стойността, която се подава на функция при извикването ѝ. Аргументът 'Станимир' се присвоява на локалната променлива с име name. Параметри са променливите, на които се присвоява стойността на аргументите.

Използването на правилните термини улеснява разбирането на техническите текстове, както и по-добрата комуникация между програмистите.

Връщане на стойност и конструкция return

Ако се извика функцията len() с аргумент 'Здравей', тя се изчислава до цялото число 7, което е дължината на низа. Стойността, до която се изчислава дадено извикване на функция, се нарича върнатата стойност на функцията.

При създаване на функция с конструкция def върнатата стойност се определя с конструкция return. Конструкцията return се състои от :

  • Запазената дума return
  • Стойността (или израз), който функцията връща

Ако в return конструкцията се използва израз, върнатата стойност е равна на резултатът от пресмятането на израза.

Следващата програма (magic8Ball.py) дефинира функция, която връща различен низ, в зависимост от подадено като аргумент число :

➊ import random

➋ def getAnswer(answerNumber):
    ➌ if answerNumber == 1:
        return 'Със сигурност'
    if answerNumber == 2:
        return 'Така е'
    if answerNumber == 3:
        return 'Да'
    if answerNumber == 4:
        return 'Неясно, опитай отново'
    if answerNumber == 5:
        return 'Питай по-късно'
    if answerNumber == 6:
        return 'Концентрирай се и питай пак'
    if answerNumber == 7:
        return 'Отговорът е - не'
    if answerNumber == 8:
        return 'Перспективата не е добра'
    if answerNumber == 9:
        return 'Много съмнително'

➍ r = random.randint(1, 9)
➎ fortune = getAnswer(r)
➏ print(fortune)

Първото нещо след стартиране на програмата е импортиране на модула random ➊. След това се дефинира функцията getAnswer() ➋. Тук функцията само се дефинира (без да се извиква), така че тялото ѝ не се изпълнява. После се извиква функцията random.randint() с два аргумента: 1 и 9 ➍. Това се изчислява до случайно число между 1 и 9, включително. Тази стойност се записва в променливата с име r.

Функцията getAnswer() се извиква с r за аргумент ➎. Изпълнението на програмата продължава от началото на функцията getAnswer() ➌, като стойността r се записва в параметър с име answerNumber. Там функцията връща една от няколкото възможни стойности, според стойността в answerNumber. Изпълнението на програмата се връща до реда, в който е извикана функцията getAnswer() ➎. Върнатият низ се присвоява на променливата fortune, която след това се подава при извикването на print() ➏ и се показва на екрана.

Тъй като върнатата стойност може да се подаде на друга функция, тези три реда:

r = random.randint(1, 9)
fortune = getAnswer(r)
print(fortune)

могат да се съкратят до един със същото действие:

print(getAnswer(random.randint(1, 9)))

Трябва да се помни, че изразите са съставени от стойности и оператори. В израз може да се използва и извикване на функция, защото то се пресмята до върнатата стойност.

Стойността None

В Python има специална стойност None, която преставлява липса на стойност. Стойността None е единствената възможна стойност на типа данни NoneType. (В някои други езици за програмиране тази стойност може да се нарича null, nil или undefined.) None стойността трябва да се изписва с главно N, подобно на булевите стойности True и False.

Тази "стойност без стойност" може да е полезна, ако се налага в променлива да се запише нещо, което да не се обърка с истинска стойност. Например print() връща стойност None. Функцията print() показва текст на екрана, но не трябва да връща резултат, за разлика от len() или input(). Но всички функции трябва да върнат стойност, така че print() връща None. Кратко потвърждение с код в интерактивната конзола :

>>> spam = print('Здравей!')
Здравей!
>>> None == spam
True

Зад завесата Python добавя return None в края на всяка функция, която няма return. Това е подобно на невидимото continue, който е в края на тялото на while или for цикъл. Също така None се връща и при използване на return без стойност (тоест - само запазената дума return).

Аргументи с име и функцията print()

Повечето аргументи се разпознават по тяхната позиция при извикването на функцията. Например random.randint(1, 10) е различно от random.randint(10, 1). Извикването на функцията random.randint(1, 10) ще върне случайно число между 1 и 10, защото първият аргумент е долната граница на интервала, а вторият аргумент е горната граница. В същото време random.randint(10, 1) води до грешка.

Именуваните аргументи не се разпознават по тяхната позиция, а по думата, която се поставя пред тях при извикването на функция. Аргументи с име често се използват за незадължителни параметри. Пример е функцията print(), която има незадължителни параметри end и sep. Те служат за задаване на низ, който да се добави след аргументите и на низ, който се добавя между аргументите (от sep - separator - разделител).

Ако има следната програма:

print('Здравей')
print('Свят')

тя ще принтира :

Здравей
Свят

Двата принтирани стринга са на отделни редове, защото функцията print() автоматично добавя нов ред към низа, който ѝ се подава. Това може да се промени с използване на именувания аргумент end, за който може да се използва произволен низ. За пример - малка промяна в предния код :

print('Здравей', end='')
print('Свят')

ще доведе до следния резултат :

ЗдравейСвят

Резултатът е само на един ред, защото вече няма добавен нов ред след принтирането на 'Здравей'. Вместо това се принтира празен низ. Това е полезно ако е необходимо да не се добавя нов ред след всяко извикване на функцията print().

По подобен начин, при подаване на няколко стойности на print(), функцията автоматично ги разделя с по един интервал. Ето пример от интерактивната конзола :

>>> print('котки', 'кучета', 'мишки')
котки кучета мишки

Стойността на разделителя може да се промени с подаване на друг низ за именуван аргумент sep. В интерактивната конзола :

>>> print('котки', 'кучета', 'мишки', sep=',')
котки,кучета,мишки

Именувани аргументи могат да се добавят при дефинирането на функция. Преди това обаче трябва да се научат типовете данни списък и речник от следващите глави. За сега е достатъчно да се знае, че някои функции имат незадължителни аргументи, които могат да се използват при извикване на функцията.

Стек на извикванията (call stack)

Нека си представим продължителен разговор с някого. В началото се говори за общия познат Ангел, който напомня за случка с колегата Боби, но за нейното обяснение трябва да се спомене братовчеда Краси. Приключва се историята с Краси и се връща на разговора за Боби, а след случката с Боби разговора се връща на Ангел. Но тогава се отплесва в разговор за Добри, така че първо се разказва за него, след това се довършва първоначалния разказ за Ангел. Този разговор следва структура подобна на стек. Разговорът прилича на стек, защото текущата тема е винаги отгоре в стека :

Стекът на разговора
Стекът на разговора

Подобно на този разговор, извикването на функция не изпраща изпълнението еднопосочно към началото на функцията. Python запомня в кой ред е извикана функцията, така че изпълнението може да се върне обратно при достигане на return. Ако тази функция извиква други функции, изпълнението се връща от тях към нея, преди да се върне към мястото, където е извикана тя.

Следва примерна програма, записана като abcdCallStack.py:

def a():
    print('a() започва')
    ➊ b()
    ➋ d()
    print('a() завършва')

def b():
    print('b() започва')
    ➌ c()
    print('b() завършва')

def c():
    ➍ print('c() започва')
    print('c() завършва')

def d():
    print('d() започва')
    print('d() завършва')

➎ a()

При изпълнение на програмата се получава следния резултат:

a() започва
b() започва
c() започва
c() завършва
b() завършва
d() започва
d() завършва
a() завършва

Извиква се функцията a() ➎, която извиква b() ➊, която на свой ред вика c() ➌. Функцията c() не вика нищо, тя просто принтира c() започва ➍ и c() завършва, преди изпълнението да се върне до реда в b(), който я е извикал ➌. След края на b() изпълнението се връща в реда от a(), който е извикал b(). Изпълнението продължава със следващия ред от функцията a() ➋, където се извиква d(). Подобно на функцията c(), функцията d() не извиква друга функция. Само се принтира d() започва и d() завършва и изпълнението се връща в реда от a(), където се извиква d(). Следващият ред от a() принтира a() завършва и изпълнението се връща в реда на първоначалното извиква на a() в програмата ➎.

Стекът на извикванията (call stack) се използва от Python за запомняне къде трябва да се върне изпълнението след извикване на функция. Стекът не се пази в променлива от програмата, а Python го използва зад кулисите. При всяко извикване на функция Python създава нов обект (frame object, активационен запис) и го добавя в стека. Тези обекти съдържат номер на ред, в който функцията е извикана, за да може Python да се върне. Python добавя нов обект в стека при всяко извикване на функция.

При приключване на функция Python премахва най-горния обект от стека и премества изпълнението до реда, който е записан в обекта. Обектите винаги се добавят и премахват само от горния край на стека. Следващото изображение показва състоянието на стека на извикванията в abcdCallStack.py след всяко извикване и връщане от функция.

Стекът на програмата abcdCallStack.py
Обектите в стека на извикванията при изпълнение на програмата abcdCallStack.py

Най-отгоре в стека се намира функцията, която се изпълнява в момента. Стекът е празен, когато се изпълява ред извън функция.

Стекът на извикванията е техническа подробност, чието познаване не е необходимо за писането на програми. Достатъчно е да се разбере, че извикването на функция след това се връща до реда, от който е извикана. Все пак разбирането на стека на извикванията спомага за по-лесно усвояване на локалния и глобалния обхват, които са описани в следващата секция.

Локален и глобален обхват

Параметрите и променливите, които се присвояват във функция, съществуват в локалния обхват (local scope) на функцията. В глобалния обхват (global scope) съществуват променливите, които получават стойност извън функциите. Променлива, която съществува в локален обхват се нарича локална променлива (local variable), докато променлива, съществуваща в глобалния обхват, се нарича глобална променлива (global variable). Всяка променлива може да е само един от двата типа, не може да е едновременно локална и глобална.

Обхватът може да се разглежда като контайнер за променливите. Всички променливи и стойности, дефинирани в даден обхват, се забравят при излизане от него. Има само един глобален обхват, който се създава при стартиране на програмата. Глобалният обхват се унищожава при прекратяване на програмата и всички негови променливи се забравят. Така променливите не помнят стари стойности при ново стартиране на програмата.

Локален обхват се създава при всяко извикване на функция. Всички променливи, дефинирани във функцията, съществуват в локалния обхват на функцията. Локалният обхват се унищожава при връщане от функцията и локалните променливи се забравят. При следващо извикване на функцията локалните променливи няма да помнят стойностите си, записани при предишното извикване. Локалните променливи също се записват в обектите, които се добавят в стека на извикванията.

Обхватите имат значение по няколко причини :

  • Кодът в глобалния обхват (извън всички функции) не може да използва нито една от локалните променливи.
  • Но код в локален обхват може да достъпи глобалните променливи.
  • Код в локалния обхват на една функция не може да използва променливи от друг локален обхват.
  • Може да има различни променливи с еднакви имена, ако са в различни обхвати. Тоест може да има локална променлива с име spam и глобална променлива с име spam.

Целта на отделните обхвати е ограничаване на взаимодействието между функциите само до параметрите и върнатата стойност. Така се намалява броят редове, които могат да причинят бъг. Ако в програма се използват само глобални променливи и има бъг, причинен от записване на грешна стойност в променлива, ще е трудно да се определи къде е била присвоена тази грешна стойност. Тя може да е присвоена навсякъде в програмата, която може да е стотици или хиляди редове. Но ако грешната стойност е присвоена на локална променлива, трябва да се провери само кода на функцията.

Използването на глобални променливи е допустимо в малки програми, но е лош навик.

Локалните променливи не могат да се използват в глобалния обхват

Ето една програма, която води до грешка при стартирането ѝ :

def spam():
    ➊ eggs = 31337
spam()
print(eggs)

При стартиране на програмата се изписва нещо подобно на:

Traceback (most recent call last)
     File "test1.py", line 4, in <module>
        print(eggs)
NameError: name 'eggs' is not defined

Грешката се получава, защото променливата eggs съществува само в локалния обхват, създаден при извикване на spam() ➊. Локалният обхват на spam се унищожава след връщане от функцията и вече няма променлива с име eggs. За това Python дава грешка при опит за използване на променливата в print(eggs). Това се обяснява лесно - няма локални обхвати, докато се изпълнява код в глобалния обхват. По тази причина в глобалния обхват могат да се използват само глобални променливи.

В локален обхват не могат да се използват променливи от други локални обхвати

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

def spam():
    ➊ eggs = 99
    ➋ bacon()
    ➌ print(eggs)

def bacon():
    ham = 101
    ➍ eggs = 0

➎ spam()

При стартиране на програмата се извиква функцията spam() ➎ и се създава локален обхват. На локалната променлива eggs ➊ се присвоява 99. След това се извиква функцията bacon() ➋, което създава втори локален обхват. По едно и също време могат да съществуват няколко локални обхвата. В този нов локален обхват се присвоява 101 на локалната променлива ham, а локална променлива eggs - която е различна от променливата в локалния обхват на spam() - се създава ➍ със стойност 0.

При връщането от bacon() се унищожава локалният ѝ обхват, включително и нейната променлива eggs. Изпълнението продължава във функцията spam() с принтиране на стойността на eggs ➌. Локалния обхват на spam() все още съществува, така че променливата eggs е тази, която има стойност 99. Тази стойност се принтира от програмата.

Следствие на локалния обхват е пълното разделяне на локалните променливи на различните функции.

Глобалните Променливи могат да се прочетат в локален обхват

Кратка примерна програма:

def spam():
    print(eggs)

eggs = 42
spam()
print(eggs)

Функцията spam() няма параметър с име eggs, няма и код, който да присвоява стойност на променлива eggs, така че Python използва глобалната променлива eggs. По тази причина горната програма принтира два пъти 42.

Локални и глобална променлива с еднакво име

Няма технически пречки в Python да се използва едно и също име за глобална променлива и локални променливи в различни обхвати. Въпреки това силно се препоръчва избягването на преизползване на име. Следва примерната програма localGlobalSameName.py:

def spam():
    ➊ eggs = 'spam локална'
    print(eggs) # принтира 'spam локална'

def bacon():
    ➋ eggs = 'bacon локална'
    print(eggs) # принтира 'bacon локална'
    spam()
    print(eggs) # принтира 'bacon локална'

➌ eggs = 'глобална'
bacon()
print(eggs) # принтира 'глобална'

При изпълнение на програмата се вижда следното:

bacon локална
spam локална
bacon локална
глобална

В тази програма има три различни променливи, които объркващо са с едно и също име - eggs. Променливите са :

➊ Променлива с име eggs, съществуваща в локален обхват при извикване на spam()

➋ Променлива с име eggs, съществуваща в локален обхват при извикване на bacon()

➌ Променлива с име eggs, съществуваща в глобалния обхват

Използването на едно и също име за три различни променливи води до объркване коя точно се използва в даден момент. Това е добра причина за избягването на еднаквите имена в различни обхвати.

Конструкцията global

Конструкцията global се използва при необходимост от модифициране на глобална променлива в тялото на функция. В началото на функцията се пише ред като global eggs. Той казва на Python "В тази функция под eggs се има предвид глобалната променлива, така че не създавай локална променлива с това име.". Следва кратка примерна програма, записана като globalStatement.py:

def spam():
    ➊ global eggs
    ➋ eggs = 'променена в spam()'

eggs = 'глобална'
spam()
print(eggs)

При изпълнение на програмата се принтира следното :

променена в spam()

eggs е декларирана като глобална (global) в началото на spam() ➊, така че при присвояване на стойност 'променена в spam()' ➋, тази стойност се дава на eggs в глобалния обхват. Не се създава локална променлива eggs.

Има четири правила, по които да се определи дали променлива е в локален или глобален обхват:

  • Ако променлива се използва в глобалния обхват (извън функциите) - винаги е глобална променлива.
  • Ако има global конструкция за тази променлива във функция - глобална променлива.
  • Ако няма global, но във функцията се присвоява стойност на променливата - локална променлива.
  • Ако на променливата не се присвоява стойност - глобална променлива.

Следва примерната програмата sameNameLocalGlobal.py, която демонстрира тези правила:

def spam():
    ➊ global eggs
    eggs = 'spam' # това е глобалната променлива

def bacon():
    ➋ eggs = 'bacon' # това е локална

def ham():
    ➌ print(eggs) # това е глобалната

eggs = 42 # това е глобалната
spam()
print(eggs)

Във функцията spam() eggs е глобалната променлива eggs, защото в началото на функцията има global eggs ➊. В bacon() eggs е локална променлива, защото там ѝ се присвоява стойност ➋. В ham()eggs е глобалната променлива, защото нито ѝ се присвоява стойност, нито има global за нея. При изпълнението на програмата се вижда следното:

spam

В дадена функция една променлива може да или само глобална, или само локална. Не може кодът на функция да използва локална променлива eggs и после да използва глобалната променлива eggs в същата функция.

БЕЛЕЖКА

За модифицирането на глобална променлива в тялото на функция трябва да се използва global за тази променлива.

Python дава грешка, ако се използва локална променлива, преди да ѝ е дадена стойност. За демонстрация следва програма sameNameError.py:

def spam():
    print(eggs) # ГРЕШКА!
    ➊ eggs = 'spam локална'

➋ eggs = 'глобална'
spam()

При стартиране на програмата се вижда следното съобщение за грешка :

Traceback (most recent call last):
    File "sameNameError.py", line 6, in <module>
        spam()
    File "sameNameError.py", line 6, in <module>
        print(eggs) # ГРЕШКА!
UnboundLocalError: local variable 'eggs' referenced before assignment

Тази грешка се получава, защото Python вижда присвояването на стойност на eggs ➊ и я приема за локална променлива. Но print(eggs) се изпълнява преди eggs да има стойност, така че в този момент локална променлива eggs не съществува. Python не опитва да използва глобалната променлива eggs ➋, защото има локална със същото име.

Обработка на изключения

В досегашните Python програми получаването на грешка, наричана още изключение (exception), води до преждевременно приключване на цялата програма. Това не е желано поведение на истинска програма. Много по-добре е програмата да засече грешката, да я обработи и да продължи.

Следва примерна програма, наречена zeroDivide.py, в която има грешка "делене на нула":

def spam(divideBy):
    return 42 / divideBy

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

Дефинирана е функция с име spam, с един параметър. След това се принтира резултатът от извикване на функцията с различни параметри. При изпълнение на програмата се вижда следното:

21.0
3.5
Traceback (most recent call last):
    File "zeroDivide.py", line 6, in <module>
        print(spam(0))
    File "zeroDivide.py", line 2, in spam
        return 42 / divideBy
ZeroDivisionError: division by zero

Грешката ZeroDivisionError се получава при опит за делене на нула. В съобщението за грешка се дава номер на ред, за да се разбере откъде идва грешката. В случая грешката е в израза return в spam().

Грешките могат да се обработват с конструкцията try-except. В тялото на try се слага кодът, който може да доведе до грешка. При възникване на грешка изпълнението продължава с първия ред на except клаузата.

Може кодът, който дели на нула, да се сложи в тялото на try и да се добави except тяло, което да обработи грешката.

def spam(divideBy):
    try:
        return 42 / divideBy
    except ZeroDivisionError:
        print('Грешка : Невалиден аргумент.')

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

При достигане на грешка в код, който е в тялото на try, изпълнението на програмата веднага се прехвърля в тялото на except. Изпълнението продължава нормално след изпълнението на тялото на except. Това води до следния резултат :

21.0
3.5
Грешка : Невалиден аргумент.
None
42.0

try прихваща и грешките, които се получават в извикани функции. В следващата програма извикванията на spam() са в try блок:

def spam(divideBy):
    return 42 / divideBy

try
    print(spam(2))
    print(spam(12))
    print(spam(0))
    print(spam(1))
except ZeroDivisionError:
    print('Грешка : Невалиден аргумент.')

Тази програма дава следния резултат:

21.0
3.5
Грешка : Невалиден аргумент.

print(spam(1)) не се изпълнява, защото предния ред print(spam(0)) предизвиква изключение, което се прихваща и изпълнението продължава с тялото на except. След изпълнение на тялото на except програмата продължава надолу, а не се връща до реда, където е предизвикана грешката.

Кратка програма : Зигзаг

Следва демонстрация на научените програмни идеи - създаване на малка анимираща програма. Програмата ще пресъздава движение напред-назад, докато потребителят не я спре от редактора или с натискане на CТRL-C. При стартиране на програмата има резултат, приличащ на :

    ********
   ********
  ********
 ********
********
 ********
  ********
   ********
    ********

Ето и кода на програмата, записан като zigzag.py:

import time, sys
indent = 0 # брой интервали, с които да се отмести шаблона
indentIncreasing = True # Дали отместването се увеличава или не

try:
    while True: # основният цикъл на програмата
        print(' ' * indent, end='')
        print('********') # шаблон, който се мести
        time.sleep(0.1) # пауза за една десета от секундата

        if indentIncreasing:
            # Увеличаване на броя интервали
            indent = indent + 1
            if indent == 20:
                # Смяна на посоката
                indentIncreasing = False

        else:
            # Намаляване на броя интервали
            indent = indent - 1
            if indent == 0:
                # Смяна на посоката
                indentIncreasing = True
except KeyboardInterrupt:
    sys.exit()

Анализ на програмата ред по ред, започвайки от началото.

import time, sys
indent = 0 # брой интервали, с които да се отмести шаблона
indentIncreasing = True # Дали отместването се увеличава или не

Първо се импортират модулите time и sys. След това се дефинират две променливи. Променливата indent пази броят на интервалите, които се добавят пред шаблона от осем звездички. Другата променлива, indentIncreasing, съдържа булева стойност, по която се определя дали количеството интервали се увеличава или намалява.

try:
    while True: # основният цикъл на програмата
        print(' ' * indent, end='')
        print('********') # шаблон, който се мести
        time.sleep(0.1) # пауза за една десета от секундата

Цялата останала програма е в тялото на try конструкция. Python предизвиква KeyboardInterrupt изключение при натискане на CTRL-C. Ако липсва try-except конструкция, която да прихване това изключение, програмата ще приключи преждевременно с грозно съобщение за грешка. В тази програма обаче, изключението KeyboardInterrupt се обработва чисто с извикване на sys.exit() (кодът за обработване е в тялото на except в края на програмата).

Безкрайният цикъл while True: постоянно ще повтаря инструкциите в програмата. Инстрункциите включват принтирането на правилния брой интервали с ' ' * indent. След интервалите не трябва автоматично да се добавя нов ред, така че се подава и end='' към първото извикване на print(). Второто извикване на print() принтира групата от звездички. Функцията time.sleep() не е показвана до сега, но е достатъчно да се знае, че тя ще добави пауза от една десета секунда в програмата.

        if indentIncreasing:
            # Увеличаване на броя интервали
            indent = indent + 1
            if indent == 20:
                # Смяна на посоката
                indentIncreasing = False

След това се променя големината на отместването, на което следващия път ще се принтират звездичките. Ако indentIncreasing е True се добавя единица към indent. Отместването започва да намалява, когато достигне 20.

        else:
            # Намаляване на броя интервали
            indent = indent - 1
            if indent == 0:
                # Смяна на посоката
                indentIncreasing = True

Стойността на indent се намаля с единица, ако indentIncreasing е False. Когато стойността на отместването стигне до нула започва пак да се увеличава. И в двата случая изпълнението на програмата се връща в началото на основния цикъл, където отново принтира звездичките.

except KeyboardInterrupt:
    sys.exit()

При натискане на CTRL-C се генерира изключение, което се прихваща от try-except конструкцията и изпълнението продължава с тялото на except. Там се изпълнява sys.exit(), с което програмата приключва. По този начин потребителят може да спре програмата, въпреки че основният цикъл е безкраен.

Обобщение

Функциите са основният начин за обособяване на кода в логически групи. Променливите във функциите съществуват само в локалния ѝ обхват, така че кода в една функция не може директно да променя стойности на променливи в други функции. Това ограничава количеството код, което може да променя променливите, което много улеснява оправянето на грешки (дебъгването) в кода.

Функциите са полезен инструмент за организиране на кода. Те могат да се възприемат като черни кутии - те имат вход, под формата на параметри, и изход, под формата на връщана стойност. Кодът в тях няма значение за останалите функции.

В предишните глави всяка грешка води до преждевременно приключване на програмата. В тази глава бяха въведени try и except, които позволяват засичане и обработване на грешки. Това прави програмите по-устойчиви на често срещаните грешки.

Упражнения

  1. Какви предимства дава използването на функции в програмата?
  2. Кога се изпълнява кода на функцията - когато се дефинира или когато се извиква?
  3. Как се създава функция?
  4. Каква е разликата между функция и извикване на функция?
  5. Колко глобални обхвата има в програма на Python? А колко локални?
  6. Какво стваа с променливите от локалния обхват след връщане от функцията?
  7. Какво е върната стойности? Може ли върнатата стойност да се използва в израз?
  8. Каква е върнатата стойност от функция, в която няма return?
  9. Как може да се принуди функция да използва глобална променлива?
  10. От кой тип данни е None?
  11. Какво е действието на реда import proizvolen?
  12. Как се извиква функция bacon() от модул spam (модулът spam е вече импортиран)?
  13. Как може да се предотврати преждевременно приключване на програмата при грешка?
  14. Какво има в тялото на try? Какво има в тялото на except?

Практически упражнения

Да се напишат програми, които изпълняват следващите задачи.

Поредица на Колац

Да се напише функция с име collatz(), която има еди параметър, наречен number. Ако number е четно, collatz() трябва да принтира number // 2 и връща тази стойност. Ако number е нечетно - collatz() трябва да принтира и върне 3 * number + 1.

След това да се напише програма, която иска от потребителя да въведе цяло число, след това вика collatz() с това число, докато функцията не върне 1. (Учудващо, тази поредица работи за всяко цяло число - рано или късно се стига до единица ! Дори математиците не са сигурни защо. Тази програма потвърждава хипотезата на Колац, понякога наричана "най-простият неразрешен математически проблем")

Въведената с input() стойност трябва да се преобразува до цяло число с функцията int(), защото връща низова стойност.

Подсказка: цяло число е четно, ако number % 2 == 0 и нечетно, ако number % 2 == 1.

Резултатът от програмата трябва да прилича на :

Въведи число:
3
10
5
16
8
4
2
1

Валидиране на входните данни

Към предишната програма да се добавят try и except за засичане на въвеждане на низ, различен от цяло число. Обикновено функцията int() предизвиква ValueError, ако ѝ е подаден низ, който не е цяло число, като int('куче'). В тялото на except на потребителя да се показва съобщение, че трябва да въведе цяло число.

Подкрепете автора чрез покупка на оригиналната книга от No Starch Press или Amazon.