Functions#

What is a function?#

A reusable piece of code

def FUNC_NAME(ARGUMENTS): 
    BODY

Why do we need functions?#

  • Simplify the maintenance of the code base due to the code reuse

  • Logical structuring since it’s easier to work with smaller pieces of code

  • Self-documenting of the code via using meaningful names for function names and arguments

Function declaration#

def function():
    pass
Hint: The keyword pass is a placeholder for do_nothing statement

Function call and value#

result = function()
print(result)
None
Info: In Python functions always reutrn a value. If the key word return is missing in the function's body, it returns None

A function of little use#

def meaning_of_life():
    return 42
print(meaning_of_life)
print(meaning_of_life())
<function meaning_of_life at 0x1068a3520>
42

A more useful function#

def is_prime(n):
    """
    Returns True iff n is prime
    It's ok to have multiple returns in the function body
    """
    if n == 1:
        return False
    
    for i in range(2, n):
        if n % i == 0:
            return False
    return True 

Now it’s time to test it in the contest!

Scope#

def add_a(x):
    # local scope
    x = x + a 
    return x

# global scope
a = 8
z = add_a(10)
print(z)
18

Variables from the global scope are available in all local scopes

def add_b(x):
    # local scope
    b = 8 
    x = x + b
    return x

# global scope
z = add_b(10)
print(b)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [22], in <cell line: 9>()
      7 # global scope
      8 z = add_b(10)
----> 9 print(b)

NameError: name 'b' is not defined

Variables from the local scope are not available in the global scope

dir() lists the global scope#

A_GLOBAL_SCOPE_CONSTANT = 42
dir()
['A_GLOBAL_SCOPE_CONSTANT',
 'In',
 'Out',
 '_',
 '_23',
 '_24',
 '_25',
 '_26',
 '_3',
 '_5',
 '_6',
 '_7',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '_dh',
 '_i',
 '_i1',
 '_i10',
 '_i11',
 '_i12',
 '_i13',
 '_i14',
 '_i15',
 '_i16',
 '_i17',
 '_i18',
 '_i19',
 '_i2',
 '_i20',
 '_i21',
 '_i22',
 '_i23',
 '_i24',
 '_i25',
 '_i26',
 '_i27',
 '_i28',
 '_i3',
 '_i4',
 '_i5',
 '_i6',
 '_i7',
 '_i8',
 '_i9',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'a',
 'add_a',
 'add_b',
 'exit',
 'function',
 'get_ipython',
 'meaning_of_life',
 'quit',
 'register_cell_magic',
 'result',
 'spades',
 'typecheck',
 'z']
def add_1(x): 
    x = x + 1 # Which x to use? Local or global?
    return x

x = 10 # creates a global variable x
z = add_1(12)
print(z)
print(x)
13
10
def add_1(x): 
    print("id of local x =", id(x))
    x = x + 1 # creates a new local variable x
    print("id of new local x =", id(x))
    return x

x = 10 # creates a global variable x
print("id of global x =", id(x))
z = add_1(12)
id of global x = 4351427088
id of local x = 4351427152
if of new local x = 4351427184

LEGB#

https://lh4.googleusercontent.com/C37NusYKSsIaZzbsmPOk8d-kQJB9i-fOihVFdy1gSTG6e9l_NHq-aUuiTdJPJ4sH9p0iim2bad_7DRmXuNHi1OhKzJjnT100jAfopE7vpZ0_oU-e2LBqiZ7scTrllxAgQrl4wZEJ
n = 1000

def outer():
    n = 100
    def inner(lst):
        return(sum(lst) / n)
    print(inner([1, 2, 3, 42]))
    
outer()
0.48

Aliasing once again#

def list_modify(sample):
    # here sample is local but aliasing!
    last_element = sample.pop()
    return sample

sample = [1, 2, 3] # global variable
new_sample = list_modify(sample)
print(f"old_sample = {sample}, new_sample = {new_sample}")
old_sample = [1, 2], new_sample = [1, 2]
def list_modify(sample, num):
    # use copy to protect the global list
    new_sample = sample.copy()
    new_sample.append(num)
    return new_sample

