Lambda Functions

Lambda functions are anonymous functions, meaning functions that don't have to be named. The idea is based off Church's Lambda Calculus but was first implemented into a programming language named Haskell.

The best suggested use-case of lambda functions within Python is when a simple function needs to be passed as a parameter, such as for the key argument of list.sort().

Warning

Be careful when using lambda functions in loops and the scope of variables used in lambdas.

I find them to be most useful when sorting slightly more complex lists.

# add one and print
print((lambda x: x + 1)(2))
 
# two var example
print((lambda x, y: x + y)(2, 3))

# Use it to sort a list of class objects
class Person:
    def __init__(self, name):
        self.name = name
        self.id = hash(name)

myList = [Person("JJ"), Person("Purdy"), Person("Kelce"), Person("Achane")]

myList.sort(key=lambda person: person.id)

print(myList)

Conditional Variable Assignment

While this may seem somewhat useless it sort of comes in handy for easy one liners.

It may also come in handy when dealing with type annotation.

myCondition = True

# 5 lines to declare and assign a variable
myVar = None
if myCondition: 
    myVar = "yes"
else:
    myVar = "no"
print(myVar)

# Becomes one line
myVar = "yes" if myCondition else "no"
print(myVar)

# Practical example
def announce(person = None):
    person = person if person else "Nobody"
    print(person + "has arrived!")

# A convoluted example which you should not use
def square(val):
    # Converts val to a proper float/int
    base = int(val) if type(val) is str and val.isdecimal() else val if type(val) is int or type(val) is float else 0
    return base ** 2

print(square("10"))
print(square("NaN"))
print(square(10))

# If your variable has "booleanness"
empty = ""
myVar = empty or "not empty"

Global Variable Scope

This allows for a function to access the state of a global variable, or just create a global scope variable.

Note

Yes you can just return the value or modify a classes' attribute, but you may occasionally want to modify or instantiate a variable that determines how an entire program runs.

# global variable
statement = "empty"

# Creates a local variable "statement"
myFunc(val):
    statement = "Hello"
	print(statement)

# Modifies the global variable "statement"
myFuncGlobal(val):
    global statement
    statement = "Hello"
	print(statement)

myFunc()          # Hello
print(statement)  # empty

myFuncGlobal()    # Hello
print(statement)  # Hello

Enumerate

Enumerate is a builtin function that takes any iterable and returns another iterable of tuples. Each tuple contains an "index" and the value at that "index".

The two most useful thing about enumerate besides the ease of use is that you can combine it with unpacking and easily modify where an index count starts.

The basic syntax of enumerate is as following: enumerate(*iterable*, *start_int*)

myList = [1, 2, 3, 4, 5, 6, 7, 8]

for value in enumerate(myList):
    print(value)

myList = [1, 2, 3, 4, 5, 6, 7, 8]

# Print a value in an iterable with indexing starting at 1
# A classic implementation
myFuncIterate(iterable, print_index)
    count = 1
    for index in range(len(iterable)):
        if index + 1 == print_index:
            print(f"Reached index {count} with value: {myList[index]}")
        count += 1

# Print a value in an iterable with indexing starting at 1
# A classier implementation
myFuncEnumerate(iterable, print_index)
    for index, value in enumerate(iterable, 1):
        if index == print_index:
            print(f"Reached index {index} with value: {value}")

myFuncIterate(myList, 3)
myFuncEnumerate(myList, 4)

Zip

zip is another python builtin that takes two or more iterables and "zips" them together in one list of tuples.

This is mostly useful when trying to map or group values of two lists together.

The basic syntax of zip is the following: zip(*iterable1*, *iterable2*, *iterable3*, ...)

names = ["C. Kirk", "J. Hurts", "J. Reed", "B. McManus"]
positions = ["WR", "QB", "WR", "K"]

# Easier way to access the names and position at the same time
for player, position in zip(names positions):
    print(f"{player} plays {position}.")

# Create a dictionary of players mapped to position.
position_lookup = {player: position for player, position in zip(names, positions)}

Map

map takes two arguments, a function object and iterable(s). The function is applied to each iterable(s) and a new iterable is returned.

The basic syntax of map is the following: map(*func*, *iterable1*, *iterable2*, ...)

