ruk·si

# 🐍 Python - Functions

Updated at 2018-06-11 02:27

Function declaration.

def add(x: int, y: int) -> int:
return x + y

Lambdas are anonymous functions with a single expression. If you find a lambda that is hard to understand, refactor it into a normal function. A well-defined function always beats a lambda. Lambdas are so bad that the Python standard library even has operator module that contains usual stuff you would use a lambda for.

from typing import Callable

f: Callable[[int], bool] = (lambda x: x > 2)
assert f(3) is True
assert f(2) is False

add: Callable[[int, int], int] = lambda x, y: x + y

tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')]
print(sorted(tuples, key=lambda x: x[1]))
# => [(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]

Python has first-class functions. Functions can be stored to variables and they can be passed around.

from typing import Callable

funcs = [str.lower, str.upper]

print(funcs[0]('TiCk ToCk'))
# => tick tock

for f in funcs:
print(f, f('TiCk ToCk'))
# => (<method 'lower' of 'str' objects>, 'tick tock')
# => (<method 'capitalize' of 'str' objects>, 'TICK TOCK')

def greet(func: Callable[[str], str]) -> None:
greeting = func('Hi, I am a Python program')
print(greeting)

greet(funcs[1])
# => HI, I AM A PYTHON PROGRAM

print(list(map(funcs[1], ['hello', 'hey', 'hi'])))
# => ['HELLO', 'HEY', 'HI']

def create_logger(name: str) -> Callable[[str], None]:
def logger(message: str) -> None:
print('%s: %s' % (name, message))

return logger

log = create_logger('Controller')
log('SNAFU!')  # Controller: SNAFU!

Always consider using built-in functions. They are written in C so they are fast.

# http://docs.python.org/library/functions.html
return x + 10

assert list(map(add_10, [1, 2, 3])) == [11, 12, 13]
assert list(filter(lambda x: x > 5, [3, 4, 5, 6, 7])) == [6, 7]

Objects become callable if you define a __call__ function.

def __init__(self, n: int) -> None:
self.n: int = n

def __call__(self, x: int) -> int:
return self.n + x

assert callable(plus_3)
assert plus_3(4) == 7
import random
from typing import Any, Iterable, List

class BingoCage:

def __init__(self, items: Iterable[Any]) -> None:
self._items: List[Any] = list(items)
random.shuffle(self._items)

def __call__(self) -> Any:
try:
return self._items.pop()
except IndexError:
raise LookupError(f'call to empty {self.__class__.__name__}')

bingo = BingoCage(range(3))
assert callable(bingo)
assert isinstance(bingo(), int)
assert isinstance(bingo(), int)
assert isinstance(bingo(), int)
# assert isinstance(bingo(), int) # => LookupError: pick from empty BingoCage

Learn about *args and **kwargs. *args and **kwargs allow you to pass a variable number of arguments to a function. Only the *s are necessary. Writing *args and **kwargs is just a convention.

## Optional Arguments

You define a default value for optional arguments.

def greet(text: str = 'Hello') -> str:
return f'{text}!'

assert greet() == 'Hello!'
assert greet('Moi') == 'Moi!'

Use labelled parameters with default parameters. Always use labelled parameters when calling a function with default parameters. Helps extendibility when adding new default variables.

def foo(a: int, b: int = 1, c: int = 2) -> None:
pass

foo(10)
foo(10, b=20)
foo(10, c=30)

Don't use mutable variables in default arguments. None should be reserved for "argument wasn't passed" in these cases. The most common bugs are using lists or dictionaries as a default argument.

from typing import Iterable, Optional

def bad(numbers: Iterable[int] = []) -> Iterable[int]:
numbers.append(123)
return numbers

assert bad() == [123, 123, 123]
# so don't do this

# GOOD
def good(numbers: Optional[Iterable[int]] = None) -> Iterable[int]:
if numbers is None:
numbers = []
numbers.append(123)
return numbers

assert good() == [123]
assert good() == [123]
assert good() == [123]

Don't use a result of a function as a default function parameter. It will only use the value that it returned when it was defined.

import time

def print_now(now: float = time.time()) -> float:
return now

