Към съдържанието

4. Функции ​

Занятие 4упражнения: 04-functions/ в homework repositoryпримерните данни

Днес ще се научите да:

  • дефинирате собствени функции с def и да ги извиквате;
  • подавате стойности на функция: по позиция, по име, със стойности по подразбиране и в произволен брой с *args;
  • връщате един или няколко резултата с return - и да видите защо print не е return;
  • разбирате къде се вижда едно име (scope) и защо функцията трябва да получава данните си чрез параметри;
  • документирате функция с docstring и type hints;
  • подавате функции като стойности, да пишете кратки функции с lambda и да сортирате по ключ;
  • заменяте SAS macro с функция на Python и да изградите малка библиотека от функции за кредитния портфейл.

От SAS към Python - едно и също парче логика за многократна употреба, по два начина:

sas
%macro interest(outstanding, rate);
    (&outstanding * &rate / 100)
%mend;

data _null_;
    yearly = %interest(651927.30, 5.44);
    put yearly=;
run;
python
def interest(outstanding, rate):
    """The interest for one year: outstanding * rate / 100."""
    return outstanding * rate / 100


print(interest(651927.30, 5.44))
print(interest(1044231.30, 8.83))
Променете кода и го изпълнете отново

1. Какво е функция? ​

Функцията е именуван блок от код, който върши една задача. Дефинирате я веднъж и я извиквате - изпълнявате я - колкото пъти ви трябва, с различни стойности. Добра представа е рецепта в готварска книга: написана веднъж, сготвена много пъти, с различни количества.

Вече извиквате функции всеки ден: print(), len(), round(), sum(), sorted(), int(). Днес пишете свои.

