Intermediate topics#
If you are already familiar with the Python basics, this notebook will introduce you to some more advanced topics. Neither of these topics is required to complete the exercises, but they may be useful to you in the future.
This notebook is not a complete introduction to these topics, but rather a quick overview of some of the most useful features. If you want to learn more, you can find many tutorials online.
We will cover:
Dict-comprehensions
Lambda functions
Type hints
Keyword arguments
*args
and**kwargs
Dataclasses
Decorators
Utilities from the standard library
Dict-comprehension#
We previously introduced list
-comprehension:
mylist = [2, 1, 3, 4, 5]
mysquares = [x**2 for x in mylist]
mysquares
[4, 1, 9, 16, 25]
There is also dict
-comprehension, with various flavors:
mydict = {"a": 1, "b": 2, "c": 3}
# Iterate over dict items
mysquaresdict = {key: value**2 for (key, value) in mydict.items()}
# Iterate over dict items and modify keys
mydictwithnewkeys = {key.upper(): value**2 for (key, value) in mydict.items()}
# Iterate over a list or other iterable
mydictfromlist = {x: x**2 for x in mylist}
print(mysquaresdict)
print(mydictwithnewkeys)
print(mydictfromlist)
{'a': 1, 'b': 4, 'c': 9}
{'A': 1, 'B': 4, 'C': 9}
{2: 4, 1: 1, 3: 9, 4: 16, 5: 25}
Lambda functions#
Lambda functions are a way to define functions “inline”, without giving them a name. This can be useful when we deal with another function that takes a function as an argument. The syntax is:
lambda <arguments>: <expression>
The expression is evaluated and returned when the function is called. Example:
def process_list(values, function):
return [function(x) for x in values]
mylist = [2, 1, 3, 4, 5]
print(process_list(mylist, lambda x: x + 2))
print(process_list(mylist, lambda x: x**2))
[4, 3, 5, 6, 7]
[4, 1, 9, 16, 25]
Type hints#
Python is a dynamically typed language, which means that the type of a variable is not known at compile time. This is in contrast to statically typed languages like C, where the type of a variable must be declared before it is used. As a relatively recent addition to the language, Python now supports type hints. These are not enforced by the interpreter, but can be used by external tools to check for type errors. Maybe more importantly, they can be used to document the expected types of arguments and return values, improving readability. Over the past years, type hints have become increasingly popular, and are now used in many popular libraries, to the point where they are almost expected.
Example:
def format_info(name: str, age: int) -> str:
return f"{name} is {age} years old"
Type hints for function arguments are written after the argument name, separated by a colon:
name
is a stringage
is an integer
The return type is written after an arrow
->
:The function returns a string
Note that the type hints are literally hints and are not enforced by the interpreter, so the following code will run without errors:
format_info("John", 25.5)
'John is 25.5 years old'
format_info(11, [1, 2, 3])
'11 is [1, 2, 3] years old'
Apart from built-in types like int
, float
, str
, list
, dict
, etc., we can also use custom types.
In many cases, we will use the typing
module, which provides a number of useful types.
Example:
from typing import List, Union
import math
def mysqrt(x: List[Union[int, float]]) -> List[float]:
"""Take a list of integers or floats and return a list of their square roots"""
return [math.sqrt(y) for y in x]
Keyword arguments#
When calling a function, we can specify the arguments by position or by name. The latter is called keyword arguments. Keyword arguments can be used to make the code more readable, and to specify default values for arguments. Using keyword arguments for all arguments is a good practice (apart from cases such as function with a single arguments), as it makes the code more robust to changes in the function signature and improves readability:
def format_info(name: str, age: int) -> str:
return f"{name} is {age} years old"
format_info(name="John", age=25)
'John is 25 years old'
*args and **kwargs#
Sometimes we want to write a function that takes an arbitrary number of arguments.
This can be done using *args
and **kwargs
.
The names args
and kwargs
are not special, but are commonly used.
Example:
def average_age(*ages: int) -> float:
return sum(ages) / len(ages)
def format_family_info(**ages: int) -> str:
info = ", ".join([f"{name} is {age} years old" for name, age in ages.items()])
return info + f". Their average age is {average_age(*ages.values())}."
format_family_info(John=25, Jane=24, Jack=17)
'John is 25 years old, Jane is 24 years old, Jack is 17 years old. Their average age is 22.0.'
We can also use *
and **
when calling a function, to unpack a list or dictionary into arguments:
family = {"Jack": 17, "Jill": 15}
print(f"Average age: {average_age(*family.values())}")
print(format_family_info(**family))
Average age: 16.0
Jack is 17 years old, Jill is 15 years old. Their average age is 16.0.
Dataclasses#
Dataclasses are a convenient way to create classes that are mostly used to store data. Even if you are not familiar with classes in Python, you can probably understand the following example. For more details, see, for example, this video introduction: If you’re not using Python DATA CLASSES yet, you should.
from dataclasses import dataclass
@dataclass
class Experiment:
name: str
date: str
temperature: float
pressure: float
@dataclass
is a so-called decorator that adds convenient functionality to the class.
We can use our class Experiment
as follows:
exp1 = Experiment(
name="my first experiment", date="2020-01-01", temperature=20.0, pressure=1.0
)
exp2 = Experiment(
name="my second experiment", date="2020-01-02", temperature=21.0, pressure=1.1
)
print(exp1)
print(exp2)
# Access fields
print(exp1.date)
Experiment(name='my first experiment', date='2020-01-01', temperature=20.0, pressure=1.0)
Experiment(name='my second experiment', date='2020-01-02', temperature=21.0, pressure=1.1)
2020-01-01
Dataclasses can also have default arguments.
For mutable default argument such as a list, we need to use field(default_factory=list)
:
from dataclasses import dataclass, field
@dataclass
class Experiment:
date: str
location: str = "ESS"
comments: list = field(default_factory=list)
# Default location and comments
print(Experiment(date="2020-01-01"))
# Override default location, keep default comments
print(Experiment(date="2020-01-01", location="ILL"))
# Default location, override default comments
print(Experiment(date="2020-01-01", comments=["test1", "test2"]))
Experiment(date='2020-01-01', location='ESS', comments=[])
Experiment(date='2020-01-01', location='ILL', comments=[])
Experiment(date='2020-01-01', location='ESS', comments=['test1', 'test2'])
Decorators#
Decorators are a way to modify the behavior of a function. They are written as functions that take a function as an argument, and return a new function. The syntax is:
def mydecorator(func):
def wrapper(*args, **kwargs):
# Do something before calling the function
print(
f"Your function {func.__name__} is about to be called with arguments {args} and {kwargs}"
)
# Call the function
result = func(*args, **kwargs)
# Do something after calling the function
print(f"Your function {func.__name__} was called")
return result
return wrapper
@mydecorator
def myfunction():
print("Hello world")
@mydecorator
def myfunction2(x):
print(f"Hello {x}")
myfunction()
myfunction2("John")
Your function myfunction is about to be called with arguments () and {}
Hello world
Your function myfunction was called
Your function myfunction2 is about to be called with arguments ('John',) and {}
Hello John
Your function myfunction2 was called
Utilities from the standard library#
When you are writing Python code, you should always check if there is a utility in the standard library that does what you want. More often than not, there is. Here are some examples:
Built-in functions such as
map
,filter
,enumerate
,zip
, andreduce
.functools, in particular functools.partial.