n = print_now()
assert print_now() == print_now() == print_now()
assert print_now() == print_now() == print_now()
assert n == print_now()

Clone mutable variables in your class constructors. If you don't, internals of the class can be modified outside of the class.

from typing import Iterable, List

class Bus:
passengers: List[str]

def __init__(self, passengers: Iterable[str] = None) -> None:
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)  # the cloning

def pick(self, name: str) -> None:
self.passengers.append(name)

def drop(self, name: str) -> None:
self.passengers.remove(name)

team = ['Jordan', 'Sue', 'Alex']
bus = Bus(team)
assert bus.passengers == ['Jordan', 'Sue', 'Alex']
bus.drop('Alex')
assert bus.passengers == ['Jordan', 'Sue']
assert team == ['Jordan', 'Sue', 'Alex']
# so team and bus.passengers are separate, good

## Undefined Arguments

Pass undefined arguments with *args and **kwargs The names args and kwargs are just a convention, follow that naming if it makes sense.

def foo(required, *args, **kwargs) -> None:
print(required)
if args:
print(args)
if kwargs:
print(kwargs)

foo('I am required', 'I am optional', john='I am optional and I have a name')
# => I am required
# => ('I am optional',)
# => {'john': 'I am optional and I have a name'}

You can modify the undefined arguments before passing them forward. Passing optional arguments forward is especially useful with subclasses and decorator functions.

def foo(required, *args, **kwargs) -> None:
print(required)
if args:
print(args)
if kwargs:
print(kwargs)

def bar(required, *args, **kwargs) -> None:
new_args = args + ('something-extra', )
kwargs['person'] = 'Bob'
foo(required, *new_args, **kwargs)

bar('I am required', 'I am optional', john='I am optional and I have a name')
# => I am required
# => ('I am optional', 'something-extra')
# => {'john': 'I am optional and I have a name', 'person': 'Bob'}

You can mix-and-match named arguments and argument unpacking.

from typing import Optional

def tag(name: str, *contents, cls: Optional[str] = None, **attrs) -> str:

if cls is not None:
attrs['class'] = cls

if attrs:
attrs_str = ''.join(f' {attr}="{value}"' for attr, value in sorted(attrs.items()))
else:
attrs_str = ''

if contents:
return ''.join(f'<{name}{attrs_str}>{c}</{name}>' for c in contents)
else:
return f'<{name}{attrs_str} />'

assert tag('br') == '<br />'
assert tag('p', 'hello') == '<p>hello</p>'
assert tag('p', 'hello', 'world') == '<p>hello</p><p>world</p>'
assert tag('div', 'John', id=123) == '<div id="123">John</div>'
assert tag('footer', cls="small") == '<footer class="small" />'

## Function Returns

Python functions return None if nothing else is specified.

def i_return_none():
pass

assert i_return_none() is None

Add explicit return None to functions with one or more return keywords. Using implicit None is misleading and error-prone in functions with multiple possible returns.

from typing import Optional

# BAD, even the type is wrong in here
def foo(name: str) -> str:
if name:
return name.upper()

# GOOD
def bar(name: str) -> Optional[str]:
if name:
return name.upper()
return None

You can return multiple values. Returning multiple values in Python wraps the values in a tuple. You can easily unpack them with Python multiple assignment.

from typing import Tuple

def get_full_name() -> Tuple[str, str]:
return 'Ruksi', 'Korpisara'  # implicit cast to a tuple

assert get_full_name() == ('Ruksi', 'Korpisara')

first_name, last_name = get_full_name()  # multiple assignment
assert first_name == 'Ruksi'
assert last_name == 'Korpisara'

Consider returning self to allow function chaining. But be consistent with this behavior.

class Reactor:

def release_water(self) -> 'Reactor':
print('releasing water')
return self

def shutdown(self) -> 'Reactor':
print('shutting down')
return self

def alarm(self) -> 'Reactor':
print('alarm triggered')
return self

r = Reactor()
r.release_water().shutdown().alarm()

If chaining doesn't make sense, explicitly return None. Functions that change an object in place should return None to make it clear that the object itself was changed.

from typing import List

def add_person(people: List[str], person: str) -> None:
people.append(person)
return None

people = ['Alice', 'Bob']