My Notes

Productivity, DevOps, Email, Kubernetes, Programming, Python, Mongo DB, macOS, REST, RDBMS, Powershell, SCM, Unix Tools,

Notes on Python

First Class Objects

Python can treates function (including anonymous function lambda) as an object. A function that takes a function as an argument or returns a function as the result is a higher-order function1.

NOTE: In the lambda, the body cannot contain Python statements (eg:while, try). Assignment with = is also a statement, therefore it cannot be occured in a lambda. However, the new assignment expression syntax using := can be used.

There are 9 flavors of callable objects:

  1. User-defined functions
  2. Built-in functions (len)
  3. Built-in methods (of dict, tuple and so on)
  4. Methods of a class
  5. Classes
  6. Class instances (__call__ method must be defined)
  7. Generator functions (yield returns the generator object)
  8. Native coroutine functions (async def)
  9. Asynchronous generator functions ( function or method defined with async def and return by the yield.

Type Hints

For Python 3.7 and 3.8, you need a __future__ import to make the [] notation work with built-in collections such as list and Python ≥ 3.5, use the from typing import List.

for example in the jupyter notebook cell

%%python3
from __future__ import annotations

def upperWords(text: str) -> list[str]:
    return text.upper().split()

Tuple Types

There are three ways to annotate tuple types:

1.Tuples as records 2.Tuples as records with named fields 3.Tuples as immutable sequences

Tuples as records

%%python3

from __future__ import annotations
def printCountryCode(country_code:tuple[str,int]) -> str:
    s, n = country_code
    return f'{s} is +{n}'

print(printCountryCode(('AU', 61)))

Tuples as records with named fields

from __future__ import annotations
from typing import NamedTuple

class CountryCode(NamedTuple):
    country: str
    code: int

au_code = CountryCode('AU',61)

# using above method
print(printCountryCode(au_code))

# Method with CountryCode as a parameter

def printCCode(country_code:CountryCode) -> str:
    s, n = country_code
    return f'{s} is +{n}'

print(printCCode(au_code))
#-> Same result from both functions
# AU is +61
# AU is +61

Tuples as immutable sequences

%%python3

from __future__ import annotations

t1:tuple[str,str] = ('a','b')
t2:tuple[str,str] = ('c','d')
t:tuple[str, ...] = t1 + t2
print(t) #-> ('a', 'b', 'c', 'd')

Specify a single type tuple[<type>,...].

Map types

Here the simple example

letterMap:dict[int,str] = {1:'a',2:'b'}

In general it’s better to use abc.Mapping or abc.MutableMapping in parameter type hints.

from collections.abc import Mapping
def printLetters(letters:Mapping[int,str]) ->list[str]:
    return [v for _, v in letterMap.items()]

printLetters(letterMap) #-> ['a', 'b']

Using abc.Mapping allows to pass the instance of dict, defaultdict, ChainMap, or any other type that is a subtype-of Mapping.

The method signature can be change as follows for Python 3.8 or earlier (typing.List):

from collections.abc import Mapping
from typing import List

def printLetters(letters:Mapping[int,str]) ->List[str]: # concrete
    return [v for _, v in letterMap.items()]

printLetters(letterMap)

As shown above function returns the Concrete object of List which is the generic class of list. However, to minimize the memeory usage:

from collections.abc import Mapping, Iterator

def printLetters(letters:Mapping[int,str]) ->Iterator[str]:
    for _, v in letterMap.items():
        yield v

l = printLetters(letterMap)
for i in l:
    print(i)
#->
# a
# b

In the Python 3.10, you can use type alias

from collections.abc import Mapping
from typing import List
from typing import TypeAlias

# define the type alias
ta:TypeAlias = Mapping[int,str]

def printLetters(letters:ta) ->List[str]:
    return [v for _, v in letterMap.items()]

printLetters(letterMap)

This will simplify the method signature.

Parameterized Generics

Generic type, written as list[T], where T is a type variable that will be bound to a specific type in each occation.

Risticted TypeVar

from typing import TypeVar
from collections.abc import Sequence, Iterable

T = TypeVar('T',int,float, str)

def doubElem(l:Sequence[T]) -> Iterable[T]:
    for i in l:
        yield i*2

for i in doubElem((1,2)):
    print(i)  

for i in doubElem(['a','b','c']):
    print(i)
  

output is

2
4
aa
bb
cc

Bounded TypeVar

from typing import TypeVar
from collections.abc import Sequence, Iterable

T = TypeVar('T',bound=int)

def doubIntElem(l:Sequence[T]) -> Iterable[T]:
    return [i for i in l]

for i in doubIntElem((1,2)):
    print(i)  

# for i in doubIntElem(['a','b','c']):
#     print(i)  

output is

1
2

NOTE: One of the predefined bounded type variable is AnyStr.

Closures

A function variable becomes local when assigned a value; otherwise, it searches for global scope variable assignment of the same variable (the name of the variables are the same). Therefore, before use the local variable, you have to assign a variable locally, or you have to explicitly inform using the global keyword to avoid the misconception:

a=1
b=2
def bla(p)
    global a
    ...
    print(a) # this is valid because `a` is a global variable
    print(b) # this is not valid, there are errors because 
             # `b` is a local variable due to the following assignment 
    b=3
    ...
    a=5

A closure is simply a function with free variables, where the bindings for all such variables are known in advance.

The free variable is a technical term meaning a variable that is not bound in the local scope but in the lexical scope. In Python language, all functions are closed. Therefore in the Python closures, free variables defined in the enclosing functions to access by its nested returning function.

lexical scope is the norm: free variables are evaluated considering the environment where the function is defined. Python does not have a program global scope, only module global scopes.

You have to use nonlocal keywoard instead of the global keyword to access the free variables from the nested function.

g=10
def hoFunc(p):
    my_free_var=0
    def nested(n):
        nonlocal my_free_var,p
        global g # only define as global
        p += 1
        n += g # n is local
        g=1
        my_free_var += (n+p)
        return my_free_var
    return nested     

hoFunc(2)(1) #-> 4        

In the above code, n parameter is a local variable but p parameter is not.

Decorators

The singledispatch is one of the standard library decorator, which is use to create overloaded functions similar to Java overload methods instead dispatch function. For example,

from functools import singledispatch
from collections import abc

@singledispatch
def myprint(obj:object) -> str: # generic function
    return f'object: {obj}'

@myprint.register
def _(text: str) -> str:
    return f'str: {text}'

@myprint.register
def _(n: int) -> str:
    return f'int: {n}'

@myprint.register(abc.Sequence) # pass the type if you want
def _(seq) -> str:
    return (f'sequence: {seq}')   

When you call myprint(1) the output: 'int: 1' and call myprint('oj') the output is 'str: oj'. In the case of input sequence such as myprint([1,2,3]) the output is 'sequence: [1, 2, 3]'.

NOTE: Java-style method overloading is missing in Python. When possible, register the specialized functions to handle abstract classes such as numbers.Integral and abc.MutableSequence, instead of concrete implementations like int and list. from Fluent Python

Here the simple decorator

def deco(func):
    def inner(*args):
        result = func(*args)
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f' {name}({arg_str}) -> {result!r} in the decorator')
        return result
    return inner    

