Last modified on 01 Oct 2021.

Facts

  • In python, function is the first-class object. It’s can be used as an argument.
  • Function can be used as a return value.
    • return func1: reference to func1.
    • return func1(): results of evaluating func().
  • Should we check the argument/input?: No! The responsibility is on the caller! Your function should be well-documented, that’s it![ref]

Create a normal function

If a function doesn’t return any value, it returns None.

# without arguments
def func_name():
  pass
# with arguments
def func_name(<args>):
  pass
# return
def func_name(<args>):
  return <some_thing>
# call a function
func_name(<args>)

Unpacking a function

def sum_and_div(num1, num2):
  sum_nums = num1 + num2
  div_nums = num1 / num2
  return sum_nums, div_nums # multiple returns

sum, div = sum_and_div(18, 9)
print(sum, div)
27 2.0

Functions with stars (*args and **kwargs)

[ref]The *args will give you all function parameters as a tuple:

def foo(*args):
  print(args)
  for a in args:
    print(a)

foo(1)
foo(2, 3, 4)
(1,)
1
(2, 3, 4)
2
3
4
def foo(rarg1, rarg2):
  print(rarg1, rarg2)

lst = [1, 2]
foo(*lst)
tpl = (3, 4)
foo(*tpl)
1 2
3 4

If you wanna use “keywords arguments”, use **args:

def kwfunc(**kwargs):
  print(type(kwargs))
  print(kwargs)

kwfunc()
kwfunc(kw1="thi", kw2="dinh")
<class 'dict'>
{}
<class 'dict'>
{'kw1': 'thi', 'kw2': 'dinh'}

Use a dictionary as an input,

def kwfunc(**kwargs): # must have **
  print(kwargs)

kwargs = {'kw1': "thi", 'kw2': "dinh"}
kwfunc(**kwargs) # must have **
{'kw1': 'thi', 'kw2': 'dinh'}
def kwfunc(kw1="john", kw2="doe"):
  print(kw1, kw2)

kwargs = {'kw1': "thi", 'kw2': "dinh"}

kwfunc()
kwfunc(kwargs) # goes to kw1
kwfunc(**kwargs) # goes to both kw1 & kw2
john doe
{'kw1': 'thi', 'kw2': 'dinh'} doe
thi dinh

Coupling rargs, *args and **kwargs:

  • Required positional arguments: rarg1, rarg2, …
  • Optional positional arguments: *args.
  • Optional key-values arguments: **kwargs.
def kwfunc(rarg1=0, rarg2=0, *args, **kwargs):
  print("required args: ", rarg1, rarg2)
  if args:
    print("*args: ", args)
  if kwargs:
    print("**kwargs: ", kwargs)
  print("\n")


kwfunc()
kwfunc(1, 2)
kwfunc(3, 4, 5, 6)
kwfunc(kw1="thi", kw2="dinh")
required args:  0 0

required args:  1 2

required args:  3 4
*args:  (5, 6)

required args:  0 0
**kwargs:  {'kw1': 'thi', 'kw2': 'dinh'}

All arguments after * must be key-value arguments,

def func(rarg1, rarg2, *, kwarg1, kwarg2):
  print("required args: ", rarg1, rarg2)
  print("kwarg*: ", kwarg1, kwarg2)

# func(1, 2, 3, 4) # error!
func(1, 2, kwarg1=3, kwarg2=4)
required args:  1 2
kwarg*:  3 4

Lambda function

It’s convenient but don’t use it regularly, use def (in 1 line) instead.

x = lambda a : a + 10
print(x(5))

# you can use this
def x(a): return a + 10
15
# if else with lambda function
lambda row: 'good' if (row>=80) else ('bad' if row<80 else '')

Check input & raise error

Something like that,

if par1 is None:
  msg = "par1 must be in type `int`"
  raise TypeError(msg)

You can check other exceptions here.

Decorators

def my_decorator(func):
    def wrapper():
        print("Before func called.")
        func()
        print("After func called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)
say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

In a class (note that, there is no self parameter in _deco),

class test_class():

    def _deco(func):
        def wrapper(self, name):
            print('before func called')
            func(self, name)
            print('after func called')
        return wrapper

    @_deco
    def fit(self, name):
        print('Hello, ', name)

a = test_class()
a.fit('thi')
before func called
Hello,  thi
after func called