Български
Тема
Български
Тема
Днес ще се научите да:
def и да ги извиквате;*args;return - и да видите защо print не е return;lambda и да сортирате по ключ;От SAS към Python - едно и също парче логика за многократна употреба, по два начина:
%macro interest(outstanding, rate);
(&outstanding * &rate / 100)
%mend;
data _null_;
yearly = %interest(651927.30, 5.44);
put yearly=;
run;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))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))Функцията е именуван блок от код, който върши една задача. Дефинирате я веднъж и я извиквате - изпълнявате я - колкото пъти ви трябва, с различни стойности. Добра представа е рецепта в готварска книга: написана веднъж, сготвена много пъти, с различни количества.
Вече извиквате функции всеки ден: print(), len(), round(), sum(), sorted(), int(). Днес пишете свои.
Функциите помагат да:
Програма, съставена от функции, изглежда така:
### the functions
def read_data():
pass
def clean_data():
pass
def make_report():
pass
### the main program
read_data()
clean_data()
make_report()| SAS macro | SAS функция с PROC FCMP | Функция на Python | |
|---|---|---|---|
| Дефинира се с | %macro name(...); ... %mend; | function name(...); ... endsub; | def name(...): |
| Какво прави | пише SAS код (текст), който SAS после изпълнява | изчислява стойност | изпълнява код, връща стойност |
| Параметри | текст: ¶m | стойности | всякакви обекти: числа, lists, таблици, други функции |
| Променливи вътре | %local / %global | локални | локални по подразбиране (раздел 6) |
| Използва се за | повтаряне на стъпки, генериране на код | изчисления в DATA step | всичко |
Macro-то е генератор на текст: %interest(651927.30, 5.44) се заменя с текста (651927.30 * 5.44 / 100). Функцията на Python не е текст - тя се изпълнява и връща стойност. Не ви трябват &, %str() или правила за кавичките: параметърът е просто име за стойността.
SQL също има функции, дефинирани от потребителя (CREATE FUNCTION), но в аналитичната работа те са рядкост. В Python функциите са навсякъде.
def function_name(parameters):
"""docstring"""
block
return expressiondef - ключовата дума, с която започва дефиницията.function_name - валидно име (занятие 1): snake_case, глагол, който казва какво прави функцията - calculate_interest, read_loans, is_late.parameters - не са задължителни: имената, които ще получат входните стойности. Скобите са задължителни, дори когато няма параметри."""docstring""" - не е задължителен: текст, който документира функцията (раздел 8).block - тялото на функцията, отместено навътре. Поне един statement.return expression - не е задължителен: резултатът на функцията (раздел 5).Най-простата дефиниция:
def calculate_sum():
passdef calculate_sum():
passpass (занятие 2) е тяло, което не прави нищо - за функция, която ще напишете по-късно. Пълна дефиниция:
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))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))def greet():
"""Just print hello."""
print("Hello!")def greet():
"""Just print hello."""
print("Hello!")Нищо не се отпечатва: def само създава функцията и ѝ дава име. Тялото се изпълнява, когато функцията бъде извикана.
function_name(arguments)Скобите са задължителни - дори когато няма аргументи:
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 calldef 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 изпълнява файла отгоре надолу; в момента на извикването името трябва вече да съществува:
greet()
def greet():
print("Hello!")greet()
def greet():
print("Hello!")Затова скриптът слага функциите си най-горе, а основния код - най-долу (раздел 11). (Вътре в тялото на функция можете да извикате функция, дефинирана по-долу във файла - тялото се изпълнява едва по-късно, когато всички def са изпълнени.)
Функция, която винаги работи с едни и същи стойности, не е много полезна:
def add():
print(2 + 3)
add()
add()def add():
print(2 + 3)
add()
add()По-добре е да ѝ давате стойностите при всяко извикване - add(20, 22), add(123, 321). За това са параметрите:
def add(x, y):. Вътре във функцията той е локална променлива.add(20, 22). При извикването всеки параметър се свързва със своя аргумент.def add(x, y):
print(x + y)
add(20, 22)
add(123, 321)
add(16, 10)def add(x, y):
print(x + y)
add(20, 22)
add(123, 321)
add(16, 10)Анимация
Извиквания на функции стъпка по стъпка: 1. Извикване и връщане
По подразбиране аргументите се дават по позиция: първият аргумент - на първия параметър, вторият - на втория и т.н. Броят трябва да съвпада, иначе е TypeError:
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'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'Параметърът може да има стойност по подразбиране, която се използва, когато извикването не дава аргумент за него:
def greet(name="Nobody"):
"""Greet a user."""
print("Hello", name)
greet("Maria")
greet()def greet(name="Nobody"):
"""Greet a user."""
print("Hello", name)
greet("Maria")
greet()Върху портфейла - най-честият срок като стойност по подразбиране:
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 yearsdef 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 не може да знае кой аргумент за кой е:
def greet(msg="Hi", name):
print(f"{msg} {name}!")def greet(msg="Hi", name):
print(f"{msg} {name}!")Правилният начин:
def greet(name, msg="Hi"):
print(f"{msg} {name}!")
greet("Maria")
greet("Maria", "Good morning")def greet(name, msg="Hi"):
print(f"{msg} {name}!")
greet("Maria")
greet("Maria", "Good morning")При извикването можете да назовете параметъра: name=value. Тогава редът няма значение - а извикването казва какво означава всяка стойност:
def greet(msg, name):
print(f"{msg} {name}!")
greet(name="Maria", msg="Hi")
greet("Hi", name="Maria") # positional first, then keywordsdef greet(msg, name):
print(f"{msg} {name}!")
greet(name="Maria", msg="Hi")
greet("Hi", name="Maria") # positional first, then keywordsИменуваните аргументи трябва да са след позиционните:
def greet(msg, name):
print(f"{msg} {name}!")
greet(name="Maria", "Hi")def greet(msg, name):
print(f"{msg} {name}!")
greet(name="Maria", "Hi")Именуваните аргументи правят четими извикванията с няколко числа - сравнете:
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)) # cleardef 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="").
Стойността по подразбиране се създава веднъж, когато се изпълни def - а не при всяко извикване. Ако е list, всички извиквания делят един и същ list:
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!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-а вътре:
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"))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, но името може да е всякакво:
def foo(*args):
print(args)
foo(1)
foo(1, 2)
foo(1, 2, 3)
foo()def foo(*args):
print(args)
foo(1)
foo(1, 2)
foo(1, 2, 3)
foo()print(), max() и min() работят така: max(3, 8, 5) приема произволен брой стойности. Ваша собствена:
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))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 параметър):
def greet(*args, first_name):
print(f"Hello, {first_name}!")
greet("Alice", "Bob", "Charlie") # all go into args - first_name gets nothingdef greet(*args, first_name):
print(f"Hello, {first_name}!")
greet("Alice", "Bob", "Charlie") # all go into args - first_name gets nothingdef greet(first_name, *args):
print(f"Hello, {first_name}!")
for other_name in args:
print(f"Hello, {other_name}!")
greet("Alice", "Bob", "Charlie", "Diana")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")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))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 - тук двойки от име и стойност):
def foo(**kwargs):
print(kwargs)
foo(a=1, b=2)def foo(**kwargs):
print(kwargs)
foo(a=1, b=2)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")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")Типично използване е функция с много незадължителни настройки:
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)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)Редът на параметрите в дефиниция, когато използвате всички видове:
def f(positional, with_default=1, *args, keyword_only, **kwargs):При извикване ** разопакова dictionary в именувани аргументи:
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)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 в позиционни аргументи:
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 p1def 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Запис от файл, направо във функция:
def describe(loan_id, product, outstanding):
print(f"{loan_id}: {product}, {outstanding:,.2f}")
record = ("L000002", "investment", 651927.30)
describe(*record)def describe(loan_id, product, outstanding):
print(f"{loan_id}: {product}, {outstanding:,.2f}")
record = ("L000002", "investment", 651927.30)
describe(*record)* и positional-only /Самостоятелна * в дефиницията означава "всичко след това трябва да се дава по име". Това прави извикванията четими и сигурни - никой не може да размени две числа по позиция:
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))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, /).
return return приключва функцията и връща стойност на извикващия. Тогава самото извикване е израз с тази стойност: можете да го отпечатате, да му дадете име, да го използвате в изчисление или да го подадете на друга функция.
def add(x, y):
return x + y
print(add(2, 4) ** 2) # 36
result = add(2, 3) + add(4, 5)
print(result)def add(x, y):
return x + y
print(add(2, 4) ** 2) # 36
result = add(2, 3) + add(4, 5)
print(result)return приключва функцията - кодът след него никога не се изпълнява:
def add(x, y):
return x + y
print("after return") # never runs
print(add(2, 4))def add(x, y):
return x + y
print("after return") # never runs
print(add(2, 4))return: None Функция без return (или с празен return) връща None. В Python всяка функция връща нещо:
def foo():
print("foo() was executed!")
def bar():
print("bar() was executed!")
return "End"
print(foo())
print(bar())def foo():
print("foo() was executed!")
def bar():
print("bar() was executed!")
return "End"
print(foo())
print(bar())print не е return Най-честата грешка с функциите: print показва стойност на екрана, но извикващият не получава нищо. return дава стойността на извикващия - и не показва нищо.
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 * 2def 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Правило: функция, която изчислява, връща резултата си. Отпечатването е работа на кода, който я е извикал - или на отделна функция, която само отпечатва. Така една и съща функция може да се използва в отчет, във файл, в тест.
return a, b връща tuple (занятие 3) - и извикващият може да го разопакова:
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")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")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))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 - първият, който се изпълни, приключва функцията. Ранното връщане за специалните случаи държи основната логика плоска:
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))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 останалото не се изпълнява.
Scope на едно име е частта от програмата, в която можете да го използвате. Функцията има свои имена, които останалата част от програмата не вижда.
locals() и globals()x = 10 # a global name
def foo():
x = 20 # a local name
print("local:", locals())
print("global x:", globals()["x"])
foo()x = 10 # a global name
def foo():
x = 20 # a local name
print("local:", locals())
print("global x:", globals()["x"])
foo()(globals() показва много повече имена - вградените на модула, като __name__.)
Имената, създадени вътре във функция - нейните параметри и всяко име, на което се присвоява в тялото ѝ - са локални: съществуват само по време на извикването и само вътре във функцията.
def f1():
y = 2
print(f"y = {y} inside f1")
f1()
print(f"y = {y} outside f1") # NameError: y is not defined heredef f1():
y = 2
print(f"y = {y} inside f1")
f1()
print(f"y = {y} outside f1") # NameError: y is not defined hereИме, дефинирано извън всяка функция, на най-горното ниво на файла, е глобално за файла: всяка функция във файла може да го чете.
x = 10
def foo():
print(f"x = {x} inside foo()")
foo()
print(f"x = {x} outside foo()")x = 10
def foo():
print(f"x = {x} inside foo()")
foo()
print(f"x = {x} outside foo()")Когато на име се присвоява вътре във функция, то е локално - дори ако съществува глобално име със същото име. Това са две различни променливи:
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 changex = 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Същото важи за параметрите - параметърът е локално име. Нова стойност за него не променя променливата на извикващия:
def change(x):
x = 4
print("inside:", x)
x = 2
change(x)
print("outside:", x)def change(x):
x = 4
print("inside:", x)
x = 2
change(x)
print("outside:", x)Анимация
Извиквания на функции стъпка по стъпка: 2. Локално и глобално
И един капан: ако функцията присвоява на име някъде в тялото си, името е локално в цялото тяло - също и на редовете преди присвояването:
total = 0
def add(amount):
total = total + amount # UnboundLocalError: the local total has no value yet
return total
add(100)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 продължава от реда, който я е извикал:
def monthly(rate):
return rate / 12
def payment(amount, rate):
r = monthly(rate)
return amount * r / 100
print(payment(1200, 6))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 означава глобалното име" - тогава присвояването променя глобалната променлива:
count = 0
def increment():
global count
count += 1 # changes the global count
increment()
increment()
print(count)count = 0
def increment():
global count
count += 1 # changes the global count
increment()
increment()
print(count)Използвайте го рядко. Функция, която променя глобални променливи, е трудна за разбиране и за тестване: от извикването не се вижда какво променя. Предпочитайте: стойностите влизат през параметрите, резултатите излизат през return.
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)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Функция може да се дефинира вътре в друга. Всяка има свои локални имена:
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")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:
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")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 означава името на най-близката обхващаща функция (не глобалното):
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")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 го търси на четири места, в този ред, и взема първото, което намери:
print, len, sum, ...Ако името го няма никъде, това е NameError.
Затова не бива да използвате вградени имена за своите променливи: sum = 0 скрива вградената sum() в този scope (чест навик от други езици):
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 callablesum = 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| SAS | Python |
|---|---|
%local x; - macro променлива само в това macro | присвояване във функция (по подразбиране) |
%global x; | име на най-горното ниво на файла; global x, за да му присвоите във функция |
%let извън macros | глобална променлива - използвайте я само за константи |
Параметърът се свързва със същия обект като аргумента - Python не го копира (занятие 3: имена и референции). За числа и strings това няма значение, защото те не могат да се променят. List, подаден на функция, може да бъде променен от функцията - и извикващият вижда промяната:
def add_fee(amounts):
amounts.append(10) # changes the caller's list!
fees = [5, 7]
add_fee(fees)
print(fees)def add_fee(amounts):
amounts.append(10) # changes the caller's list!
fees = [5, 7]
add_fee(fees)
print(fees)Анимация
Извиквания на функции стъпка по стъпка: 5. List, подаден на функция
Понякога точно това искате (функция, която пълни list). Обикновено е изненада. Сигурният подход по подразбиране: не променяйте аргументите - изградете и върнете нов list:
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)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)Docstring е string точно след реда с def, в тройни кавички. Той казва какво прави функцията, какво приема и какво връща - за хората, които ще я използват (вие, след три месеца). Инструментите го показват: help(), и VS Code, когато задържите мишката над името.
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])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 документират типовете на параметрите и на резултата:
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)]))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 | None | string или None (от Python 3.10) |
-> None | функцията не връща нищо |
Python не проверява type hints, когато кодът се изпълнява - те са документация:
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 timedef 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)"):
{
"python.analysis.typeCheckingMode": "basic"
}В работата с данни hints са най-полезни при функциите, които други хора извикват.
По-новите версии на Python
Python 3.12 добави statement-а type за псевдоними на типове: type Amount = float. Python 3.14 (версията на този сайт) изчислява анотациите само когато потрябват, така че hint може да назове клас, дефиниран по-надолу във файла. Нито едното не ви трябва в този курс - hints по-горе работят в 3.12 и във всяка по-нова версия.
В Python функцията е обект, като число или list. Затова една функция може да бъде:
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)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)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))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). Отчет, който може да използва всяко правило за закръгляне:
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))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))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))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.
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))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).
Идеи от функционалното програмиране, които правят кода по-надежден:
expected_loss(pd, lgd, ead) е чиста.Чистите функции се тестват лесно (извикайте, сравнете резултата), лесно се преизползват и безопасно се пускат отново върху нови данни - точно това, което трябва на един месечен отчет.
lambda Lambda изразът създава малка функция без име, на един ред:
lambda parameters: expressionМоже да има произволен брой параметри, но само един израз - неговата стойност е резултатът. Без return, без statements.
double = lambda x: x * 2
print(double(5))
# the same as:
def double(x):
return x * 2
print(double(5))double = lambda x: x * 2
print(double(5))
# the same as:
def double(x):
return x * 2
print(double(5))greet = lambda: "Hello, World!" # no parameters
print(greet())
add = lambda x, y: x + y # two parameters
print(add(4, 2))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 с произволен израз:
data = [(1, "Z"), (2, "Y"), (3, "X")]
print(sorted(data, key=lambda t: t[1])) # by the second valuedata = [(1, "Z"), (2, "Y"), (3, "X")]
print(sorted(data, key=lambda t: t[1])) # by the second valueloans = [
("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])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 - кредитът с най-голяма непогасена сума, а не само сумата:
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]))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:
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))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.
Структурата на скрипт, който можете да пускате отново всеки месец: най-горе imports и константите, после функциите, а най-долу - основният код.
"""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)"""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.
Отворете своето копие на Homework repository във VS Code (преди занятието: вземете новите задачи) и пускайте задачите от терминала, в главната папка на repository-то:
python 04-functions/practice/task1-first-functions.pyВ тези задачи пишете функции; проверките ги извикват с много различни стойности.
| Задача | Файл | Какво правите |
|---|---|---|
| 1 | task1-first-functions.py | Малки функции: лихва, погасен дял, дали е просрочен, месечна лихва |
| 2 | task2-defaults-keywords.py | Стойности по подразбиране и именувани аргументи: вноска, български формат на числата |
| 3 | task3-args-and-tuples.py | *args, няколко върнати стойности: общо, min / max / средно, месеци и дни |
| 4 | task4-portfolio-rules.py | Правилата за портфейла като функции: група, етап, клас, продуктова група |
| 5 | task5-predict-scope.py | Предвидете стойностите: локални и глобални имена, list, подаден на функция |
| 6 | task6-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 / return | print показва, 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 macros | macro-то пише код; функцията се изпълнява и връща стойност |
04-functions/homework.py (в homework repository): библиотека от функции за портфейла - прочетете кредитите и рейтинговата скала от файловете, намерете PD на даден рейтинг, очакваната загуба на кредит и сумите по етапи. Всяка проверка трябва да отпечата OK.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):
def stars_decorator(f):
def wrapper():
print("*" * 30)
f()
print("*" * 30)
return wrapper
def greet():
print("Howdy World!")
greet = stars_decorator(greet) # decorate
greet()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):
def stars_decorator(f):
def wrapper():
print("*" * 30)
f()
print("*" * 30)
return wrapper
@stars_decorator
def greet():
print("Howdy World!")
greet()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-а на оригинала. Полезен декоратор - колко време отнема всяка стъпка на един отчет:
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}")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__ е името на функцията.)
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()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).
*args, **kwargs, lambda, docstrings, анотации)Следващото занятие: lists за напреднали - list comprehensions, сортиране с key, zip, enumerate, map и filter и таблица като list от редове.