Functions and other things

By on

🐍 Functions and etc.

Viktor Tiulpin @ SPbPU · 2021


Syntax 1

def get_lectures_stream(): 
  return "youtu.be/dQw4w9WgXcQ"

>>> get_lectures_stream()
"youtu.be/dQw4w9WgXcQ"

Syntax 2

  • name: number can’t be the first symbol, but almost any unicode symbol can be used
  • return: if none, then returns None
def get_lectures_stream(): 
  "youtu.be/dQw4w9WgXcQ" 

>>> get_lectures_stream()
None

Syntax 3

def get_lectures_stream() -> str:
  """Returns the last lecture stream."""
  return "youtu.be/dQw4w9WgXcQ"

1️⃣ What is First-Class object

  • Created at runtime
  • Assigned to a variable or element in a data structure
  • Passed as an argument to a function
  • Returned as the result of a function

Integers, strings, dictionaries, and functions in Python

🚀 Function as an object

>>> get_lectures_stream.__doc__
'Returns the last lecture stream.'

🚲 Let’s create a function


Let’s create a function 1

def min(x, y):
  return x if x < y else y

>>> min(-5, 0)
-5

Let’s create a function 2

def min(x, y):
  return x if x < y else y

>>> min(a=-5, b=0)
-5

Let’s create a function 3

def min(x, y):
  return x if x < y else y

>>> min(x=-5, z=0)
# ???

Let’s upgrade the function!

>>> min(1, 2, 3)
1
>>> min({1, 2, 3})
1
>>> bounded_min(-42, 0, 42, low=0, high=255)
0

📦 Packing and unpacking


We did it!

def min(*args):  # what's type(args)?
  res = float("inf")
  for arg in args:
    if arg < res:
      res = arg
  return res

>>> min(-5, 12, 13)
-5
>>> min()  # how to require at least 1 arg?
inf

The solution

def min(first, *args):
  res = first
  # ...

🧐 How to apply the function to a collection? (list, tuple, set)


The answer: unpacking

>>> xs = {-5, 12, 13}
>>> min(*xs)
-5
>>> min(*[-5, 12, 13])
-5
>>> min(*(-5, 12, 13))
-5

Let’s get back…

>>> def bounded_min(first, *args, lo=float("-inf"), hi=float("inf")):
        res = hi
        for arg in (first, ) + args:  # what's the algorithmic complexity?
            if arg < res and lo < arg < hi:
                res = arg
        return max(res, lo)
...
>>> bounded_min(-5, 12, 13, lo=0, hi=255)
12

👀 What’s wrong?

>>> def unique(iterable, seen=set()):
        acc = []
        for item in iterable:
            if item not in seen:
                seen.add(item)
                acc.append(item)
        return acc

Hint: when do default values initialize?


>>> xs = [1, 1, 2, 3]
>>> unique(xs)
[1, 2, 3]
>>> unique(xs)
[]
>>> unique.__defaults__
({1, 2, 3},)

How to solve it?


The solution

>>> def unique(iterable, seen=None):
        seen = set(seen or []) # None – falsy.
        acc = []
        for item in iterable:
            if item not in seen:
                seen.add(item)
                acc.append(item)
        return acc
...
>>> xs = [1, 1, 2, 3]
>>> unique(xs)
[1, 2, 3]
>>> unique(xs)
[1, 2, 3]

🤔 How-to: positional-only parameters