sample = [1, 2, 3] # global variable
new_sample = list_modify(sample, 42)
print(f"old_sample = {sample}, new_sample = {new_sample}")
old_sample = [1, 2, 3], new_sample = [1, 2, 3, 42]

Positional, keyword and default arguments#

def final_price(price, discount):
    return price - price * discount / 100

# Both arguments are positional
print(final_price(1000, 5))

# The order is important!
print(final_price(5, 1000))
950.0
-45.0
def final_price(price, discount):
    return price - price * discount / 100

# Both are keyword arguments 
print(final_price(price=5000, discount=10))

# Any order of keyword arguments is allowed
print(final_price(discount=2, price=200))

# Positional and keyword
print(final_price(2000, discount=6))
4500.0
196.0
1880.0

All positional arguments should go before keyword ones

print(final_price(price=10000, 20))
  Input In [38]
    print(final_price(price=10000, 20))
                                     ^
SyntaxError: positional argument follows keyword argument
PEP 8: No spaces around the equality sign are allowed for keyword and default arguments
def final_price(price, discount, bonus=0):
    # bonus by default equals 0
    return price - price * discount / 100 - bonus

# No bonus by default
print(final_price(price=5000, discount=10))

# bonus is a keyword argument
print(final_price(price=5000, discount=10, bonus=100))

# A default agrument cannot be positional
# print(final_price(price=5000, discount=10, 100))
4500.0
4400.0

Yet another list trap#

def function(list_argument=[]):
    list_argument.append("Hi!")  
    return list_argument
function()
['Hi!']
function()
['Hi!', 'Hi!']
function()
['Hi!', 'Hi!', 'Hi!']
Anti-pattern: using mutable objects as values of default arguments
Advice: Use None as default argument value
def function(list_argument=None):
    if list_argument is None:
        list_argument = []
    list_argument.append("Hi!")  
    return list_argument
function()
['Hi!']
function()
['Hi!']
function()
['Hi!']

Type hints and annotations#

def relative_difference(x, y):
    delta = x - y
    mean = (x + y) / 2
    if mean == 0.0:
        return None 
    return abs(delta / mean)
relative_difference(1, 100)
1.9603960396039604
help(relative_difference)
Help on function relative_difference in module __main__:

relative_difference(x, y)
import typing as tp

def relative_difference(x: float, y: float) -> tp.Optional[float]:
    """
    Compares two quantities taking into account their absolute values
    And another line just to make an example of multiline docstrings
    """
    delta = x - y
    mean = (x + y) / 2
    if mean == 0.0:
        return None
    return abs(delta / mean)
help(relative_difference)
Help on function relative_difference in module __main__:

relative_difference(x: float, y: float) -> Optional[float]
    Compares two quantities taking into account their absolute values
    And another line just to make an example of multiline docstrings

Variadic arguments#

def root_mean_square(args: tp.List[float]) -> float:
    if not args:
        return 0.0

    squares_sum = sum(x ** 2 for x in args)

    mean = squares_sum / len(args)
    return mean ** 0.5
root_mean_square([4, 8, 15, 16, 23, 42])
21.80978373727412

*args#

def root_mean_square(*args: float) -> float:
    if not args:
        return 0.0
    
    squares_sum = sum(x ** 2 for x in args)

    mean = squares_sum / len(args)
    return mean ** 0.5
root_mean_square(4, 8, 15, 16, 23, 42, 0.34, 1098.3435)
388.78216259112406

**kwargs#

def root_mean_square(*args: float, **kwargs: tp.Any) -> float:
    verbose = kwargs.get('verbose', False)
    if not len(args):
        if verbose:
            print('Empty arguments list!')
        return 0.0
    squares_sum = sum(x ** 2 for x in args)
    if verbose:
        print(f'Sum of squares: {squares_sum}')
    mean = squares_sum / len(args)
    if verbose:
        print(f'Mean square: {mean}')

    return mean ** 0.5
root_mean_square(4, 8, 15, 16, 23, 42, verbose=True)
Sum of squares: 2854
Mean square: 475.6666666666667
21.80978373727412
root_mean_square(verbose=True)
Empty arguments list!
0.0

Lambda functions#

