🐍 Python - Functions
Function declaration.
def add(x: int, y: int) -> int:
return x + y
assert add(2, 3) == 5
assert add(y=3, x=5) == 8
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
assert add(5, 3) == 8
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
def add_10(x: int) -> int:
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.
class Adder:
def __init__(self, n: int) -> None:
self.n: int = n
def __call__(self, x: int) -> int:
return self.n + x
assert callable(Adder)
plus_3 = Adder(3)
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)
foo(10, 30) # BAD
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.
# BAD, BAD, BOY
from typing import Iterable, Optional
def bad(numbers: Iterable[int] = []) -> Iterable[int]:
numbers.append(123)
return numbers
assert bad() == [123]
assert bad() == [123, 123]
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
# bad
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('header', style="color: red;") == '<header style="color: red;" />'
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']
assert add_person(people, 'John') is None
assert people == ['Alice', 'Bob', 'John']
Sources
- Style Guide For Python Code
- Django Coding Style
- Google Python Guide
- Python Newbie Mistakes
- Learn Python in Y minutes
- Python’s null equivalent: None
- Python Idioms
- Python Best Practice Patterns
- Become More Advanced: Master the 10 Most Common Python Programming Problems
- Python Tricks The Book, Dan Bader
- Fluent Python, Luciano Ramalho