Функциите помагат да:

  • разделите голяма задача на малки - всяка функция прави едно нещо и има име, което казва какво;
  • не се повтаряте - едно и също изчисление на едно място: поправка там го поправя навсякъде (правилото "DRY": don't repeat yourself);
  • тествате - функция с входни стойности и резултат се проверява лесно (така задачите в този курс проверяват работата ви).

Програма, съставена от функции, изглежда така:

python
### the functions
def read_data():
    pass

def clean_data():
    pass

def make_report():
    pass

### the main program
read_data()
clean_data()
make_report()

Функции и SAS macros ​

SAS macroSAS функция с PROC FCMPФункция на Python
Дефинира се с%macro name(...); ... %mend;function name(...); ... endsub;def name(...):
Какво правипише SAS код (текст), който SAS после изпълняваизчислява стойностизпълнява код, връща стойност
Параметритекст: &paramстойностивсякакви обекти: числа, lists, таблици, други функции
Променливи вътре%local / %globalлокалнилокални по подразбиране (раздел 6)
Използва се заповтаряне на стъпки, генериране на кодизчисления в DATA stepвсичко

Macro-то е генератор на текст: %interest(651927.30, 5.44) се заменя с текста (651927.30 * 5.44 / 100). Функцията на Python не е текст - тя се изпълнява и връща стойност. Не ви трябват &, %str() или правила за кавичките: параметърът е просто име за стойността.

SQL също има функции, дефинирани от потребителя (CREATE FUNCTION), но в аналитичната работа те са рядкост. В Python функциите са навсякъде.

2. Дефиниране на функция ​

Синтаксис ​

text
def function_name(parameters):
    """docstring"""
    block
    return expression
  • def - ключовата дума, с която започва дефиницията.
  • function_name - валидно име (занятие 1): snake_case, глагол, който казва какво прави функцията - calculate_interest, read_loans, is_late.
  • parameters - не са задължителни: имената, които ще получат входните стойности. Скобите са задължителни, дори когато няма параметри.
  • """docstring""" - не е задължителен: текст, който документира функцията (раздел 8).
  • block - тялото на функцията, отместено навътре. Поне един statement.
  • return expression - не е задължителен: резултатът на функцията (раздел 5).

Най-простата дефиниция:

python
def calculate_sum():
    pass
Променете кода и го изпълнете отново

pass (занятие 2) е тяло, което не прави нищо - за функция, която ще напишете по-късно. Пълна дефиниция:

python
def calculate_sum(number1, number2):
    """Take two numbers and return their sum.

    Parameters:
        number1 (int / float): the first number
        number2 (int / float): the second number

    Returns:
        int / float: the sum of number1 and number2
    """
    total_sum = number1 + number2
    return total_sum

print(calculate_sum(2, 3))
Променете кода и го изпълнете отново

Дефиницията не изпълнява тялото ​

python
def greet():
    """Just print hello."""
    print("Hello!")
Променете кода и го изпълнете отново

Нищо не се отпечатва: def само създава функцията и ѝ дава име. Тялото се изпълнява, когато функцията бъде извикана.

3. Извикване на функция ​

text
function_name(arguments)

Скобите са задължителни - дори когато няма аргументи:

python
def greet():
    """Just print hello."""
    print("Hello!")

greet()          # the call: now the body runs
greet()          # and again
print(greet)     # without brackets: the function object itself, not a call
Променете кода и го изпълнете отново

Функцията трябва да е дефинирана преди да бъде извикана. Python изпълнява файла отгоре надолу; в момента на извикването името трябва вече да съществува:

python
greet()

def greet():
    print("Hello!")
Променете кода и го изпълнете отново

Затова скриптът слага функциите си най-горе, а основния код - най-долу (раздел 11). (Вътре в тялото на функция можете да извикате функция, дефинирана по-долу във файла - тялото се изпълнява едва по-късно, когато всички def са изпълнени.)

4. Параметри и аргументи ​

Защо параметри ​

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

python
def add():
    print(2 + 3)

add()
add()
Променете кода и го изпълнете отново

По-добре е да ѝ давате стойностите при всяко извикване - add(20, 22), add(123, 321). За това са параметрите:

  • Параметърът е име в дефиницията: def add(x, y):. Вътре във функцията той е локална променлива.
  • Аргументът е стойността, дадена при извикването: add(20, 22). При извикването всеки параметър се свързва със своя аргумент.
python
def add(x, y):
    print(x + y)

add(20, 22)
add(123, 321)
add(16, 10)
Променете кода и го изпълнете отново

Анимация

Извиквания на функции стъпка по стъпка: 1. Извикване и връщане

Позиционни аргументи ​

По подразбиране аргументите се дават по позиция: първият аргумент - на първия параметър, вторият - на втория и т.н. Броят трябва да съвпада, иначе е TypeError:

python
def sub(x, y):
    """Subtract y from x and print the result."""
    print(x - y)

sub(99, 3)
sub(3, 99)        # the order matters
sub(99)           # TypeError: missing 1 required positional argument: 'y'
Променете кода и го изпълнете отново

Стойности по подразбиране ​

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

python
def greet(name="Nobody"):
    """Greet a user."""
    print("Hello", name)

greet("Maria")
greet()
Променете кода и го изпълнете отново

Върху портфейла - най-честият срок като стойност по подразбиране:

python
def annuity_payment(amount, yearly_rate, years=5):
    """The monthly payment of an annuity loan."""
    r = yearly_rate / 12 / 100
    n = years * 12
    return amount * r / (1 - (1 + r) ** -n)

print(round(annuity_payment(20000, 6), 2))          # 5 years
print(round(annuity_payment(20000, 6, 10), 2))      # 10 years
Променете кода и го изпълнете отново

Параметрите със стойности по подразбиране трябва да са след тези без - иначе Python не може да знае кой аргумент за кой е:

python
def greet(msg="Hi", name):
    print(f"{msg} {name}!")
Променете кода и го изпълнете отново

Правилният начин:

python
def greet(name, msg="Hi"):
    print(f"{msg} {name}!")

greet("Maria")
greet("Maria", "Good morning")
Променете кода и го изпълнете отново

Именувани аргументи (keyword arguments) ​

При извикването можете да назовете параметъра: name=value. Тогава редът няма значение - а извикването казва какво означава всяка стойност:

python
def greet(msg, name):
    print(f"{msg} {name}!")

greet(name="Maria", msg="Hi")
greet("Hi", name="Maria")        # positional first, then keywords
Променете кода и го изпълнете отново

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

python
def greet(msg, name):
    print(f"{msg} {name}!")

greet(name="Maria", "Hi")
Променете кода и го изпълнете отново

Именуваните аргументи правят четими извикванията с няколко числа - сравнете:

python
def annuity_payment(amount, yearly_rate, years=5):
    r = yearly_rate / 12 / 100
    return amount * r / (1 - (1 + r) ** -(years * 12))

print(annuity_payment(20000, 6, 10))                              # which is which?
print(annuity_payment(amount=20000, yearly_rate=6, years=10))     # clear
Променете кода и го изпълнете отново

Познавате това от SAS macros с keyword параметри: %report(data=loans, limit=90), и от print(..., sep=";", end="").

Капан: list като стойност по подразбиране ​

Стойността по подразбиране се създава веднъж, когато се изпълни def - а не при всяко извикване. Ако е list, всички извиквания делят един и същ list:

python
def add_loan(loan_id, loans=[]):
    loans.append(loan_id)
    return loans

print(add_loan("L000001"))
print(add_loan("L000002"))        # ['L000001', 'L000002'] - the list remembers!
Променете кода и го изпълнете отново

Правилото: никога не използвайте променяема стойност (list, dict, set) като стойност по подразбиране. Използвайте None и създайте list-а вътре:

python
def add_loan(loan_id, loans=None):
    if loans is None:
        loans = []
    loans.append(loan_id)
    return loans

print(add_loan("L000001"))
print(add_loan("L000002"))
Променете кода и го изпълнете отново

Анимация

Извиквания на функции стъпка по стъпка: 6. List като стойност по подразбиране (за напреднали)

Произволен брой позиционни аргументи: *args ​

Параметър със звездичка * събира всички допълнителни позиционни аргументи в tuple. По традиция се казва *args, но името може да е всякакво:

python
def foo(*args):
    print(args)

foo(1)
foo(1, 2)
foo(1, 2, 3)
foo()
Променете кода и го изпълнете отново

print(), max() и min() работят така: max(3, 8, 5) приема произволен брой стойности. Ваша собствена:

python
def total_exposure(*amounts):
    """The sum of any number of amounts."""
    total = 0
    for amount in amounts:
        total += amount
    return total

print(total_exposure(1044231.30))
print(total_exposure(1044231.30, 651927.30, 644.25))
Променете кода и го изпълнете отново

*args идва след обикновените параметри. Параметър, написан след *args, може да се дава само по име (keyword-only параметър):

python
def greet(*args, first_name):
    print(f"Hello, {first_name}!")

greet("Alice", "Bob", "Charlie")      # all go into args - first_name gets nothing
Променете кода и го изпълнете отново
python
def greet(first_name, *args):
    print(f"Hello, {first_name}!")
    for other_name in args:
        print(f"Hello, {other_name}!")

greet("Alice", "Bob", "Charlie", "Diana")
Променете кода и го изпълнете отново
python
def count_over(*values, limit):
    """How many values are over the limit."""
    count = 0
    for value in values:
        if value > limit:
            count += 1
    return count

print(count_over(0, 35, 120, 200, 5, limit=90))
print(count_over(0, 35, 120, 200, 5, limit=30))
Променете кода и го изпълнете отново
За напреднали: произволен брой именувани аргументи, **kwargs

Две звездички ** събират всички допълнителни именувани аргументи в dictionary (dictionaries: занятие 6 - тук двойки от име и стойност):

python
def foo(**kwargs):
    print(kwargs)

foo(a=1, b=2)
Променете кода и го изпълнете отново
python
def print_user_data(**kwargs):
    print(f"Data for user {kwargs['name']}:")
    for key, value in kwargs.items():
        print(f"  {key} - {value}")

print_user_data(name="Ada", age=28)
print_user_data(name="Maria", age=30, town="London")
Променете кода и го изпълнете отново

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

python
def report_settings(**options):
    print("Report settings:")
    for setting, value in options.items():
        print(f" - {setting}: {value}")

report_settings(report_date="2026-09-30", currency="EUR", decimals=2)
Променете кода и го изпълнете отново

Редът на параметрите в дефиниция, когато използвате всички видове:

text
def f(positional, with_default=1, *args, keyword_only, **kwargs):

При извикване ** разопакова dictionary в именувани аргументи:

python
def menu_print(fruit, price):
    print(f"{fruit:.<20s}{price:.2f}")

data = {"price": 2.5, "fruit": "apple"}
menu_print(**data)              # the same as menu_print(fruit="apple", price=2.5)
Променете кода и го изпълнете отново

Разопаковане при извикване: * ​

Звездичката работи обратно при извикване: * разопакова list или tuple в позиционни аргументи:

python
def my_func(p1, p2, p3):
    print(p1, p2, p3)

data = [1, 2, 3]
my_func(*data)                  # the same as my_func(data[0], data[1], data[2])
my_func(data)                   # TypeError: the whole list goes to p1
Променете кода и го изпълнете отново

Запис от файл, направо във функция:

python
def describe(loan_id, product, outstanding):
    print(f"{loan_id}: {product}, {outstanding:,.2f}")

record = ("L000002", "investment", 651927.30)
describe(*record)
Променете кода и го изпълнете отново
За напреднали: keyword-only * и positional-only /

Самостоятелна * в дефиницията означава "всичко след това трябва да се дава по име". Това прави извикванията четими и сигурни - никой не може да размени две числа по позиция:

python
def expected_loss(pd_value, lgd, ead, *, rounded=False):
    el = pd_value * lgd * ead
    return round(el, 2) if rounded else el

print(expected_loss(0.02, 0.45, 100000))
print(expected_loss(0.02, 0.45, 100000, rounded=True))
Променете кода и го изпълнете отново

sorted(items, key=..., reverse=True) е дефинирана така: key и reverse са keyword-only. / означава "всичко преди това трябва да се дава по позиция" - виждате го в help-а на много вградени функции, като len(obj, /).

5. Върнати стойности ​

return ​

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

python
def add(x, y):
    return x + y

print(add(2, 4) ** 2)          # 36
result = add(2, 3) + add(4, 5)
print(result)
Променете кода и го изпълнете отново

return приключва функцията - кодът след него никога не се изпълнява:

python
def add(x, y):
    return x + y
    print("after return")      # never runs

print(add(2, 4))
Променете кода и го изпълнете отново

Без return: None ​

Функция без return (или с празен return) връща None. В Python всяка функция връща нещо:

python
def foo():
    print("foo() was executed!")

def bar():
    print("bar() was executed!")
    return "End"

print(foo())
print(bar())
Променете кода и го изпълнете отново

Най-честата грешка с функциите: print показва стойност на екрана, но извикващият не получава нищо. return дава стойността на извикващия - и не показва нищо.

python
def interest_print(outstanding, rate):
    print(outstanding * rate / 100)

def interest_return(outstanding, rate):
    return outstanding * rate / 100

a = interest_print(1000, 5)        # prints 50.0 ...
b = interest_return(1000, 5)       # prints nothing ...
print(a, b)                        # ... but a is None, b is 50.0
print(b * 2)
print(a * 2)                       # TypeError: None * 2
Променете кода и го изпълнете отново

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

Няколко стойности: tuple ​

return a, b връща tuple (занятие 3) - и извикващият може да го разопакова:

python
def split_days(days_past_due):
    """Days past due as whole months of 30 days and the days left."""
    return days_past_due // 30, days_past_due % 30

result = split_days(125)
print(result)

months, days = split_days(125)
print(months, "months and", days, "days")
Променете кода и го изпълнете отново
python
def min_max_average(*values):
    return min(values), max(values), sum(values) / len(values)

low, high, average = min_max_average(5.44, 8.83, 4.79, 8.35)
print(low, high, round(average, 2))
Променете кода и го изпълнете отново

Ранно връщане ​

Функцията може да има няколко return - първият, който се изпълни, приключва функцията. Ранното връщане за специалните случаи държи основната логика плоска:

python
def dpd_bucket(days_past_due):
    """The bucket of days past due, like SQL CASE WHEN."""
    if days_past_due == 0:
        return "current"
    if days_past_due <= 30:
        return "1-30"
    if days_past_due <= 90:
        return "31-90"
    return "90+"

for days in [0, 15, 45, 200]:
    print(days, dpd_bucket(days))
Променете кода и го изпълнете отново

Не е нужен elif: след return останалото не се изпълнява.

6. Scope: къде се вижда едно име ​

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

За напреднали: namespaces и scopes, locals() и globals()
  • Namespace пази имената и обектите, с които са свързани (отвътре е dictionary). Той съществува, докато програмата работи: глобалният namespace се създава, когато файлът се зареди; локалният namespace на функция се създава при всяко извикване и се изтрива, когато извикването свърши.
  • Scope е частта от кода, в която един namespace се вижда пряко. Python определя scopes от текста на кода, преди да го изпълни.
python
x = 10                    # a global name

def foo():
    x = 20                # a local name
    print("local:", locals())
    print("global x:", globals()["x"])

foo()
Променете кода и го изпълнете отново

(globals() показва много повече имена - вградените на модула, като __name__.)

Локални имена ​

Имената, създадени вътре във функция - нейните параметри и всяко име, на което се присвоява в тялото ѝ - са локални: съществуват само по време на извикването и само вътре във функцията.

python
def f1():
    y = 2
    print(f"y = {y} inside f1")

f1()
print(f"y = {y} outside f1")        # NameError: y is not defined here
Променете кода и го изпълнете отново

Глобални имена ​

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

python
x = 10

def foo():
    print(f"x = {x} inside foo()")

foo()
print(f"x = {x} outside foo()")
Променете кода и го изпълнете отново

Присвояването създава локално име ​

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

python
x = 1

def foo():
    x = 99                # a new, local x
    print(f"x = {x} inside foo")

foo()
print(f"x = {x} outside foo")        # the global x did not change
Променете кода и го изпълнете отново

Същото важи за параметрите - параметърът е локално име. Нова стойност за него не променя променливата на извикващия:

python
def change(x):
    x = 4
    print("inside:", x)

x = 2
change(x)
print("outside:", x)
Променете кода и го изпълнете отново

Анимация

Извиквания на функции стъпка по стъпка: 2. Локално и глобално

И един капан: ако функцията присвоява на име някъде в тялото си, името е локално в цялото тяло - също и на редовете преди присвояването:

python
total = 0

def add(amount):
    total = total + amount        # UnboundLocalError: the local total has no value yet
    return total

add(100)
Променете кода и го изпълнете отново
За напреднали: защо грешката - първо компилация, после изпълнение

За да стане ясно, помага да знаем, че Python обработва кода на две отделни фази: компилация (прочита целия код, преди да го изпълни) и изпълнение (изпълнява кода ред по ред).

Когато Python компилира функцията add, той преглежда всички редове в нея. Вижда total = total + amount. Заради знака = Python решава: "total е локална променлива на тази функция" - за цялото тяло на функцията.

Тъй като total вече е отбелязано като локално, Python изобщо не търси глобалното total извън функцията. При изпълнението total + amount чете локалното total - но то още няма стойност, затова Python дава UnboundLocalError.

Другите команди, които свързват име със стойност, също го правят локално: +=, променливата на цикъл for, import, def.

Анимация

Извиквания на функции стъпка по стъпка: 3. UnboundLocalError

Функция извиква функция ​

Функция може да извиква други функции. Всяко извикване получава свои локални имена: по-долу има две имена rate, едно в payment и едно в monthly, и те не се смесват. Когато monthly върне резултат, payment продължава от реда, който я е извикал:

python
def monthly(rate):
    return rate / 12

def payment(amount, rate):
    r = monthly(rate)
    return amount * r / 100

print(payment(1200, 6))
Променете кода и го изпълнете отново

Анимация

Извиквания на функции стъпка по стъпка: 4. Функция извиква функция

global: промяна на глобално име - рядко ​

global name казва: "в тази функция name означава глобалното име" - тогава присвояването променя глобалната променлива:

python
count = 0

def increment():
    global count
    count += 1              # changes the global count

increment()
increment()
print(count)
Променете кода и го изпълнете отново

Използвайте го рядко. Функция, която променя глобални променливи, е трудна за разбиране и за тестване: от извикването не се вижда какво променя. Предпочитайте: стойностите влизат през параметрите, резултатите излизат през return.

python
total = 0

def add(total, amount):           # in: through the parameters
    return total + amount         # out: through return

total = add(total, 100)
total = add(total, 250)
print(total)
Променете кода и го изпълнете отново
За напреднали: вложени функции, global и nonlocal

Функция може да се дефинира вътре в друга. Всяка има свои локални имена:

python
def outer():
    x = 2
    def inner():
        x = 3
        print(f"x = {x} in inner")
    inner()
    print(f"x = {x} in outer")

x = 1
outer()
print(f"x = {x} in global")
Променете кода и го изпълнете отново

С global x във вътрешната функция присвояването променя глобалното x:

python
def outer():
    x = 2
    def inner():
        global x
        x = 3               # changes the global x
        print(f"x = {x} in inner")
    inner()
    print(f"x = {x} in outer")

x = 1
outer()
print(f"x = {x} in global")
Променете кода и го изпълнете отново

nonlocal name означава името на най-близката обхващаща функция (не глобалното):

python
def outer():
    x = 2
    def inner():
        nonlocal x
        x = 3               # changes the x of outer
        print(f"x = {x} in inner")
    inner()
    print(f"x = {x} in outer")

x = 1
outer()
print(f"x = {x} in global")
Променете кода и го изпълнете отново

Как Python намира едно име: LEGB ​

Когато се използва име, Python го търси на четири места, в този ред, и взема първото, което намери:

  1. Local - функцията, която се изпълнява сега;
  2. Enclosing - обхващащите я функции, от най-близката навън;
  3. Global - най-горното ниво на файла (модула);
  4. Built-in - имената на самия Python: print, len, sum, ...

Ако името го няма никъде, това е NameError.

Четири вложени кутии: Built-in около Global около Enclosing около Local, с примерни имена; Python търси име отвътре навън

Затова не бива да използвате вградени имена за своите променливи: sum = 0 скрива вградената sum() в този scope (чест навик от други езици):

python
sum = 0                      # a global name sum hides the built-in sum
for n in [1, 2, 3]:
    sum += n
print(sum)
print(sum([1, 2, 3]))        # TypeError: 'int' object is not callable
Променете кода и го изпълнете отново
SASPython
%local x; - macro променлива само в това macroприсвояване във функция (по подразбиране)
%global x;име на най-горното ниво на файла; global x, за да му присвоите във функция
%let извън macrosглобална променлива - използвайте я само за константи

7. Аргументите са референции ​

Параметърът се свързва със същия обект като аргумента - Python не го копира (занятие 3: имена и референции). За числа и strings това няма значение, защото те не могат да се променят. List, подаден на функция, може да бъде променен от функцията - и извикващият вижда промяната:

python
def add_fee(amounts):
    amounts.append(10)            # changes the caller's list!

fees = [5, 7]
add_fee(fees)
print(fees)
Променете кода и го изпълнете отново

Анимация

Извиквания на функции стъпка по стъпка: 5. List, подаден на функция

Понякога точно това искате (функция, която пълни list). Обикновено е изненада. Сигурният подход по подразбиране: не променяйте аргументите - изградете и върнете нов list:

python
def with_fee(amounts, fee=10):
    """A new list: each amount plus the fee. The input is not changed."""
    result = []
    for amount in amounts:
        result.append(amount + fee)
    return result

fees = [5, 7]
new_fees = with_fee(fees)
print(fees, new_fees)
Променете кода и го изпълнете отново

8. Docstrings и type hints ​

Docstrings ​

Docstring е string точно след реда с def, в тройни кавички. Той казва какво прави функцията, какво приема и какво връща - за хората, които ще я използват (вие, след три месеца). Инструментите го показват: help(), и VS Code, когато задържите мишката над името.

python
def expected_loss(pd_value, lgd, ead):
    """Return the expected loss of one exposure: EL = PD * LGD * EAD.

    pd_value: the probability of default, 0 ... 1
    lgd: the loss given default, 0 ... 1
    ead: the exposure at default, in EUR
    """
    return pd_value * lgd * ead

help(expected_loss)
print(expected_loss.__doc__.splitlines()[0])
Променете кода и го изпълнете отново

Конвенциите (PEP 257): първият ред е кратко изречение, което казва какво прави функцията; после празен ред и подробностите.

Type hints ​

Type hints документират типовете на параметрите и на резултата:

python
def expected_loss(pd_value: float, lgd: float, ead: float) -> float:
    """Return the expected loss of one exposure: EL = PD * LGD * EAD."""
    return pd_value * lgd * ead

def is_late(days_past_due: int, limit: int = 30) -> bool:
    return days_past_due > limit

def total(amounts: list[float]) -> float:
    return sum(amounts)

def find_loan(loan_id: str, loans: list[tuple[str, float]]) -> tuple[str, float] | None:
    for loan in loans:
        if loan[0] == loan_id:
            return loan
    return None

print(expected_loss(0.02, 0.45, 100000))
print(is_late(45), total([1.5, 2.5]))
print(find_loan("L2", [("L1", 100.0), ("L2", 250.0)]))
Променете кода и го изпълнете отново
HintОзначава
int, float, str, boolедна стойност от този тип
list[float]list от floats
tuple[str, float]tuple от string и float
str | Nonestring или None (от Python 3.10)
-> Noneфункцията не връща нищо

Python не проверява type hints, когато кодът се изпълнява - те са документация:

python
def is_late(days_past_due: int) -> bool:
    return days_past_due > 30

print(is_late(45.5))       # works: hints are not checked at run time
Променете кода и го изпълнете отново

Но редакторите могат да ги проверят, преди да сте пуснали каквото и да е: Pylance във VS Code (идва с разширението Python) подчертава извикване с грешен тип. Pylance работи веднага след инсталиране, но статичната проверка на типовете му е изключена по подразбиране. За да я включите, добавете тази настройка в настройките на VS Code (settings.json; отваря се с Ctrl+Shift+P -> "Preferences: Open User Settings (JSON)"):

json
{
  "python.analysis.typeCheckingMode": "basic"
}

В работата с данни hints са най-полезни при функциите, които други хора извикват.

По-новите версии на Python

Python 3.12 добави statement-а type за псевдоними на типове: type Amount = float. Python 3.14 (версията на този сайт) изчислява анотациите само когато потрябват, така че hint може да назове клас, дефиниран по-надолу във файла. Нито едното не ви трябва в този курс - hints по-горе работят в 3.12 и във всяка по-нова версия.

9. Функциите са стойности ​

Функции от първи клас (first-class functions) ​

В Python функцията е обект, като число или list. Затова една функция може да бъде:

  • свързана с друго име или съхранена в list;
  • подадена като аргумент на друга функция;
  • върната като стойност от друга функция.
python
def greet(name):
    print(f"Hello, {name}")

foo = greet          # no brackets: the function itself, not a call
greet("Maria")
foo("Maria")         # the same function, another name
print(foo is greet)
Променете кода и го изпълнете отново

Функция като аргумент ​

python
def apply_function(func, value):
    """Apply a function to a value."""
    return func(value)

def square(x):
    return x * x

def cube(x):
    return x ** 3

print(apply_function(square, 5))
print(apply_function(cube, 5))
Променете кода и го изпълнете отново

Функция, която приема или връща функция, е функция от по-висок ред (higher-order function). Вече познавате някои: sorted, min и max приемат функция като key (раздел 10). Отчет, който може да използва всяко правило за закръгляне:

python
import math

def total(amounts, rounding):
    result = 0
    for amount in amounts:
        result += rounding(amount)
    return result

amounts = [10.4, 20.6, 30.5]
print(total(amounts, round))
print(total(amounts, math.floor))
print(total(amounts, math.ceil))
Променете кода и го изпълнете отново
За напреднали: функция, която връща функция (closure)
python
def multiply_by(n):
    """Return a function that multiplies its input by n."""
    def multiply(x):
        return x * n
    return multiply

double = multiply_by(2)
to_bgn = multiply_by(1.95583)      # the fixed BGN rate of the euro

print(double(5))
print(round(to_bgn(100), 2))
Променете кода и го изпълнете отново

Вътрешната функция помни n от извикването, което я е създало, дори след като то е приключило. Такава функция се нарича closure.

За напреднали: функции в dictionary (dictionaries: занятие 6)
python
def add(x, y):
    return x + y

def sub(x, y):
    return x - y

operations = {"add": add, "sub": sub}
print(operations["add"](4, 2))
print(operations["sub"](4, 2))
Променете кода и го изпълнете отново

Dictionary от функции заменя дълга верига if / elif: ключът избира функцията (dispatch table).

Чисти функции ​

Идеи от функционалното програмиране, които правят кода по-надежден:

  • Чистата функция (pure function) винаги връща един и същ резултат за едни и същи аргументи и не прави нищо друго: не отпечатва, не променя аргументите си или глобални променливи, не пише файлове. expected_loss(pd, lgd, ead) е чиста.
  • Всичко друго, което прави една функция, е страничен ефект (side effect): отпечатване, запис във файл, промяна на list, който е получила. Програмата има нужда от странични ефекти - но ги дръжте на малко ясни места.
  • Непроменимите (immutable) данни (tuples, strings) не могат да бъдат променени по грешка.

Чистите функции се тестват лесно (извикайте, сравнете резултата), лесно се преизползват и безопасно се пускат отново върху нови данни - точно това, което трябва на един месечен отчет.

10. lambda ​

Малки функции без име ​

Lambda изразът създава малка функция без име, на един ред:

text
lambda parameters: expression

Може да има произволен брой параметри, но само един израз - неговата стойност е резултатът. Без return, без statements.

python
double = lambda x: x * 2
print(double(5))

# the same as:
def double(x):
    return x * 2
print(double(5))
Променете кода и го изпълнете отново
python
greet = lambda: "Hello, World!"          # no parameters
print(greet())

add = lambda x, y: x + y                 # two parameters
print(add(4, 2))
Променете кода и го изпълнете отново

Не давайте име на lambda в истински код (double = lambda ...) - за това е def, а def дава на функцията истинско име в съобщенията за грешки (PEP 8). Lambda е за функция, която ви трябва веднъж, точно тук - най-често като key на сортиране.

Сортиране по ключ ​

sorted() и list.sort() приемат key: функция, която се извиква за всеки елемент, и елементите се сортират по нейния резултат. Това е ORDER BY от SQL с произволен израз:

python
data = [(1, "Z"), (2, "Y"), (3, "X")]
print(sorted(data, key=lambda t: t[1]))         # by the second value
Променете кода и го изпълнете отново
python
loans = [
    ("L000001", "investment", 1044231.30),
    ("L000002", "investment", 651927.30),
    ("L000003", "credit_card", 644.25),
    ("L000004", "overdraft", 5875.11),
]

# ORDER BY outstanding DESC
for loan in sorted(loans, key=lambda loan: loan[2], reverse=True):
    print(loan)

# ORDER BY product, loan_id: a tuple as the key
print(sorted(loans, key=lambda loan: (loan[1], loan[0]))[0])
Променете кода и го изпълнете отново

key работи и с min и max - кредитът с най-голяма непогасена сума, а не само сумата:

python
loans = [("L000001", 1044231.30), ("L000002", 651927.30), ("L000003", 644.25)]
print(max(loans, key=lambda loan: loan[1]))
print(min(loans, key=lambda loan: loan[1]))
Променете кода и го изпълнете отново

Именувана функция също работи като ключ - key=len, key=str.lower:

python
print(sorted(["banana", "Cherry", "apple"]))                   # capitals first
print(sorted(["banana", "Cherry", "apple"], key=str.lower))    # ignore the case
print(sorted(["cherry", "fig", "banana"], key=len))
Променете кода и го изпълнете отново

Сортирането е разгледано в дълбочина в занятие 5. Reference: Sorting techniques.

11. От macros към функции: скрипт за отчет ​

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

python
"""Outstanding amount and number of loans by days-past-due bucket."""

LOANS_FILE = "data/loans.csv"
BUCKETS = ("current", "1-30", "31-90", "90+")


def dpd_bucket(days_past_due):
    if days_past_due == 0:
        return "current"
    if days_past_due <= 30:
        return "1-30"
    if days_past_due <= 90:
        return "31-90"
    return "90+"


def read_loans(path):
    """The loans of a file as a list of (loan_id, outstanding, days_past_due)."""
    loans = []
    with open(path, encoding="utf-8") as file:
        file.readline()
        for line in file:
            fields = line.strip().split(",")
            loans.append((fields[0], float(fields[7]), int(fields[9])))
    return loans


def bucket_totals(loans, bucket):
    """The number and the outstanding sum of the loans in one bucket."""
    count, total = 0, 0.0
    for loan_id, outstanding, days_past_due in loans:
        if dpd_bucket(days_past_due) == bucket:
            count += 1
            total += outstanding
    return count, total


def print_report(loans):
    print(f"{'bucket':<10}{'loans':>7}{'outstanding':>18}")
    for bucket in BUCKETS:
        count, total = bucket_totals(loans, bucket)
        print(f"{bucket:<10}{count:>7}{total:>18,.2f}")


loans = read_loans(LOANS_FILE)
print_report(loans)
Променете кода и го изпълнете отново

Сравнете го с първия пример от занятие 2: същият резултат, но сега всяка част има име, може да се тества сама и може да се преизползва - read_loans в следващия отчет, dpd_bucket в проверка на данните.

Във файл, който се пуска като скрипт, основният код често се слага под if __name__ == "__main__": - така функциите могат да се import-ват от друг файл, без да се пуска отчетът. Модулите и import на собствени файлове са темата на занятие 10.

12. Упражнения ​

Отворете своето копие на Homework repository във VS Code (преди занятието: вземете новите задачи) и пускайте задачите от терминала, в главната папка на repository-то:

bash
python 04-functions/practice/task1-first-functions.py

В тези задачи пишете функции; проверките ги извикват с много различни стойности.

ЗадачаФайлКакво правите
1task1-first-functions.pyМалки функции: лихва, погасен дял, дали е просрочен, месечна лихва
2task2-defaults-keywords.pyСтойности по подразбиране и именувани аргументи: вноска, български формат на числата
3task3-args-and-tuples.py*args, няколко върнати стойности: общо, min / max / средно, месеци и дни
4task4-portfolio-rules.pyПравилата за портфейла като функции: група, етап, клас, продуктова група
5task5-predict-scope.pyПредвидете стойностите: локални и глобални имена, list, подаден на функция
6task6-lambda-sorting.pyСортиране и търсене с key и lambda; премахване на повторения, като редът се запазва

Обобщение ​

ТемаЗапомнете
Дефиниране, извикванеdef name(params): + отместено навътре тяло; name(args) я изпълнява - скобите са задължителни
Параметрипозиционни; стойностите по подразбиране - след останалите; name=value при извикване; *args е tuple
Стойности по подразбираненикога list или dict като стойност по подразбиране - използвайте None
returnприключва функцията и дава стойност; без return -> None; return a, b дава tuple
print / returnprint показва, return дава - изчисляващите функции връщат
Scopeимената, на които се присвоява във функция, са локални; LEGB: local, enclosing, global, built-in; избягвайте global - стойностите влизат през параметрите, излизат през return
Референцииlist, подаден на функция, може да бъде променен от нея - вместо това връщайте нов list
Документацияdocstring в тройни кавички; type hints x: float -> float не се проверяват по време на изпълнение
Функциите като стойностиподавайте функция като аргумент; lambda x: ... за кратък key
Сортиранеsorted(items, key=lambda item: item[2], reverse=True) - ORDER BY от SQL по произволен израз
SAS macrosmacro-то пише код; функцията се изпълнява и връща стойност

Домашно ​

  1. 04-functions/homework.py (в homework repository): библиотека от функции за портфейла - прочетете кредитите и рейтинговата скала от файловете, намерете PD на даден рейтинг, очакваната загуба на кредит и сумите по етапи. Всяка проверка трябва да отпечата OK.
  2. Задачите за упражнение, които не сте довършили в клас.
  3. По желание: кредитен калкулатор, съставен от функции - същата програма като домашното по желание от занятие 2, но организирана така: get_user_data() чете и проверява входа (сума > 0, лихва 0 - 50, години 1 - 30; пита отново, докато стойността е грешна) и връща tuple; calc_payment(amount, rate, years) връща вноската; print_schedule(amount, rate, years) отпечатва плана. Напишете отделна функция за всяка проверка: is_valid_amount(), is_valid_rate(), is_valid_years(), всяка връща True или False.

Самостоятелно изучаване ​

Тази част не се разглежда в час - тя е за четене вкъщи.

Декоратори ​

Декораторите не са нужни за задачите в този курс, но ще ги срещнете в библиотеки (@property и @dataclass в занятие 11, @pytest.fixture, @app.get в уеб frameworks). Тази част обяснява @, което ще виждате. Тя използва *args, **kwargs и функция във функция (каретата "За напреднали" в раздели 4 и 6).

Идеята ​

Декораторът (decorator) е функция, която приема функция и връща нова, "декорирана" функция: добавя нещо преди и след оригинала, без да променя кода му. decorated = decorator(original):

python
def stars_decorator(f):
    def wrapper():
        print("*" * 30)
        f()
        print("*" * 30)
    return wrapper

def greet():
    print("Howdy World!")

greet = stars_decorator(greet)       # decorate
greet()
Променете кода и го изпълнете отново

Синтаксисът с @ ​

@decorator над def е кратък начин да напишете name = decorator(name):

python
def stars_decorator(f):
    def wrapper():
        print("*" * 30)
        f()
        print("*" * 30)
    return wrapper

@stars_decorator
def greet():
    print("Howdy World!")

greet()
Променете кода и го изпълнете отново

Аргументите и върнатата стойност ​

Обвиващата функция (wrapper) трябва да предаде аргументите нататък и да върне резултата на оригинала. *args, **kwargs (раздел 4) приемат всякакви аргументи; functools.wraps запазва името и docstring-а на оригинала. Полезен декоратор - колко време отнема всяка стъпка на един отчет:

python
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f} s")
        return result
    return wrapper

@timed
def total_outstanding(path):
    total = 0.0
    with open(path, encoding="utf-8") as file:
        file.readline()
        for line in file:
            total += float(line.split(",")[7])
    return total

print(f"{total_outstanding('data/loans.csv'):,.2f}")
Променете кода и го изпълнете отново

Упражнение. Напишете декоратор announce, който отпечатва start: <name of the function> преди извикването и end след него, и го използвайте върху функция report(), която отпечатва the report. (Подсказка: func.__name__ е името на функцията.)

Отговор
python
import functools

def announce(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"start: {func.__name__}")
        result = func(*args, **kwargs)
        print("end")
        return result
    return wrapper

@announce
def report():
    print("the report")

report()
Променете кода и го изпълнете отново

Още за четене: Primer on Python decorators (Real Python).

Още за четене ​

Следващото занятие: lists за напреднали - list comprehensions, сортиране с key, zip, enumerate, map и filter и таблица като list от редове.

Създаден за