You can docorate myfunc as follows:

@deco
def myfunc(p):
    print(f'parameter: {p}')

myfunc(1)   

output is

parameter: 1
 myfunc(1) -> None in the decorator

But there is a problem, if you run myfunc(p=1) , you get the following error, because above decorator not support keyword arguments

TypeError                                 Traceback (most recent call last)
Cell In[11], line 5
      1 @deco
      2 def myfunc(p):
      3     print(f'parameter: {p}')
----> 5 myfunc(p=1)    

TypeError: deco..inner() got an unexpected keyword argument 'p'

The resoultion:

import functools

def deco(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        result = func(*args, **kwargs)
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f' {name}({arg_str}) -> {result!r} in the decorator')
        return result
    return inner   

As shown in the above code in the

Decorator with arguments

To send parameter in the decorator2:

import functools
def docowithargs(show=True): #1
    def deco(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            result = func(*args, **kwargs)
            if show: #2
                name = func.__name__
                arg_str = ', '.join(repr(arg) for arg in args)
                print(f' {name}({arg_str}) -> {result!r} in the decorator')
            return result
        return inner   
    return deco  #3  

Modify the deco function to pass the parameter as follows

  1. New decorator which wrap the deco function
  2. Use the argument pass in the decorator
  3. return the deco function

If you run the following code:

@docowithargs(show=True)
def myfuncT(p):
    print(f'parameter: {p}')

@docowithargs(show=False)
def myfuncF(p):
    print(f'parameter: {p}')

myfuncT(1)    
myfuncF(2)   

the output is

parameter: 1
 myfuncT(1) -> None in the decorator
parameter: 2

Class based decorator

This is simple as follows:

class classdeco:
    def __init__(self, show=True):
        self.show = show

    def __call__(self, func):
        def inner(*args, **kwargs):
            result = func(*args, **kwargs)
            if self.show: #1
                name = func.__name__
                arg_str = ', '.join(repr(arg) for arg in args)
                print(f' {name}({arg_str}) -> {result!r} in the decorator')
            return result
        return inner 

It is a same inner function with the modification to #1 in the above code. Decorate the function as follows and call the function:

@classdeco(show=False)
def myfuncT(p):
    print(f'parameter: {p}')
    
# call the function
@classdeco(show=True)
def myfuncT(p):
    print(f'parameter: {p}')

The output is

parameter: 3
 myfuncT() -> None in the decorator

static vs class method

Class method expect class as a first parameter, but static method not:

class MethodDeco:
    @classmethod #1
    def myClassMethod(*args) -> None:
        print(args)
    
    @staticmethod #2
    def myStaticMethod(*args) -> None:
        print(args)

if you call class method MethodDeco.myClassMethod(1) the output is:

(<class '__main__.MethodDeco'>, 1)

but if you call static method MethodDeco.myStaticMethod(1) the output is:

(1,)