>>> def divmod(a, b, /, key=None):  # specify keyword after '/'
        return (a // b, a % b)
... 
>>> divmod(a=1, b=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: divmod() got some positional-only arguments passed as keyword arguments: 'a, b'

🤔 How-to: keyword-only parameters

>>> def flatten(xs, *, depth=None):
        pass
...
>>> flatten([1, [2], 3], 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: flatten() takes 1 positional argument [...]

>>> def runner(cmd, **kwargs):
        if kwargs.get("verbose", True):
            print("Logging enabled")
...
>>> runner("mysqld", limit=42)
Logging enabled
>>> runner("mysqld", **{"verbose": False})
>>> options = {"verbose": False}
>>> runner("mysqld", **options)

🚀 Assignment (unpacking)

>>> x, y, z = [1, 2, 3]
>>> x, y, z = {1, 2, 3} # ???
>>> x, y, z = "xyz"
>>> first, *rest = range(1, 5)
>>> first, rest
(1, [2, 3, 4])
>>> first, *rest, last = range(1, 5)
>>> last
4
>>> for a, *b in [range(4), range(2)]:
        print(b)
...
[1, 2, 3]
[1]

Let’s look inside one time only

>>> import dis
>>> dis.dis("first, *rest, last = ('a', 'b', 'c')")
0 LOAD_CONST 4 (('a', 'b', 'c'))
3 UNPACK_EX 257
6 STORE_NAME 0 (first)
9 STORE_NAME 1 (rest)
12 STORE_NAME 2 (last)
15 LOAD_CONST 3 (None)
18 RETURN_VALUE
>>> first, *rest, last = ['a', 'b', 'c'] # ???

Functions wrap up

  • basic syntax
  • minimal example
  • packing, unpacking

👁 Scopes


>>> def wrapper():
        def identity(x):
            return x
        return identity
...
>>> f = wrapper()
>>> f(42)
42

LEGB Rule: LocalEnclosingGlobalBuilt-in
The search is done during execution, not definition.

>>> min                 # builtin
<built-in function min>
>>> min = 42            # global
>>> def f(*args):
        min = 2
        def g():        # enclosing
            min = 4     # local
            print(min)
...

🤔 Is Python a functional programming language?


Guido van Rossum

I have never considered Python to be heavily influenced by functional languages, no matter what people say or think. I was much more familiar with imperative languages such as C and Algol 68 and although I had made functions first-class objects, I didn’t view Python as a functional programming language.


We have anonymous functions!

>>> lambda arguments: expression
# which is ...
>>> def <lambda>(arguments):
        return expression
>>> lambda foo, *args, bar=None, **kwargs: 42
<function <lambda> at 0x100fb9730>

map applies the given function to every iterable (sequence element)

>>> map(indentity, range(4))
<map object at 0x100fc4c88>
>>> list(map(identity, range(4)))
[0, 1, 2, 3]
>>> set(map(lambda x: x % 7, [1, 9, 16, -1, 2, 5]))
{1, 2, 5, 6}
>>> map(lambda s: s.strip(), open("./HBA1.txt"))
<map object at 0x100fc4cc0>

filter removes any element, which is not satisfying the given predicat

>>> filter(lambda x: x % 2 != 0, range(10))
<filter object at 0x1011edfd0>
>>> list(filter(lambda x: x % 2 != 0, range(10)))
[1, 3, 5, 7, 9]

zip builds tuple sequence from elements of multiple sequences

>>> list(zip("abc", range(3), [42j, 42j, 42j]))
[('a', 0, 42j), ('b', 1, 42j), ('c', 2, 42j)]

We have sequence (list, dict, …) generators

>>> [x ** 2 for x in range(10) if x % 2 == 1]
[1, 9, 25, 49, 81]

is better than

>>> list(map(lambda x: x ** 2,
         filter(lambda x: x % 2 == 1,
                range(10))))
[1, 9, 25, 49, 81]

Which can be nested

>>> nested = [range(5), range(8, 10)]
>>> [x for xs in nested for x in xs] # flatten
[0, 1, 2, 3, 4, 8, 9]

Sets and dictionaries generators

>>> {x % 7 for x in [1, 9, 16, -1, 2, 5]}
{1, 2, 5, 6}
>>> date = {"year": 2014, "month": "September", "day": ""}
>>> {k: v for k, v in date.items() if v}
{'month': 'September', 'year': 2014}
>>> {x: x ** 2 for x in range(4)}
{0: 0, 1: 1, 2: 4, 3: 9}

🔥 Scopes and functional programminng wrap up

  • LEGB Rule: LocalEnclosingGlobalBuilt-in
  • one-line lambdas
  • map, filter, zip
  • generators

❓ How to generate 2D array of zeros in Python?


� PEP-8


Why? To improve readability. Basic rules:

  • 4 spaces for indentation
  • 79 symbols for a line of code 🤔
  • variables_and_functions, CONSTANTS_YEAH, JustClasses
  • think how it can be read

To check equality…

  • for objects: != and ==
  • for singletons: is and is not
  • for booleans: use the bool itself, e.g. if foo:

Write like it’s English

  • if not key in d 🤢
  • if key not in 🤗

And write docstrings!


Full set of rules: https://www.python.org/dev/peps/pep-0008/


🏁 Any questions?