sorted(['привет', 'как', 'дела'])
['дела', 'как', 'привет']
sorted(['привет', 'как', 'дела'], key=lambda string: len(string))
['как', 'дела', 'привет']
def string_len(string):
    return len(string)

Strings#

a = 'The word you are looking for is "Hello".'
b = "I'll wait you there"
c = '''Тройные кавычки
для строк с переносами.
Русский язык поддерживается из коробки.
Как и любой другой'''
"And also" " you can " \
"split them in pieces"
'And also you can split them in pieces'
("And also" " you can "
"split them in pieces")
'And also you can split them in pieces'

Escape sequences#

  • \n — new line character

  • \t — horizontal tab

print('Hey\tFrank!\nHow are you?')
Hey	Frank!
How are you?

Base string methods#

first = 'The Government'
second = '...considers these people "irrelevant".'
print(list(first))
['T', 'h', 'e', ' ', 'G', 'o', 'v', 'e', 'r', 'n', 'm', 'e', 'n', 't']
'irrelevant' in second, 'man' in second
(True, False)

Comparisons#

'a' < 'b'
True
'test' < 'ti'
True
'ёжик' < 'медвежонок'
False
ord('ё') < ord('м')  # 1105 vs 1084
False

ord() and chr()#

ord(ch) returns the Unicode code of the symbol ch, chr(code) gets back the symbol by its code

ord('a'), chr(97)
(97, 'a')
ord("A"), chr(65)
(65, 'A')
ord('ы'), chr(1099)
(1099, 'ы')
ord('€'), chr(8364)
(8364, '€')
spades = '\u2660'
print(spades)
print(ord(spades), hex(ord(spades)), chr(9824))
♠
9824 0x2660 ♠

Register#

test = 'We don\'t.'
test.upper()
"WE DON'T."
test.lower()
"we don't."
test.title()
"We Don'T."

Searching in strings#

secret = 'Hunted by the authorities, we work in secret.'
"by" in secret, "but" in secret
(True, False)
print(secret.count('e')) 
6
secret.find('god'), secret.find('work')
(-1, 30)

Predicates#

"You'll never find us".endswith("find us")  # also: .startswith
True
"16E45".isalnum(), "16".isdigit(), "q_r".isalpha()
(True, True, False)
"test".islower(), "Test Me".istitle()
(True, True)

Split & join#

header = 'ID\tNAME\tSURNAME\tCITY\tREGION\tAGE\tWEALTH\tREGISTERED'
'ID\tNAME\tSURNAME\tCITY\tREGION\tAGE\tWEALTH\tREGISTERED'
print(header.split())
['ID', 'NAME', 'SURNAME', 'CITY', 'REGION', 'AGE', 'WEALTH', 'REGISTERED']
print(', '.join(s.lower() for s in header.split()))
id, name, surname, city, region, age, wealth, registered

Format#

from datetime import datetime

"[{}]: Starting new process '{}'".format(
    datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    'watcher'
)
"[2022-11-17 13:33:07]: Starting new process 'watcher'"
"{1}! My name is {0}!".format("John", "Hi")
'Hi! My name is John!'

f-strings#

name = 'John'
surname = 'Reese'

f'{name} {surname}'
'John Reese'
for value in [0.6, 1.0001, 22.7]:
    print(f'value is {value:07.4f}')
value is 00.6000
value is 01.0001
value is 22.7000
comment = 'Added yet another homework solution'
commit_hash = '7a721ddd315602f94a7d4123ea36450bd2af3e89'
f'{commit_hash=}, {comment=}'
# self-documenting string
"commit_hash='7a721ddd315602f94a7d4123ea36450bd2af3e89', comment='Added yet another homework solution'"
some_random_url = 'https://yandex.ru/images/'
some_random_url.strip('/')  # rstrip, lstrip
'https://yandex.ru/images'

string module#

import string

string.ascii_letters
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
string.ascii_lowercase
'abcdefghijklmnopqrstuvwxyz'
string.punctuation
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
def to_string(*args, sep=" ", end='\n'):
    result = ""
    for arg in args:
        result += str(arg) + sep
    result += end
    return result
to_string(1, 2, 3)
'1 2 3\n'
'1 2 3 \n'