my_str = "Hello world, my name is Aniketh!"
words = my_str.split()

def count_chars(string):
	return len(set(string))

# Use map to apply the count characters function to our list of words
for num_letters in map(count_chars, words):
	print(num_letter)

Generators

Generators make it easy to create an iterable using functions. This can be used with comprehensions to create lists or use with a for in loop.

This can be useful when dealing with large sets, lists, tuples that may take up a lot of space in memory.

It can also be useful in classes to make certain types of iterables.

# Generates numbers from 0 to n all multiplied by 10 
def mygenerator(n):
    num = 0
    while num <= n:
        yield num * 10
        num += 1

for val in mygenerator():
    print(val)

import base64

# Use it in a class!
class myClass:
    def __init__(self):
        self.class_list = ["Dave", "Maria", "Arjun", "Minjoon", "Sunday", "Naatya"]

    # Return a base64 encoded string version of each name
    def class_list_b64(self):
        for name in self.class_list:
            nameb = name.encode("ascii")
            nameb_b64 = base64.b64encode(nameb)
            name_b64 = nameb_b64.decode("ascii")
            yield name_b64

testClass = myClass()

# Print each base 64 encoded names
for val in testClass.class_list_b64():
    print(val)

# Make a list of base 64 encoded names
names_b64 = [name for name in testClass.class_list_b64()]

Unpacking

Unpacking is a feature that exists in multiple high-level languages but is particularly useful in python when dealing with function parameters or iterable objects of known length and or properties.

The basic idea behind unpacking is to assign variables to the values from iterable data structures such as lists, tuples, or dictionaries. This can be used to assign values of a list to variables in a much easier to read fashion.

myList = [11, 45]
myTuple = ("thing1", 2, 3)
myDictionary = {"val1": 20, "val2": 10}
wrongDictionary = {"val1": 40, "val2": 10, "val3": 3}

# Keyword arg only function
def divide_vals(*, val1=1, val2=1):
    return val1/val2

# Positional arg only function
def add_vals(val1, val2, /):
    try:
        return str(val1 + val2)
    except Exception:
        return "Something went wrong"

# Unpack list into variables
val1, val2 = myList
print(f"val1 = {val1}")
print(f"val2 = {val2}")

#Unpack list/tuple into function
print("\nUnpacked List: ")
print(*myList, sep="\n")

print("\nUnpacked Tuple: ")
print(*myTuple, sep="\n")

# Unpack dictionary into function
print("\nUnpacked Dictionary: ")
return_val = divide_vals(**myDictionary)
print(return_val)

# Unpack incorrect dictionary into function
print("\nIncorrect Unpacked Dictionary: ")
return_val = divide_vals(**wrongDictionary)
print(return_val)

# Unpack incorrect list into function
print("\nIncorrect Unpacked Tuple: ")
return_val = add_vals(*myTuple)
print(return_val)

# Unpacking in function arguments
def myFunction(*args, **kwargs):
    print("\nMy Function Got args: ")
    print(*args, sep="\n")
    print("\nMy Function Got kwargs: ")
    for key, value in kwargs.items():
        print(f"{key} = {value}")

myFunction(1, 2, 4, 10, val1="thing1", val2="thing2", val3="thing3")

Decorators

Decorators are not specific to python and are present in a lot of high-level lanugages.

A decorator is a function itself that takes another function as a parameter. The decorator function can then call the function passed to it and run some code before or after running it.

Defining and returning an inner function allows us to use the @decorator syntax which you may have seen when using frameworks like flask.

# A simple decorator function
def mywrapper(func):
    num = int(input("Enter a number: "))
    func(num)

# Square a number
def num_squared(num):
    print(num ** 2)

# Wrap the number function with the decorator function
mywrapper(num_squared)

# Create a decorator that returns an inner function
def mydecorator(other_function):
    def inner_function(*args, **kwargs)
        print("Run before function")
        other_function(*args, **kwargs)
        print("Run after functionn")
    return inner_function

# Define a printer function
def printer(value):
    print(value)
    return 2

# Wrap the printer function with our decorator
printer = mydecorator(printer)
val = printer("Run within the function")
print(val)

# Syntactical sugar (does the same as the previous example in a cleaner fashion)
@mydecorator
def printer(value):
    print(value)
    return 1

