lambda,dynamic function calls and comprehensions in python

Comprehension

A concise way to create a new list or a new dictionary from an existing iterable or sequence.
Syntax :
It consists of brackets (for list comprehension) and of braces(for dict comprehension) containing an expression followed by a for clause, then zero or more for or if clauses.

list comprehension

very basic syntax:

foo = [x for x in foo_iterable ]

syntax with a condition:

foo = [x for x in foo_iterable if x > 10 ]

the result expression (here x variable) at the beginning of the comprehension expression represents each element that we will add in the list created.
We can use the elements of the initial iterable such as or we can modify them(adding, substracting, concat…)
Beware: the conditional statement is evaluated before the result expression

Examples :

No nested and without condition.
Create a list with all squares from 0 to 10 :

squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

No nested and with condition.
Create a list with all squares from 0 to 10 for input that are even:

squares = [x ** 2 for x in range(10) if (x % 2 == 0)]
# [0, 4, 16, 36, 64]

More complex case: reuse in the result a value computed in the conditional statement
We can avoid repeating the statement to create the value (That is in the conditional statement and in the result statement)by using an assignment expression in the conditional statement.
Example:
For each element of the initial sequence, we want to get the stripped string of the first group of a pattern match. But if the pattern doesn’t match with the element we just want to skip that element in the final result.
We can see that the match variable is initialized in the conditional statement and reused in the result statement:

    pattern: Pattern = re.compile(r'self\.(.*?)=')
    fields_list: List[str] = [match.group(1).strip() for line in lines
                              if (match := re.match(pattern, line.strip())) is not None]

Dictionary comprehensions

we have two main cases:
– create the new dictionary from a list
– create the new dictionary from a dictionary

the syntax is very close to the syntax used for the list comprehension.

create the new dictionary from a list
example: dictionary created from the expression x: x*2 and the Iterable (2, 4, 6)

dic = {x: x * 2 for x in (2, 4, 6)}
print(f'dic={dic}')

output:

    dic={2: 4, 4: 8, 6: 12}

create the new dictionary from a dictionary
example: dictionary created from the expression k: v.capitalize() and the dictionary : {'name': 'david', 'size': 'big'}

original_dic = {'name': 'david', 'size': 'big'}
dic = {k: v.capitalize() for k, v in original_dic.items()}
print(f'dic={dic}')

output:

    dic={'name': 'David', 'size': 'Big'}

call of function dynamically

in python,passing a function as a parameter is very straight.
we just need to pass the name of the function without the parenthesis
we will illustrate with the 4 main cases:
– call the function without parameter.
– call the function with specific parameters.
– call the function with *args parameter.
– call the function with *kwargs parameter.

here are the functions we want to call:

def add(a: int, b: int) -> int:
    return a + b
 
def print_hello():
    print('hello')

call the function without parameter

function = print_hello
function()#hello

call the function with specific parameters

function = add
print(f'function(1,5)={function(1, 5)}')

call the function with *args parameter or with *kwargs parameter

1) Call the function directly

result = add(*(1, 3))
print(f'result={result}')
 
result = add(**{'a': 4, 'b': 2})
print(f'result={result}')

2)Or we can also pass by a common function to call dynamically any other function:

def function_that_can_call_other_function(function, *args, **kwargs):
    if args:
        return function(*args)
    elif kwargs:
        return function(**kwargs)
    else:
        return function()

Here the calls to that function with the different cases:
The syntax is the same as above.
call with no parameter:

function_that_can_call_other_function(print_hello)
    #hello

call with kwargs parameter:

return_value = function_that_can_call_other_function(add, **{'a': 2, 'b': 5})
print(f'return_value={return_value}') # return_value=7
# or simplified syntax
return_value = function_that_can_call_other_function(add, a=2, b=5)
print(f'return_value={return_value}')# return_value=7

call with args parameter:

return_value = function_that_can_call_other_function(add, *(2, 5))
print(f'return_value={return_value}')# return_value=7
# or simplified syntax
return_value = function_that_can_call_other_function(add, 2, 5)
print(f'return_value={return_value}')# return_value=7

Define a function as parameter

– No typed way:

def do_apply(text: str, filter_function) -> str:
          return filter_function(text)

Advantages: very short syntax.
Example:

result = do_apply('john', str.upper)
print(f'result={result}')#JOHN

Drawbacks: no type checking of parameters or of the return type of the function
Example:
We pass a function taking 3 parameters and returning an int while the function executed in do_apply() doesn’t expect them.

result = do_apply('john', str.count)

It fails at runtime:TypeError: count() takes at least 1 argument (0 given)

– Typed way:

def do_apply_robust(text: str, filter_function: Callable[[str], str]) -> str:
    return filter_function(text)

Here the function expects a string parameter and return a string.
Drawbacks: more verbose syntax
Advantages: warnings if type in caller and type in definition doesn’t match and that before the execution
Example ok (It works as in the previous case):

result = do_apply_robust('john', str.upper)
print(f'result={result}')

Example ko:

result = do_apply_robust('john', str.count)

now we have a warning in pycharm before the execution on the statement str.upper :

Expected type '(str) -> str', got '(x: str, __start: Optional[SupportsIndex], __end: Optional[SupportsIndex]) -> int' instead

lambdas

Lambda expressions are used to create anonymous functions.
syntax: lambda parameters: expression.
It yields a function object behaving like a function object defined with:

def <lambda>(parameters):
    return expression

use case: define in a compact/private way a function and use it

example with an argument:
Add 1 to the argument and return the result:

