Funtion in python are a first class objects. Now what is a first class object? A program entity is a first class object if it can be:

  • Created at runtime
  • Assigned to a variable or element in a data structure
  • Passed as argument to a function
  • returned as argument to a function
def factorial(n):
    """returns n!"""
    return 1 if n < 2 else n*factorial(n-1)
factorial(42)
1405006117752879898543142606244511569936384000000000
factorial.__doc__
'returns n!'
type(factorial)
function
dir(factorial)
['__annotations__',
 '__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__get__',
 '__getattribute__',
 '__globals__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__kwdefaults__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']
fact = factorial
fact
<function __main__.factorial(n)>
fact(5)
120
map(factorial, range(11))
<map at 0x7eff0c23fc40>
list(map(factorial, range(5)))
[1, 1, 2, 6, 24]

Higher-Order Functions

Having first-class functions enables programming in a functional style. One of the hall-marks of functional programming is the use of Higher-order functions. A function that takes another function as argument or returns a function is called a higher order function. This can be seen in the map example above or the sorted function which has a keyword argumnet key which takes a function.

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
def reverse(word):
    return word[::-1]

reverse('testing')
'gnitset'
sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

map, filter and reduce are the best-know higher order functions. But there are better alternatives to these and we will see them below.

listcomps and genexps replace the functionalities of map and filter and in most cases they are more readable.

list(map(fact, range(6)))
[1, 1, 2, 6, 24, 120]
[fact(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
list(map(fact, filter(lambda n: n%2, range(6))))
[1, 6, 120]
[fact(n) for n in range(6) if n%2]
[1, 6, 120]

In python3 map and filter return generators so their direct substituion is genexps. genexps are more performant for large lists too, so use that when necessary.

from functools import reduce
from operator import add
reduce(add, range(100))
4950
sum(range(100))
4950

There is also all(iterable) and any(iterable) that also have a reducing behavious and built in.

all([0, 0, 0, 1]), all([1, 1, 1, 1,])
(False, True)
any([0, 0, 0, 0]), any([0, 0, 0, 1])
(False, True)

Anonymous Functions

using the lambda keyword, you can create a anonymous function within a python expression. the lambda function cannot be used to create complex functions with while try etc. but its best use is in the context of arguments lists.

for example lets rewrite the reverse function we saw earlier using lambda.

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

the lambda syntax is just syntatic sugar to create a "callable object" which is exactly what a function is.

The Seven Flabors of Callable Objects

the call operator (ie ()) can be applied to other objects as well. To check if an object is callable use callable() fn. There are 7 callable types definied in the Python Data Model

  1. User-defined functions - created with def or lambda
  2. Built-in functions - fn implementation in C. eg len or time.strftime
  3. Built-in methods - methods implemented in C like dict.get
  4. Methods - functions defined in the body of a class
  5. Classes - creating new instance of a class calls the __new__ and __init__ fns
  6. Class instances - if a class defines __call__ method
  7. Generator functions - fns/methods that have yeild keyword
callable(map)
True

User-defined Callable types

not only are Python functions real object, arbitary Python objects may also be made to behave like functions by implementing a __call__ instance method. This helps create function-like objects that have some internal state that must be kept across invocations.

import random

class BingoCage:
    
    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)
        
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')
            
    def __call__(self):
        return self.pick()
bingo = BingoCage(range(3))
bingo.pick()
2
bingo()
0

Function Introspection

function, as we mentioned earlier are first-class objects. Now lets look at the factors that make its so.

> > dir(factorial)

The __dict__ attribute:Like instances of plain user-defined class, a function uses __dict__ attribute to store user attributes assigned to it. Looking closer at attributes that are specific to functions. To see the detailed explaination of each attribute ref Table-5.1 pg 148.

class C: pass
obj = C()

def func(): pass

sorted(set(dir(func)) - set(dir(obj)))
['__annotations__',
 '__call__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__get__',
 '__globals__',
 '__kwdefaults__',
 '__name__',
 '__qualname__']

From Positional to Keywork-Only Parameters

Python has an extreamly flexible parameter handling mechanism as you can see with the exaple below.

def tag(name, *content, cls=None, **attrs):
    """Generates one or more HTML tags"""
    if cls is not None:
        attrs['class'] = cls
        
    if attrs:
        attr_str = ' '.join('%s="%s"' % (attr, value)
                          for attr, value
                          in sorted(attrs.items()))
    else:
        attr_str = ''
    if content:
        return '\n'.join('<%s%s>%s</%s>' %
                         (name, attr_str, c, name) for c in content)
    else:
        return '<%s%s />' % (name, attr_str)
tag('br')
'<br />'
tag('p', 'hello')
'<p>hello</p>'
tag('p', 'hello', 'world')
'<p>hello</p>\n<p>world</p>'
tag('p', 'hello', id=33)
'<pid="33">hello</p>'
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
'src': 'sunset.jpg', 'cls': 'framed'}
tag(**my_tag)
'<imgclass="framed" src="sunset.jpg" title="Sunset Boulevard" />'