val = printer("Run within the function")
print(val)

Comprehensions

Comprehensions allow for easy one line creation of any iterable python data structure (except for tuples, but there is a workaround).

The basic idea is to use generators and conditional variable assignnment to make a quick list, dictionary, or set.

NOTE: Comprehensions can get very hard to read/understand very fast and they're not always the best solution. I would suggest only using them for simple conversions. Sometimes its easier to just write it out using a loop.

# example list
mylist = ["bobby", "anne", "magda", "omar", "atif", "noor"]

# create a list of first letters
first_letters = [name[0] for name in mylist]

# only include names that end in "r"
ends_in_r = [name for name in mylist if name[-1] == "r"]

# create a dictionary of names identified by hash
name_hashmap = {hash(name): name for name in mylist}

# workaround for tuples
o_in_name = tuple(name for name in mylist if "o" in name)

Type Annotation (Hints)

This is quite an extensive topic and could probably have its own page dedicated to it so I would suggest going through the documentation to get a better idea of how this works.

Like the title suggest this does not enforce static typing, but rather assists developers in writing good code. Besides just having type hints visible to developers most IDE's and static type checkers like mypy can utilize and check for static typing errors. There are frameworks and libraries that also help actively enforce static typing of variables however.

Here are some examples of basic usage

# function takes a name of type string, and id of type int and returns nothing (None)
def myfunc(name: str, id: int) -> None:
    print(f"your name is {name} with id {id}")

# Converts a string to a list of strings
def to_list(name: str) -> list[str]:
    return list(name)

# Annoyingly complicated type annotation
def map_addresses(
    userIds: list[int], 
    addresses: list[tuple[str, str, str, int]], 
    mode: str = "d"
    ) -> dict[int, tuple[str, str, str, int]] | list[tuple[int, tuple[str, str, str, int]]]

    if len(userIds) != len(addresses):
        raise Exception("Lists must be of equal length")

    if mode.lower() == "d":
        return {id, address for id, address in zip(userIds, addresses)}
    if mode.lower() == "l":
        return [(id, address) for address in zip (userIds, addresses)]

# You can alias type annotations!
# Lets alias some of the types and retry defining the previous function
Address = tuple[str, str, str, int]
UID = int # somewhat useless but helps with logical errors

# Much cleaner!
def map_addresses(
    userIds: list[UID], 
    addresses: list[Address], 
    mode: str = "d"
    ) -> dict[UID, Address] | list[tuple[UID, Address]]

    if len(userIds) != len(addresses):
        raise Exception("Lists must be of equal length")

    if mode.lower() == "d":
        return {id, address for id, address in zip(userIds, addresses)}
    if mode.lower() == "l":
        return [(id, address) for address in zip (userIds, addresses)]

Adding in the typing module helps add some other typing functionality that I occasionally find useful.

from typing import Literal, Any, TypedDict, Iterable, List

# from previous example
Address = tuple[str, str, str, int]

# Mode only except the values of 'upper' and 'lower'
def printer(string: str, mode: Literal['upper', 'lower']) -> str:
    if mode == 'upper':
        return string.upper()

    return string.lower()

# Accepts any iterable object of any value and turns it into a list of any value
def listify(obj: Iterable[Any]) -> List[Any]
    return list(obj)

# Define a dictionary with certain values
class Person(TypedDict):
    name: str
    address: Address
    id: int | str

# Equivalent to
Person = TypedDict('Person', {"name": str, "address": Address, "id": int | str})

# Also equivalent
Person = TypedDict('Person', name=str, address=Address, id=int|str)

# This indicates that any variable typed with Person should be a dict with the "name", "address" and "id" keys present.
def printPerson(person: Person) -> None:
    print(f"{person['name']} with id {person['id']} has an address of {person['address']}")

Fun

The following code generates the Unicode character: "ඞ"

c = chr(sum(range(ord(min(str(not()))))))

You can run the following function to understand the progression of how the previous code is evaluated.

def amogus():
    print(c := not())
    print(c := str(c))
    print(c := min(c))
    print(c := ord(c))
    print(c := sum(range(c)))
    print(c := chr(c))

Reference

  1. https://www.w3schools.com/python/python_reference.asp

Related