x = lambda a : a + 1
print(x(5)) # 6
print(x(8)) # 9

example with two arguments:
sum the arguments and return the result:

x = lambda a,b : a + b
print(x(1,9)) # 10

Limitations of chaining lambda with functions in python

Python is not at all a functional language, so chaining the stream functions is not as straight as with languages providing more functional features such as scala or even java, javascript…
In functional paradigm, we have generally a fluent API, that is we invoke a function on an object referencing an interface and the function returns in its contract the same interface in the general case, so we can chain calls obj.a().b().c().
Python that has a more procedural paradigm requires to wrap calls such as: a(b(c())), which is of course less simple and straight to read and to write.

Lambda use cases: make a function with a parameter as function that callers will define their own implementation as lambda

That function can be used for filtering,adding,computing,collecting and so for…

example with a filtering function

The filtering function takes a string as parameter and return a boolean:

def filter_animals(animals: List[str], filter_function) -> List[str]:
    return [x for x in animals if filter_function(x)]

Alternatively, we can define the function with the callable interface as seen above to allow stronger type checking:

def filter_animals(animals: List[str], filter_function: Callable[[str], bool]) -> List[str]:
    return [x for x in animals if filter_function(x)]

How to call this function
– We can specify a parameter in the lambda expression and use it such as:

animals = filter_animals(['dog', 'cat', 'lion', 'jaguar'], lambda x: len(x) == 3)
print(f'animals={animals}')  # animals=['dog', 'cat']
 
animals = filter_animals(['dog', 'cat', 'lion', 'jaguar'], lambda x: str(x).count('a') >= 1)
print(f'animals={animals}')  # animals=['cat', 'jaguar']

– Or we can provide no value for the parameter of the lambda expression and instead of use a hardcoded value or a constant.
But we are forced to specify the lambda parameter (with _) even if it is empty.
For example, we define lambda where we hard code the value of the parameter in a way where the lambda return is always true:

animals = filter_animals(['dog', 'cat', 'lion', 'jaguar'], lambda _: len('foo') == 3)
print(f'animals={animals}')  # animals=['dog', 'cat', 'lion', 'jaguar']

example with a creator function

The creator function is called if a specific condition is true.

In our example, the called function doesn’t require any parameter, it is just a basic creator function:

def get_if_valid(condition: bool, creator_function: Callable[[], str]) -> str:
    # basic condition just too illustrate
    if condition:
        return creator_function()

Usage:

text = get_if_valid(True, lambda: 'Super item')
print(f'text={text}')#text=Super item
text = get_if_valid(False, lambda: 'Another item')
print(f'text={text}')#text=None

Stream like features

The filter(function,iterable) function:
Accept as arguments an iterable and a predicate function,and return an iterator containing items which are true for the predicate.
generally we want to transform the returned iterator into a more handy object like a list or a tuple.
example:

animals = ['dog', 'cat', 'lion', 'jaguar']
animals_filtered = list(filter(lambda x: len(x) == 3, animals))
print(f'animals_filtered={animals_filtered}')    #animals=['dog', 'cat']

The next(iterator, default) function:
It is quite similar to the findFirst() in java stream.
Accept as arguments an iterator and return the next item from the iterator by calling its __next__() method.
If the iterator is exhausted, StopIterationis raised or default is returned if argument was provided.

Example where an element is found:

animals = ['dog', 'cat', 'lion', 'jaguar']
animal_filtered = next(filter(lambda x: str(x).count('a') >= 1, animals))
print(f'animal_filtered={animal_filtered}')#animal_filtered=cat

Example where an element is found and no default return is provided as parameter:

animals = ['dog', 'cat', 'lion', 'jaguar']
next(filter(lambda x: str(x).count('x') >= 1, animals))

As expected, and exception is raised:

Traceback (most recent call last):
  File "C:\Users\david\AppData\Roaming\JetBrains\PyCharmCE2021.3\scratches\scratch.py", line 103, in <module>
    next(filter(lambda x: str(x).count('x') >= 1, animals))
StopIteration

Example where an element is found but a default return is provided as parameter:

animals = ['dog', 'cat', 'lion', 'jaguar']
animal_filtered =next(filter(lambda x: str(x).count('x') >= 1, animals),'pig')
print(f'animal_filtered={animal_filtered}')#pig

As expected, the default value is returned: ‘pig’

The map(function,iterables) function
Accepts as argument a function and one or multiple iterables.

– Case where we have only one iterable:
Return an iterator that applied function(x) to every item of iterable.

Example:

animals = ['dog', 'cat', 'lion', 'jaguar']
mapped_animals = list(map(lambda x: x.upper(), animals))
print(f'mapped_animals={mapped_animals}')
# mapped_animals=['DOG', 'CAT', 'LION', 'JAGUAR']

We can see that the function is applied for each element of the iterable provided: we have 4 elements in input and so we have 4 elements in output.

– Case where we have multiple iterables:
Return an iterator that applied function(x,y,...) to every item of iterables where the function has as many as parameters that iterables parameter provided.
The map() function works quite similarly to the zip() function:
The iterator stops when the shortest iterable is exhausted.

Example:
we will illustrate with a case where the iterable objects don’t have the same length in order to show that we have less elements in output in this case.

animals = ['dog', 'cat', 'lion', 'jaguar']
colors = ['blue', 'red', 'black']
mapped_animals_with_color = list(map(lambda a, c: c + ' ' + a, animals, colors))
print(f'mapped_animals_with_color={mapped_animals_with_color}')
# mapped_animals_with_color=['blue dog', 'red cat', 'black lion']
Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *