<jupyter_start><jupyter_text>First Class Functions Functions in Python are first-class objects. Programming language theorists define a“first-class object” as a program entity that can be:* 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<jupyter_code># The __doc__ attribute:
print(str.__doc__)
help(str)
# Example 5-1. Create and test a function, then read its __doc__ and check its type

def factorial(n):
    '''returns n!'''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial(42))

print(factorial.__doc__) # prints function comment/documentation

print(type(factorial))
# Also works with the help function
help(factorial)

# the text is from the __doc__ attribute of the function object
# Example 5-2. Use function through a different name, and pass function as argument

fact = factorial
print(fact)
print(fact(5))
print(map(factorial, range(11)))
print(list(map(fact, range(11))))<jupyter_output><function factorial at 0x7f13882b81e0>
120
<map object at 0x7f138826e198>
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]<jupyter_text>higher order functionsA **function that takes a function as argument or returns a function as the result** is ahigher-order function. One example is map, shown in Example 5-2. Another is thebuilt-in function sorted: an optional key argument lets you provide a function to beapplied to each item for sortingFor example, to sort a list of words by length, simply pass the len function as the key<jupyter_code>fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)<jupyter_output><empty_output><jupyter_text>Any one-argument function can be used as the key. For example, to create a rhymedictionary it might be useful to sort each word spelled backward. In Example 5-4,note that the words in the list are not changed at all; only their reversed spelling isused as the sort criterion, so that the berries appear together<jupyter_code>def reverse(word):
    return word[::-1]

print(reverse('testing'))

print(sorted(fruits, key=reverse))
# Test KWargs

def test(**someKWargs):
    [print(f'{type(y)} {x}, {y}') for (x,y) in someKWargs.items()]

test(test='test', test2='test2', test3=['test', 'test2'], test4={'test':'test', 'test2':'test2'}, test5=('test1', 'test2'))
from functools import reduce
from operator import add
print(reduce(add, range(100)))
print(sum(range(100)))<jupyter_output>4950
4950<jupyter_text>Anonymous FunctionsThe **lambda** keyword creates an anonymous function within a Python expressionHowever, the simple syntax of Python limits the body of lambda functions to be pureexpressions. In other words, the body of a lambda cannot make assignments or useany other Python statement such as while, try, etc.<jupyter_code>fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=lambda word: word[::-1])<jupyter_output><empty_output><jupyter_text>Callable ObjectsThe call operator (i.e., **()**) may be applied to other objects beyond user-defined func‐tions. To determine whether an object is callable, use the callable() built-in func‐tion. The Python Data Model documentation lists seven callable types:**User-defined functions**Created with def statements or lambda expressions.**Built-in functions**A function implemented in C (for CPython), like len or time.strftime.**Built-in methods**Methods implemented in C, like dict.get.**Methods**Functions defined in the body of a class.**Classes**When invoked, a class runs its \__new__ method to create an instance, then\__init__ to initialize it, and finally the instance is returned to the caller. Becausethere is no new operator in Python, calling a class is like calling a function. (Usu‐ally calling a class creates an instance of the same class, but other behaviors arepossible by overriding \__new__. **Class instances**If a class defines a \__call__ method, then its instances may be invoked as functions. **Generator functions**Functions or methods that use the yield keyword. When called, generator functions return a generator object.<jupyter_code># Given the variety of existing callable types in Python, the safest way
# to determine whether an object is callable is to use the callable() built-in:

[callable(obj) for obj in (abs, str, 13)]<jupyter_output><empty_output><jupyter_text>User-Defined Callable TypesNot only are Python functions real objects, but arbitrary Python objects may also bemade to behave like functions. Implementing a __call__ instance method is all it takes.<jupyter_code># Example 5-8 implements a BingoCage class. An instance is built from any iterable,
# and stores an internal list of items, in random order. Calling the instance pops an item.

import random

class BingoCage:

    # __init__ accepts any iterable; building a local copy prevents unexpected side effects on any list passed as an argument
    def __init__(self, items):
        self._items = list(items)
        # shuffle is guaranteed to work because self._items is a list
        random.shuffle(self._items)

    # The main method
    def pick(self):
        try:
            return self._items.pop()
        except IndexError: 
            # Raise exception with custom message if self._items is empty
            raise LookupError('pick from empty BingoCage')

    # Shortcut to bingo.pick(): bingo()
    def __call__(self):
        return self.pick()
    
bingo = BingoCage(range(3))
print(bingo.pick()) # example calling pick
print(bingo()) # example calling class callable (calls bingo.pick())
print(callable(bingo)) # class is callable<jupyter_output>0
2
True<jupyter_text>Function Introspective<jupyter_code>print(dir(factorial))
# Example of treating a function like an object, assigning a variable to a function and reading it out using __dict__


def upper_case_name(obj):
    return ("%s %s" % (obj.first_name, obj.last_name)).upper()

print(upper_case_name.__dict__)

upper_case_name.short_description = 'Customer name'

print(upper_case_name.__dict__)

# Example 5-10. tag generates HTML; a keyword-only argument cls is used to pass
# “class” attributes as a workaround because class is a keyword in Python

def tag(name, *content, cls=None, **attrs):
    """Generate 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)
    
    
print(tag('br'))
print(tag('p', 'hello'))
print(tag('p', 'hello', 'world'))
print(tag('p', 'hello', id=33))
print(tag('p', 'hello', 'world', cls='sidebar'))
print(tag(content='testing', name="img"))
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
print(tag(**my_tag))<jupyter_output><br />
<p>hello</p>
<p>hello</p>
<p>world</p>
<p id="33">hello</p>
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
<img content="testing" />
<img class="framed" src="sunset.jpg" title="Sunset Boulevard" /><jupyter_text>Keyword only functionsKeyword-only arguments are a new feature in Python 3. In Example 5-10, the clsparameter can only be given as a keyword argument—it will never capture unnamedpositional arguments. To specify keyword-only arguments when defining a function,name them after the argument prefixed with *. If you don’t want to support variablepositional arguments but still want keyword-only arguments, put a * by itself in thesignature, like this:<jupyter_code>def f(a, *, b):
    return a, b

f(1, b=2)

# Note that keyword-only arguments do not need to have a default value: they can be mandatory, like b in the preceding example.
# example of how to retrieve function arguments and defaults

def clip(text, max_len=80):
    """Return text clipped at the last space before or after 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: # no spaces were found
        end = len(text)
    return text[:end].rstrip()

print(clip.__defaults__)
print(clip.__code__)
print(clip.__code__.co_varnames)
print(clip.__code__.co_argcount)

# The argument names appear in __code__.co_varnames, but that also includes the names
# of the local variables created in the body of the function. Therefore, the argument
# names are the first N strings, where N is given by __code__.co_argcount which—by
# the way—does not include any variable arguments prefixed with * or **. The default
# values are identified only by their position in the __defaults__ tuple, so to link each
# with the respective argument, you have to scan from last to first. In the example, we
# have two arguments, text and max_len, and one default, 80, so it must belong to the
# last argument, max_len.
# a better way to grab function signature

# Example 5-17. Extracting the function signature
from inspect import signature
sig = signature(clip)
print(sig)

str(sig)

for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)
# Function Annotations
def clip_annot(text:str, max_len:'int > 0'=80) -> str: # NOTE THE "-> STR"
    """Return text clipped at the last space before or after 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: # no spaces were found
        end = len(text)
    return text[:end].rstrip()


# Python 3 provides syntax to attach metadata to the parameters of a function declaration 
# and its return value. Example 5-19 is an annotated version of Example 5-15. The only differences are in the first line

print(clip.__annotations__) # notice no metadata on other clip example
print(clip_annot.__annotations__) # notice meta data for return type provided<jupyter_output>{}
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}<jupyter_text>Packages for Functional Programming<jupyter_code># The operator module

# Example 5-21. Factorial implemented with reduce and an anonymous function

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

print(fact(5))

# To save you the trouble of writing trivial anonymous functions like lambda a, b:
# a*b, the operator module provides function equivalents for dozens of arithmetic
# operators. With it, we can rewrite Example 5-21 as Example 5-22.

from functools import reduce
from operator import mul
def fact(n):
    return reduce(mul, range(1, n+1))

print(fact(5))
# Example 5-23 shows a common use of itemgetter: sorting a list of tuples by the
# value of one field. In the example, the cities are printed sorted by country code (field 1). 
# Essentially, itemgetter(1) does the same as lambda fields: fields[1]: create a
# function that, given a collection, returns the item at index 1.

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
for city in sorted(metro_data, key=itemgetter(1)):
    print(city)
# If you pass multiple index arguments to itemgetter, the function it builds will return tuples with the extracted values:

cc_name = itemgetter(1, 0)
for city in metro_data:
    print(cc_name(city))<jupyter_output><empty_output><jupyter_text>Freezing Arguments with functools.partialfunctools.partial is a higher-order function that allows partial application of afunction. Given a function, a partial application produces a new callable with some ofthe arguments of the original function fixed. This is useful to adapt a function thattakes one or more arguments to an API that requires a callback with fewer argu‐ments. Example 5-26 is a trivial demonstration.<jupyter_code># Example 5-26. Using partial to use a two-argument function where a one-argument callable is required
from operator import mul
from functools import partial
triple = partial(mul, 3)
print(triple(7)) # multiply has been modified so the first argument is fixed as 3
print(list(map(triple, range(1, 10)))) # create a list of multiples of 3<jupyter_output>21
[3, 6, 9, 12, 15, 18, 21, 24, 27]<jupyter_text>**partial** takes a callable as first argument, followed by an arbitrary number of positional and keyword arguments to bind<jupyter_code># Example 5-27. Building a convenient Unicode normalizing function with partial
# we set the first param of unicodedata.normalize to NFC, our new function nfc will normalise unicode strings and allow us to compare the strings

import unicodedata, functools
nfc = functools.partial(unicodedata.normalize, 'NFC')
s1 = 'café'
s2 = 'cafe\u0301'
print(s1, s2)

print(s1 == s2)

print(nfc(s1), nfc(s2))

print(nfc(s1) == nfc(s2))<jupyter_output>café café
False
café café
True
{'fraction_non_alphanumeric': 0.10086711628251686, 'fraction_numerical': 0.024012450900466907, 'mean_word_length': 4.474239350912779, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 70, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '202835', 'license': 'MIT', 'n_tokens_mistral': 4243, 'n_tokens_neox': 3986, 'n_words': 1644, 'path': 'Fluent Python Sam/Chapter 5 - Functions as Objects.ipynb', 'repo_name': 'slastrina/notebooks', 'size': '13493'}