Retrieving Information about Parameters

An interesting application of function introspection can be seen in Bobo HTTP micro-framework.

import bobo

@bobo.query('/')
def hello(person):
    return 'Hello %s!' % person

What's special about this is that the decorator @bobo.query() interospects the hello function and understands that it expects one argument. It then parses the response and return the person to the function. All this without the user having to work with the response.

This is achieved using various attributes like __defaults__ and __kwdefaults__, __code__ etc. Lets see another example to see better.

def clip(text, max_len=80):
    """
    Return text clipped at the last space before or afte max_len.
    """
    
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else: 
            space_after = text.rfind(' ', max_len)
            if space_after >= 0:
                end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()
clip.__defaults__
(80,)
clip.__code__
<code object clip at 0x7eff0c1ce920, file "<ipython-input-50-c90bf6018cd7>", line 1>
clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
clip.__code__.co_argcount
2
clip.__defaults__, clip.__name__, clip.__kwdefaults__
((80,), 'clip', None)

Now this is kind off a messy way to learn about the functions but fortunately there is the inspect module.

from inspect import signature
sig = signature(clip)
sig
<Signature (text, max_len=80)>
str(sig)
'(text, max_len=80)'
for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

as you can see this is a much better way to parse the argument info. signature also has a annotation attribute to give info about annotations.

inspect.Signature object also has a bind function that takes any number of arguments and binds them to the parameters. This can be used in frameworks to do validation of arguments before invocations. This is the same machinery the interpreter uses to bind arguments to formal parameters in function calls

sig = signature(tag)
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
          'src': 'sunset.jpg', 'cls': 'framed'}
bound_args = sig.bind(**my_tag)
bound_args
<BoundArguments (name='img', cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>
for name, value in bound_args.arguments.items():
    print(name, '=', value)
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
del my_tag['name']
bound_args = sig.bind(**my_tag)

TypeErrorTraceback (most recent call last)
<ipython-input-67-26e5f49b96e2> in <module>
      1 del my_tag['name']
----> 2 bound_args = sig.bind(**my_tag)

/usr/lib/python3.8/inspect.py in bind(self, *args, **kwargs)
   3023         if the passed arguments can not be bound.
   3024         """
-> 3025         return self._bind(args, kwargs)
   3026 
   3027     def bind_partial(self, /, *args, **kwargs):

/usr/lib/python3.8/inspect.py in _bind(self, args, kwargs, partial)
   2938                             msg = 'missing a required argument: {arg!r}'
   2939                             msg = msg.format(arg=param.name)
-> 2940                             raise TypeError(msg) from None
   2941             else:
   2942                 # We have a positional argument to process

TypeError: missing a required argument: 'name'

Function Annotations

You have seen above how the Python data model with the help of inspect exposes the same machinery the interpreter uses to bind arguments to formal parameters in function call. Function annotation enhances the possible uses of this.

def clip(text:str, max_len:'int>0' = 0) -> str:
    """
    Return text clipped at the last space before or afte max_len.
    """
    
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else: 
            space_after = text.rfind(' ', max_len)
            if space_after >= 0:
                end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()
# its not used by the python interpreter

sig = signature(clip)
sig.return_annotation
str
for param in sig.parameters.values():
    note = repr(param.annotation).ljust(13)
    print(note, ':', param.name, '=', param.default)
<class 'str'> : text = <class 'inspect._empty'>
'int>0'       : max_len = 0

These annotation can be used with tools like mypy to bring the benifits of static type checking into python.

Packages for Functional Programming

Althou python is not a functional language, a functional coding style can be brought in by the operator & functools libs.

operator module

Provides some arithmetic operators as a functions for when you need em.

%%timeit
from functools import reduce

def fact(n):
    return reduce(lambda a, b: a*b, range(1, n+1))

fact(5)
1.03 µs ± 15.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%%timeit
# operator has these functions for you!
from operator import mul

def fact(n):
    return reduce(mul, range(1, n+1))

fact(5)
820 ns ± 2.44 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Another group of one-trick lambdas that operator replaces are functions to pick items from sequences or read attributes from objects: itemgetter and attrgetter actually build custom functions to do that.

metro_data = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

from operator import itemgetter
# sort based of tuple[2]
for city in sorted(metro_data, key=itemgetter(2)):
    print(city)
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
cc_name = itemgetter(1, 0)
for city in metro_data:
    print(cc_name(city))
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')

itemgetter supports seqences and mappings (any object with a __getitem__

Simalar to that is attrgetter, which creates a functions to extract ojbect attributes by name. It will also get attributes that require the .(dot)

from collections import namedtuple
LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
               for name, cc, pop, (lat, long) in metro_data]
metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
metro_areas[0].coord.lat
35.689722
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')

for city in sorted(metro_areas, key=attrgetter('coord.lat')):
    print(name_lat(city))
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
import operator
[name for name in dir(operator) if not name.startswith('_')]
['abs',
 'add',
 'and_',
 'attrgetter',
 'concat',
 'contains',
 'countOf',
 'delitem',
 'eq',
 'floordiv',
 'ge',
 'getitem',
 'gt',
 'iadd',
 'iand',
 'iconcat',
 'ifloordiv',
 'ilshift',
 'imatmul',
 'imod',
 'imul',
 'index',
 'indexOf',
 'inv',
 'invert',
 'ior',
 'ipow',
 'irshift',
 'is_',
 'is_not',
 'isub',
 'itemgetter',
 'itruediv',
 'ixor',
 'le',
 'length_hint',
 'lshift',
 'lt',
 'matmul',
 'methodcaller',
 'mod',
 'mul',
 'ne',
 'neg',
 'not_',
 'or_',
 'pos',
 'pow',
 'rshift',
 'setitem',
 'sub',
 'truediv',
 'truth',
 'xor']

Similar to itemgetter and attrgetter, methodcaller also returns a function that calls a method by name on the object given as argument.

from operator import methodcaller
s = 'The time has home'
upcase = methodcaller('upper')
upcase(s)
'THE TIME HAS HOME'
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)
'The-time-has-home'

Here, in the last case you can see methodcaller froze 2 arguments to the replace function.

functools.partial

The functools module brings together a handful of higher-order functions. The best know function is probably reduce. The other most popular functions are partial and partialmethod

from operator import mul
from functools import partial

triple = partial(mul, 3)
triple(7)
21
[triple(n) for n in range(10)]
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]

partial takes a callable function name and any number of positional or keyword arguments and generates a new function with the args in place. This is effective in situations where you want to pass a callback to an API that expects fewer args that that is there in the function. partial takes a callable as first argument, followed by an arbitrary number of positional and keyword arguments to bind.

partialmethod does the same thing but for methods.