Python Intermediate FreeCodeCamp
Python Intermediate FreeCodeCamp
Content
Lists - Advanced Python 01 2
Tuples - Advanced Python 02 8
Dictionary - Advanced Python 03 14
Sets - Advanced Python 04 19
Strings - Advanced Python 05 26
Collections - Advanced Python 06 32
Itertools - Advanced Python 07 37
Lambda Functions - Advanced Python 08 41
Exceptions And Errors - Advanced Python 09 44
Logging - Advanced Python 10 49
JSON - Advanced Python 11 57
Random Numbers - Advanced Python 12 64
Decorators - Advanced Python 13 69
Generators - Advanced Python 14 76
Threading vs Multiprocessing - Advanced Python 15 81
Multithreading - Advanced Python 16 85
Multiprocessing - Advanced Python 17 93
Function arguments - Advanced Python 18 103
The Asterisk (*) operator - Advanced Python 19 110
Shallow vs Deep Copying - Advanced Python 20 114
Context Managers - Advanced Python 21 118
1
Lists - Advanced Python 01
List is a collection data type which is ordered and mutable.
Unlike Sets, Lists allow duplicate elements.
Access elements¶
You access the list items by referring to the index number.
Note that the indices start at 0.
item = list_1[0]
print(item)
Change items¶
Just refer to the index number and assign a new value.
Useful methods¶
3
Have a look at the Python Documentation to see all list
methods: https://docs.python.org/3/tutorial/
datastructures.html
# concatenation
list_concat = list_with_zeros + my_list
print(list_concat)
Copy a list¶
Be careful when copying references.
list_copy = list_org.copy()
# list_copy = list(list_org)
# list_copy = list_org[:]
Iterating¶
# Iterating over a list by using a for in loop
for i in list_1:
print(i)
banana
cherry
lemon
Slicing¶
Access sub parts of the list wih the use of colon (:), just as with
strings.
List comprehension¶
A elegant and fast way to create a new list from an existing list.
7
a = [1, 2, 3, 4, 5, 6, 7, 8]
b = [i * i for i in a] # squares each element
print(b)
[1, 4, 9, 16, 25, 36, 49, 64]
Nested lists¶
Lists can contain other lists (or other container types).
8
ff
ff
ff
• Tuples with their immutable elements can be used as key
for a dictionary. This is not possible with lists.
• If you have data that doesn't change, implementing it as
tuple will guarantee that it remains write-protected.
Create a tuple¶
Tuples are created with round brackets and comma separated
values. Or use the built-in tuple function.
Access elements¶
You access the tuple items by referring to the index number.
Note that the indices start at 0.
item = tuple_1[0]
print(item)
# You can also use negative indexing, e.g -1 refers to the
last item,
9
# -2 to the second last item, and so on
item = tuple_1[-1]
print(item)
Max
New York
tuple_1[2] = "Boston"
----------------------------------------------------------
-----------------
TypeError Traceback (most
recent call last)
<ipython-input-5-c391d8981369> in <module>
----> 1 tuple_1[2] = "Boston"
Delete a tuple¶
del tuple_2
Iterating¶
# Iterating over a tuple by using a for in loop
for i in tuple_1:
print(i)
Max
28
New York
10
Useful methods¶
my_tuple = ('a','p','p','l','e',)
# repetition
my_tuple = ('a', 'b') * 5
print(my_tuple)
# concatenation
my_tuple = (1,2,3) + (4,5,6)
print(my_tuple)
tuple_to_list = list(list_to_tuple)
print(tuple_to_list)
Slicing¶
Access sub parts of the tuple with the use of colon (:), just as
with strings.
Unpack tuple¶
# number of variables have to match number of tuple
elements
tuple_1 = ("Max", 28, "New York")
name, age, city = tuple_1
print(name)
print(age)
print(city)
12
# tip: unpack multiple elements to a list with *
my_tuple = (0, 1, 2, 3, 4, 5)
item_first, *items_between, item_last = my_tuple
print(item_first)
print(items_between)
print(item_last)
Max
28
New York
0
[1, 2, 3, 4]
5
Nested tuples¶
Tuples can contain other tuples (or other container types).
Dictionary - Advanced
Python 03
A dictionary is a collection which is unordered, changeable and
indexed. A dictionary consists of a collection of key-value
pairs.
Create a dictionary¶
Create a dictionary with braces, or with the built-in dict
function.
14
print(my_dict_2)
{'name': 'Max', 'age': 28, 'city': 'New York'}
{'name': 'Lisa', 'age': 27, 'city': 'Boston'}
Access items¶
name_in_dict = my_dict["name"]
print(name_in_dict)
Delete items¶
# delete a key-value pair
del my_dict["email"]
print(my_dict)
Copy a dictionary¶
Be careful when copying references.
dict_copy = dict_org.copy()
# dict_copy = dict(dict_org)
17
# now modifying the copy does not affect the original
dict_copy["name"] = "Lisa"
print(dict_copy)
print(dict_org)
{'name': 'Lisa', 'age': 28, 'city': 'New York'}
{'name': 'Lisa', 'age': 28, 'city': 'New York'}
{'name': 'Lisa', 'age': 28, 'city': 'New York'}
{'name': 'Max', 'age': 28, 'city': 'New York'}
my_dict.update(my_dict_2)
print(my_dict)
{'name': 'Lisa', 'age': 27, 'email': '[email protected]',
'city': 'Boston'}
18
print(my_dict[my_tuple])
# print(my_dict[8, 7])
Nested dictionaries¶
The values can also be container types (e.g. lists, tuples,
dictionaries).
Create a set¶
Use curly braces or the built-in set function.
19
# or use the set function and create from an iterable,
e.g. list, tuple, string
my_set_2 = set(["one", "two", "three"])
my_set_2 = set(("one", "two", "three"))
print(my_set_2)
my_set_3 = set("aaabbbcccdddeeeeeffff")
print(my_set_3)
Add elements¶
my_set = set()
# note: the order does not matter, and might differ when
printed
print(my_set)
Remove elements¶
# remove(x): removes x, raises a KeyError if element is
not present
my_set = {"apple", "banana", "cherry"}
my_set.remove("apple")
print(my_set)
# KeyError:
# my_set.remove("orange")
21
if "apple" in my_set:
print("yes")
yes
Iterating¶
# Iterating over a set by using a for in loop
# Note: order is not important
my_set = {"apple", "banana", "cherry"}
for i in my_set:
print(i)
banana
apple
cherry
i = odds.intersection(primes)
print(i)
i = evens.intersection(primes)
print(i)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
set()
22
{3, 5, 7}
{2}
Di erence of sets¶
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}
# A.symmetric_difference(B) = B.symmetric_difference(A)
diff_set = setB.symmetric_difference(setA)
print(diff_set)
{4, 5, 6, 7, 8, 9}
{10, 11, 12}
{4, 5, 6, 7, 8, 9, 10, 11, 12}
{4, 5, 6, 7, 8, 9, 10, 11, 12}
Updating sets¶
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}
Copying¶
set_org = {1, 2, 3, 4, 5}
25
Frozenset¶
Frozen set is just an immutable version of normal set. While
elements of a set can be modi ed at any time, elements of
frozen set remains the same after creation. Creation with:
my_frozenset = frozenset(iterable)
a = frozenset([0, 1, 2, 3, 4])
my_string = 'Hello'
26
fi
Python strings are immutable which means they cannot be
changed after they are created.
Creation¶
# use singe or double quotes
my_string = 'Hello'
my_string = "Hello"
my_string = "I' m a 'Geek'"
# escaping backslash
my_string = 'I\' m a "Geek"'
my_string = 'I\' m a \'Geek\''
print(my_string)
# number of characters
print(len(my_string))
30
The binary value is 10
Hello Bob and Tom
The decimal value is 10
The float value is 10.123450
The float value is 10.12
f-Strings¶
New since Python 3.6. Use the variables directly inside the braces.
name = "Eric"
age = 25
a = f"Hello, {name}. You are {age}."
print(a)
pi = 3.14159
a = f"Pi is {pi:.3f}"
print(a)
# f-Strings are evaluated at runtime, which allows
expressions
a = f"The value is {2*60}"
print(a)
Hello, Eric. You are 25.
Pi is 3.142
The value is 120
More on immutability and concatenation¶
# since a string is immutable, adding strings with +, or
+= always
# creates a new string, and therefore is expensive for
multiple operations
# --> join method is much faster
from timeit import default_timer as timer
my_list = ["a"] * 1000000
# bad
start = timer()
a = ""
for i in my_list:
a += i
end = timer()
print("concatenate string with + : %.5f" % (end - start))
# good
start = timer()
a = "".join(my_list)
31
end = timer()
print("concatenate string with join(): %.5f" % (end -
start))
concat string with + : 0.34527
concat string with join(): 0.01191
Collections - Advanced
Python 06
The collections module in Python implements specialized
container datatypes providing alternatives to Python’s general
purpose built-in containers, dict, list, set, and tuple.
Counter¶
A counter is a container that stores elements as dictionary keys,
and their counts are stored as dictionary values.
print(my_counter.items())
print(my_counter.keys())
print(my_counter.values())
my_list = [0, 1, 0, 1, 2, 1, 1, 3, 2, 3, 2, 4]
my_counter = Counter(my_list)
print(my_counter)
33
fi
print(pt._fields)
print(type(pt))
print(pt.x, pt.y)
ordered_dict = OrderedDict()
ordered_dict['a'] = 1
ordered_dict['b'] = 2
ordered_dict['c'] = 3
ordered_dict['d'] = 4
ordered_dict['e'] = 5
print(ordered_dict)
# same functionality as with ordinary dict, but always
ordered
34
ff
fi
for k, v in ordinary_dict.items():
print(k, v)
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e',
5)])
a 1
b 2
c 3
d 4
e 5
defaultdict¶
The defaultdict is a container that's similar to the usual dict
container, but the only di erence is that a defaultdict will have a
default value if that key has not been set yet. If you didn't use a
defaultdict you'd have to check to see if that key exists, and if it
doesn't, set it to what you want.
print(d.items())
print(d['green'])
dict_items([('yellow', 1), ('blue', 2)])
0
dict_items([('yellow', [1, 3]), ('blue', [2, 4]), ('red',
[5])])
[]
deque¶
35
ff
A deque is a double-ended queue. It can be used to add or remove
elements from both ends. Deques support thread safe, memory
e cient appends and pops from either side of the deque with
approximately the same O(1) performance in either direction. The
more commonly used stacks and queues are degenerate forms of
deques, where the inputs and outputs are restricted to a single
end.
product()¶
This tool computes the cartesian product of input iterables.
It is equivalent to nested for-loops. For example, product(A, B)
returns the same as ((x,y) for x in A for y in B).
38
accumulate()¶
Make an iterator that returns accumulated sums, or accumulated
results of other binary functions.
39
fi
for key, group in group_obj:
print(key, list(group))
1
2
3
1
2
3
A
A
A
Lambda Functions -
Advanced Python 08
A lambda function is a small (one line) anonymous function
that is de ned without a name.
def myfunc(n):
return lambda x: x * n
doubler = myfunc(2)
print(doubler(6))
tripler = myfunc(3)
print(tripler(6))
12
18
Custom sorting using a lambda function
as key parameter¶
The key function transforms each element before sorting.
a = [1, 2, 3, 4, 5, 6]
b = list(map(lambda x: x * 2 , a))
a = [1, 2, 3, 4, 5, 6, 7, 8]
b = list(filter(lambda x: (x%2 == 0) , a))
43
fi
Exceptions And Errors -
Advanced Python 09
A Python program terminates as soon as it encounters an
error. In Python, an error can be a syntax error or an exception.
Syntax Errors¶
A Syntax Error occurs when the parser detects a syntactically
incorrect statement. A syntax error can be for example a typo,
missing brackets, no new line (see code below), or wrong
identation (this will actually raise its own IndentationError, but its
subclassed from a SyntaxError).
a = 5 print(a)
File "<ipython-input-5-fed4b61d14cd>", line 1
a = 5 print(a)
^
SyntaxError: invalid syntax
Exceptions¶
Even if a statement is syntactically correct, it may cause an error
when it is executed. This is called an Exception Error. There are
several di erent error classes, for example trying to add a number
and a string will raise a TypeError.
a = 5 + '10'
----------------------------------------------------------
-----------------
TypeError Traceback (most
recent call last)
44
ff
fi
<ipython-input-6-893398416ed7> in <module>
----> 1 a = 5 + '10'
x = -5
if x < 0:
raise Exception('x should not be negative.')
----------------------------------------------------------
-----------------
Exception Traceback (most
recent call last)
<ipython-input-4-2a9e7e673803> in <module>
1 x = -5
2 if x < 0:
----> 3 raise Exception('x should not be negative.')
x = -5
assert (x >= 0), 'x is not positive.'
# --> Your code will be fine if x >= 0
----------------------------------------------------------
-----------------
AssertionError Traceback (most
recent call last)
<ipython-input-7-f9b059c51e45> in <module>
1 x = -5
----> 2 assert (x >= 0), 'x is not positive.'
3 # --> Your code will be fine if x >= 0
AssertionError: x is not positive.
Handling Exceptions¶
45
fi
You can use a try and except block to catch and handle
exceptions. If you can catch an exceptions your program won't
terminate, and can continue.
try:
a = 5 / 1
46
except ZeroDivisionError as e:
print('A ZeroDivisionError occured:', e)
else:
print('Everything is ok')
Everything is ok
finally clause¶
You can use a nally statement that always runs, no matter if there
was an exception or not. This is for example used to make some
cleanup operations.
try:
a = 5 / 1 # Note: No ZeroDivisionError here
b = a + '10'
except ZeroDivisionError as e:
print('A ZeroDivisionError occured:', e)
except TypeError as e:
print('A TypeError occured:', e)
else:
print('Everything is ok')
finally:
print('Cleaning up some stuff...')
A TypeError occured: unsupported operand type(s) for +:
'float' and 'str'
Cleaning up some stuff...
Common built-in Exceptions¶
You can nd all built-in Exceptions here: https://docs.python.org/3/
library/exceptions.html - ImportError: If a module cannot be
imported - NameError: If you try to use a variable that was not
de ned - FileNotFoundError: If you try to open a le that does not
exist or you specify the wrong path - ValueError: When an
operation or function receives an argument that has the right type
but an inappropriate value, e.g. try to remove a value from a list
that does not exist - TypeError: Raised when an operation or
function is applied to an object of inappropriate type. - IndexError:
If you try to access an invalid index of a sequence, e.g a list or a
tuple. - KeyError: If you try to access a non existing key of a
dictionary.
# ImportError
import nonexistingmodule
47
fi
fi
fi
fi
# NameError
a = someundefinedvariable
# FileNotFoundError
with open('nonexistingfile.txt') as f:
read_data = f.read()
# ValueError
a = [0, 1, 2]
a.remove(3)
# TypeError
a = 5 + "10"
# IndexError
a = [0, 1, 2]
value = a[5]
# KeyError
my_dict = {"name": "Max", "city": "Boston"}
age = my_dict["age"]
De ne your own Exceptions¶
You can de ne your own exception class that should be derived
from the built-in Exception class. Most exceptions are de ned with
names that end in 'Error', similar to the naming of the standard
exceptions. Exception classes can be de ned like any other class,
but are usually kept simple, often only o ering a number of
attributes that allow information about the error to be extracted by
handlers.
def test_value(a):
48
fi
fi
ff
fi
fi
if a > 1000:
raise ValueTooHighError('Value is too high.')
if a < 5:
raise ValueTooLowError('Value is too low.', a) #
Note that the constructor takes 2 arguments here
return a
try:
test_value(1)
except ValueTooHighError as e:
print(e)
except ValueTooLowError as e:
print(e.message, 'The value is:', e.value)
Value is too low. The value is: 1
import logging
Log Level¶
There are 5 di erent log levels indicating the serverity of events. By
default, the system logs only events with level WARNING and
above.
import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message
Con guration¶
49
fi
ff
With basicConfig(**kwargs) you can customize the root logger.
The most common parameters are the level, the format, and the
lename. See https://docs.python.org/3/library/
logging.html#logging.basicCon g for all possible arguments. See
also https://docs.python.org/3/library/logging.html#logrecord-
attributes for possible formats and https://docs.python.org/3/
library/time.html#time.strftime how to set the time string. Note that
this function should only be called once, and typically rst thing
after importing the module. It has no e ect if the root logger
already has handlers con gured. For example calling
logging.info(...) before the basicCon g will already set a
handler.
import logging
logging.basicConfig(level=logging.DEBUG, format='%
(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%m/%d/%Y %H:%M:%S')
# Now also debug messages will get logged with a different
format.
logging.debug('Debug message')
# helper.py
# -------------------------------------
50
fi
ff
fi
fi
ff
fi
fi
fi
import logging
logger = logging.getLogger(__name__)
logger.info('HELLO')
# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s -
%(levelname)s - %(message)s')
import helper
# helper.py
# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.propagate = False
logger.info('HELLO')
# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s -
%(levelname)s - %(message)s')
import helper
51
Handler objects are responsible for dispatching the appropriate log
messages to the handler's speci c destination. For example you
can use di erent handlers to send log messaged to the standard
output stream, to les, via HTTP, or via Email. Typically you
con gure each handler with a level (setLevel()), a formatter
(setFormatter()), and optionally a lter (addFilter()). See https://
docs.python.org/3/howto/logging.html#useful-handlers for possible
built-in handlers. Of course you can also implement your own
handlers by deriving from these classes.
import logging
logger = logging.getLogger(__name__)
# Create handlers
stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler('file.log')
stream_format = logging.Formatter('%(name)s - %
(levelname)s - %(message)s')
file_format = logging.Formatter('%(asctime)s - %(name)s -
%(levelname)s - %(message)s')
stream_handler.setFormatter(stream_format)
file_handler.setFormatter(file_format)
52
fi
ff
fi
fi
fi
fi
# overwrite this method. Only log records for which
this
# function evaluates to True will pass the filter.
def filter(self, record):
return record.levelno == logging.INFO
.conf le¶
Create a .conf (or sometimes stored as .ini) le, de ne the loggers,
handlers, and formatters and provide the names as keys. After their
names are de ned, they are con gured by adding the words
logger, handler, and formatter before their names separated by an
underscore. Then you can set the properties for each logger,
handler, and formatter. In the example below, the root logger and a
logger named simpleExample will be con gured with a
StreamHandler.
logging.conf
[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
53
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
fi
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %
(message)s
# Then use the config file in the code
import logging
import logging.config
logging.config.fileConfig('logging.conf')
logger.debug('debug message')
logger.info('info message')
Capture Stack traces¶
Logging the traceback in your exception logs can be very helpful
for troubleshooting issues. You can capture the traceback in
logging.error() by setting the exc_info parameter to True.
import logging
try:
a = [1, 2, 3]
value = a[3]
except IndexError as e:
logging.error(e)
logging.error(e, exc_info=True)
54
ERROR:root:list index out of range
ERROR:root:list index out of range
Traceback (most recent call last):
File "<ipython-input-6-df97a133cbe6>", line 5, in
<module>
value = a[3]
IndexError: list index out of range
If you don't capture the correct Exception, you can also use the
traceback.format_exc() method to log the exception.
import logging
import traceback
try:
a = [1, 2, 3]
value = a[3]
except:
logging.error("uncaught exception: %s",
traceback.format_exc())
Rotating FileHandler¶
When you have a large application that logs many events to a le,
and you only need to keep track of the most recent events, then
use a RotatingFileHandler that keeps the les small. When the log
reaches a certain number of bytes, it gets "rolled over". You can
also keep multiple backup log les before overwriting them.
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
for _ in range(10000):
logger.info('Hello, world!')
TimedRotatingFileHandler¶
55
fi
fi
fi
If your application will be running for a long time, you can use a
TimedRotatingFileHandler. This will create a rotating log based on
how much time has passed. Possible time conditions for the when
parameter are: - second (s) - minute (m) - hour (h) - day (d) - w0-w6
(weekday, 0=Monday) - midnight
import logging
import time
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
for i in range(6):
logger.info('Hello, world!')
time.sleep(50)
Logging in JSON Format¶
If your application generates many logs from di erent modules,
and especially in a microservice architecture, it can be challenging
to locate the important logs for your analysis. Therefore, it is best
practice to log your messages in JSON format, and send them to a
centralized log management system. Then you can easily search,
visualize, and analyze your log records.
I would recommend using this Open Source JSON logger: https://
github.com/madzak/python-json-logger
pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger()
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
56
ff
logger.addHandler(logHandler)
import json
Some advantages of JSON:
JSON format¶
{
"firstName": "Jane",
"lastName": "Doe",
"hobbies": ["running", "swimming", "singing"],
"age": 28,
"children": [
{
"firstName": "Alex",
"age": 5
},
{
"firstName": "Bob",
"age": 7
}
]
57
}
JSON supports primitive types (strings, numbers, boolean), as well
as nested arrays and objects. Simple Python objects are translated
to JSON according to the following conversion:
JSON
Python
dict object
list, tuple array
str string
int, long, oat number
TRUE TRUE
FALSE FALSE
None null
import json
import json
import json
person_json = """
{
"age": 30,
"city": "New York",
"hasChildren": false,
"name": "John",
"titles": [
"engineer",
"programmer"
]
}
"""
person = json.loads(person_json)
print(person)
59
fi
{'age': 30, 'city': 'New York', 'hasChildren': False,
'name': 'John', 'titles': ['engineer', 'programmer']}
Or load data from a le and convert it to a Python object with the
json.load() method.
import json
import json
def encode_complex(z):
if isinstance(z, complex):
# just the key of the class name is important, the
value can be arbitrary.
return {z.__class__.__name__: True, "real":z.real,
"imag":z.imag}
else:
raise TypeError(f"Object of type
'{z.__class__.__name__}' is not JSON serializable")
z = 5 + 9j
zJSON = json.dumps(z, default=encode_complex)
print(zJSON)
{"complex": true, "real": 5.0, "imag": 9.0}
You can also create a custom Encoder class, and overwrite the
default() method. Use this for the cls argument in the
json.dump() method, or use the encoder directly.
60
fi
class ComplexEncoder(JSONEncoder):
z = 5 + 9j
zJSON = json.dumps(z, cls=ComplexEncoder)
print(zJSON)
# or use encoder directly:
zJson = ComplexEncoder().encode(z)
print(zJSON)
{"complex": true, "real": 5.0, "imag": 9.0}
{"complex": true, "real": 5.0, "imag": 9.0}
Decoding¶
Decoding a custom object with the defaut JSONDecoder is possible,
but it will be decoded into a dictionary. Write a custom decode
function that will take a dictionary as input, and creates your
custom object if it can nd the object class name in the dictionary.
Use this function for the object_hook argument in the json.load()
method.
def decode_complex(dct):
if complex.__name__ in dct:
return complex(dct["real"], dct["imag"])
return dct
class User:
# Custom class with all class variables given in the
__init__()
def __init__(self, name, age, active, balance,
friends):
self.name = name
self.age = age
self.active = active
self.balance = balance
self.friends = friends
class Player:
# Other custom class
def __init__(self, name, nickname, level):
self.name = name
self.nickname = nickname
self.level = level
def encode_obj(obj):
"""
Takes in a custom object and returns a dictionary
representation of the object.
This dict representation also includes the object's
module and class names.
"""
62
return obj_dict
def decode_dct(dct):
"""
Takes in a dict and returns a custom object associated
with the dict.
It makes use of the "__module__" and "__class__"
metadata in the dictionary
to know which object type to create.
"""
if "__class__" in dct:
# Pop ensures we remove metadata from the dict to
leave only the instance arguments
class_name = dct.pop("__class__")
userJSON = json.dumps(user,default=encode_obj,indent=4,
sort_keys=True)
print(userJSON)
user_decoded = json.loads(userJSON,
object_hook=decode_dct)
63
print(type(user_decoded))
player_decoded = json.loads(playerJSON,
object_hook=decode_dct)
print(type(player_decoded))
{
"__class__": "User",
"__module__": "__main__",
"active": true,
"age": 28,
"balance": 20.7,
"friends": [
"Jane",
"Tom"
],
"name": "John"
}
<class '__main__.User'>
{
"__class__": "Player",
"__module__": "__main__",
"level": 5,
"name": "Max",
"nickname": "max1234"
}
<class ‚__main__.Player'>
Random Numbers -
Advanced Python 12
Python de nes a set of functions that are used to generate or
manipulate random numbers. This tutorial covers the random
module.
64
fi
Python de nes a set of functions that are used to generate or
manipulate random numbers. This article covers:
import random
random.seed(1)
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
66
print('\nRe-seeding with 1...\n')
random.seed(1) # Re-seed
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
Seeding with 1...
0.13436424411240122
8.626903632435095
B
0.6394267984578837
1.2250967970040025
E
0.13436424411240122
8.626903632435095
B
0.6394267984578837
1.2250967970040025
E
The secrets module¶
The secrets module is used for generating cryptographically strong
random numbers suitable for managing data such as passwords,
account authentication, security tokens, and related secrets.
In particularly, secrets should be used in preference to the default
67
pseudo-random number generator in the random module, which is
designed for modelling and simulation, not security or
cryptography.
import secrets
import numpy as np
np.random.seed(1)
# rand(d0,d1,…,dn)
# generate nd array with random floats, arrays has size
(d0,d1,…,dn)
print(np.random.rand(3))
# reset the seed
np.random.seed(1)
print(np.random.rand(3))
Decorators - Advanced
Python 13
A decorator is a function that takes another function and
extends the behavior of this function without explicitly
modifying it.
• Function decoratos
• Class decorators
A function is decorated with the @ symbol:
69
@my_decorator
def my_function():
pass
Function decorators¶
In order to understand the decorator pattern, we have to
understand that functions in Python are rst class objects, which
means that – like any other object – they can be de ned inside
another function, passed as argument to another function, or
returned from other functions.
def wrapper():
print('Start')
func()
print('End')
return wrapper
def print_name():
print('Alex')
print_name()
print()
Start
70
fi
fi
Alex
End
The decorator syntax¶
Instead of wrapping our function and asigning it to itself, we can
achieve the same thing simply by decorating our function with an @.
@start_end_decorator
def print_name():
print('Alex')
print_name()
Start
Alex
End
What about function arguments¶
If our function has input arguments and we try to wrap it with our
decorator above, it will raise a TypeError since we have to call our
function inside the wrapper with this arguments, too. However, we
can x this by using *args and **kwargs in the inner function:
def start_end_decorator_2(func):
@start_end_decorator_2
def add_5(x):
return x + 5
result = add_5(10)
print(result)
Start
End
None
Return values¶
71
fi
Note that in the example above, we do not get the result back, so
as next step we also have to return the value from our inner
function:
def start_end_decorator_3(func):
@start_end_decorator_3
def add_5(x):
return x + 5
result = add_5(10)
print(result)
Start
End
15
What about the function identity?¶
If we have a look at the name of our decorated function, and
inspect it with the built-in help function, we notice that Python
thinks our function is now the wrapped inner function of the
decorator function.
print(add_5.__name__)
help(add_5)
wrapper
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
To x this, use the functools.wraps decorator, which will preserve
the information about the original function. This is helpful for
introspection purposes, i.e. the ability of an object to know about
its own attributes at runtime:
import functools
def start_end_decorator_4(func):
72
fi
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('Start')
result = func(*args, **kwargs)
print('End')
return result
return wrapper
@start_end_decorator_4
def add_5(x):
return x + 5
result = add_5(10)
print(result)
print(add_5.__name__)
help(add_5)
Start
End
15
add_5
Help on function add_5 in module __main__:
add_5(x)
The nal template for own decorators¶
Now that we have all parts, our template for any decorator looks
like this:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Do something before
result = func(*args, **kwargs)
# Do something after
return result
return wrapper
Decorator function arguments¶
Note that functools.wraps is a decorator that takes an argument
for itself. We can think of this as 2 inner functions, so an inner
function within an inner function. To make this clearer, we look at
73
fi
another example: A repeat decorator that takes a number as input.
Within this function, we have the actual decorator function that
wraps our function and extends its behaviour within another inner
function. In this case, it repeats the input function the given number
of times.
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet('Alex')
Hello Alex
Hello Alex
Hello Alex
Nested Decorators¶
We can apply several decorators to a function by stacking them on
top of each other. The decorators are being executed in the order
they are listed.
@debug
@start_end_decorator_4
def say_hello(name):
greeting = f'Hello {name}'
print(greeting)
return greeting
import functools
class CountCalls:
# the init needs to have the func as argument and
stores it
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
@CountCalls
def say_hello(num):
print("Hello!")
say_hello(5)
say_hello(5)
Call 1 of 'say_hello'
Hello!
Call 2 of 'say_hello'
Hello!
Some typical use cases¶
• Use a timer decorator to calculate the execution time of a
function
• Use a debug decorator to print out some more information
about the called function and its arguments
• Use a check decorator to check if the arguments ful ll some
requirements and adapt the bevaviour accordingly
• Register functions (plugins)
• Slow down code with time.sleep() to check network
behaviour
• Cache the return values for memoization (https://
en.wikipedia.org/wiki/Memoization)
• Add information or update a state
Generators - Advanced
Python 14
Generators are functions that can be paused and resumed on
the y, returning an object that can be iterated over.
76
fl
fi
Unlike lists, they are lazy and thus produce items one at a time and
only when asked. So they are much more memory e cient when
dealing with large datasets.
A generator is de ned like a normal function but with the yield
statement instead of return.
def my_generator():
yield 1
yield 2
yield 3
Execution of a generator function¶
Calling the function does not execute it. Instead, the function
returns a generator object which is used to control execution.
Generator objects execute when next() is called. When calling
next() the rst time, execution begins at the start of the function
and continues until the rst yield statement where the value to the
right of the statement is returned. Subsequent calls to next()
continue from the yield statement (and loop around) until another
yield is reached. If yield is not called because of a condition or
the end is reached, a StopIteration exception is raised:
def countdown(num):
print('Starting')
while num > 0:
yield num
num -= 1
77
fi
fi
fi
ffi
2
1
----------------------------------------------------------
-----------------
StopIteration Traceback (most
recent call last)
<ipython-input-1-3941498e0bf0> in <module>
16
17 # this will raise a StopIteration
---> 18 print(next(cd))
StopIteration:
# you can iterate over a generator object with a for in
loop
cd = countdown(3)
for x in cd:
print(x)
Starting
3
2
1
# you can use it for functions that take iterables as
input
cd = countdown(3)
sum_cd = sum(cd)
print(sum_cd)
cd = countdown(3)
sorted_cd = sorted(cd)
print(sorted_cd)
Starting
6
Starting
[1, 2, 3]
Big advantage: Generators save
memory!¶
Since the values are generated lazily, i.e. only when needed, it
saves a lot of memory, especially when working with large data.
Furthermore, we do not need to wait until all the elements have
been generated before we start to use them.
78
# without a generator, the complete sequence has to be
stored here in a list
def firstn(n):
num, nums = 0, []
while num < n:
nums.append(num)
num += 1
return nums
sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
499999500000
8697464 bytes
# with a generator, no additional sequence is needed to
store the numbers
def firstn(n):
num = 0
while num < n:
yield num
num += 1
sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
499999500000
120 bytes
Another example: Fibonacci numbers¶
def fibonacci(limit):
a, b = 0, 1 # first two fibonacci numbers
while a < limit:
yield a
a, b = b, a + b
fib = fibonacci(30)
# generator objects can be converted to a list (only used
for printing here)
print(list(fib))
[0, 1, 1, 2, 3, 5, 8, 13, 21]
Generator expressions¶
79
Just like list comprehensions, generators can be written in the
same syntax except with parenthesis instead of square brackets.
Be careful not to mix them up, since generator expressions are
often slower than list comprehensions because of the overhead of
function calls (https://stackover ow.com/questions/11964130/list-
comprehension-vs-generator-expressions-weird-timeit-results/
11964478#11964478)
# generator expression
mygenerator = (i for i in range(1000) if i % 2 == 0)
print(sys.getsizeof(mygenerator), "bytes")
# list comprehension
mylist = [i for i in range(1000) if i % 2 == 0]
print(sys.getsizeof(mylist), "bytes")
120 bytes
4272 bytes
Concept behind a generator¶
This class implements our generator as an iterable object. It has to
implement __iter__ and __next__ to make it iterable, keep track of
the current state (the current number in this case), and take care of
a StopIteration. It can be used to understand the concept behind
generators. However, there is a lot of boilerplate code, and the
logic is not as clear as with a simple function using the yield
keyword.
class firstn:
def __init__(self, n):
self.n = n
self.num = 0
def __iter__(self):
return self
def __next__(self):
if self.num < self.n:
cur = self.num
self.num += 1
return cur
else:
raise StopIteration()
80
fl
firstn_object = firstn(1000000)
print(sum(firstn_object))
499999500000
Threading vs
Multiprocessing - Advanced
Python 15
Overview and comparison of threads and processes, and how
to use it in Python.
Process¶
A Process is an instance of a program, e.g. a Python interpreter.
They are independent from each other and do not share the same
memory.
81
fi
A thread is an entity within a process that can be scheduled for
execution (Also known as "leightweight process"). A Process can
spawn multiple threads. The main di erence is that all threads
within a process share the same memory.
• One GIL for all threads, i.e. threads are limited by GIL
• Multithreading has no e ect for CPU-bound tasks due to the
GIL
• Not interruptible/killable -> be careful with memory leaks
• increased potential for race conditions
Threading in Python¶
Use the threading module.
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
threads = []
num_threads = 10
82
ff
ff
fi
for thread in threads:
thread.start()
Multiprocessing¶
Use the multiprocessing module. The syntax is very similar to
above.
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
processes = []
num_processes = os.cpu_count()
83
# start all processes
for process in processes:
process.start()
Why is it needed?¶
It is needed because CPython's (reference implementation of
Python) memory management is not thread-safe. Python uses
reference counting for memory management. It means that objects
created in Python have a reference count variable that keeps track
of the number of references that point to the object. When this
count reaches zero, the memory occupied by the object is
released. The problem was that this reference count variable
needed protection from race conditions where two threads
increase or decrease its value simultaneously. If this happens, it
can cause either leaked memory that is never released or
84
ff
incorrectly release the memory while a reference to that object still
exists.
Multithreading - Advanced
Python 16
In this tutorial we talk about how to use the `threading` module
in Python.
Call thread.join() to tell the program that it should wait for this
thread to complete before it continues with the rest of the code.
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
threads = []
num_threads = 10
Task: Create two threads, each thread should access the current
database value, modify it (in this case only increase it by 1), and
86
write the new value back into the database value. Each thread
should do this operation 10 times.
def increase():
global database_value # needed to modify the global
value
if __name__ == "__main__":
t1 = Thread(target=increase)
t2 = Thread(target=increase)
t1.start()
t2.start()
t1.join()
t2.join()
print('end main')
Start value: 0
End value: 1
end main
87
How to use Locks¶
Notice that in the above example, the 2 threads should increment
the value by 1, so 2 increment operations are performed. But why
is the end value 1 and not 2?
Race condition¶
A race condition happened here. A race condition occurs when two
or more threads can access shared data and they try to change it
at the same time. Because the thread scheduling algorithm can
swap between threads at any time, you don't know the order in
which the threads will attempt to access the shared data. In our
case, the rst thread accesses the database_value (0) and stores it
in a local copy. It then increments it (local_copy is now 1). With our
time.sleep() function that just simulates some time consuming
operations, the programm will swap to the second thread in the
meantime. This will also retrieve the current database_value (still 0)
and increment the local_copy to 1. Now both threads have a local
copy with value 1, so both will write the 1 into the global
database_value. This is why the end value is 1 and not 2.
Important: You should always release the block again after it was
acquired!
In our example the critical code section where database values are
retrieved and modi ed is now locked. This prevents the second
88
fi
fi
thread from mody ng the global data at the same time. Not much
has changed in our code. All new changes are commented in the
code below.
# import Lock
from threading import Thread, Lock
import time
database_value = 0
def increase(lock):
global database_value
local_copy = database_value
local_copy += 1
time.sleep(0.1)
database_value = local_copy
if __name__ == "__main__":
# create a lock
lock = Lock()
t1.start()
t2.start()
t1.join()
t2.join()
def increase(lock):
global database_value
with lock:
local_copy = database_value
local_copy += 1
time.sleep(0.1)
database_value = local_copy
Using Queues in Python¶
Queues can be used for thread-safe/process-safe data exchanges
and data processing both in a multithreaded and a multiprocessing
environment.
The queue¶
A queue is a linear data structure that follows the First In First Out
(FIFO) principle. A good example is a queue of customers that are
waiting in line, where the customer that came rst is served rst.
# create queue
q = Queue()
# add elements
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1
90
fi
fi
# now q looks like this:
# back --> 3 2 1 --> front
91
fi
fi
fi
fi
from queue import Queue
# do stuff...
with lock:
# prevent printing at the same time with this
lock
print(f"in {current_thread().name} got
{value}")
# ...
if __name__ == '__main__':
q = Queue()
num_threads = 10
lock = Lock()
for i in range(num_threads):
t = Thread(name=f"Thread{i+1}", target=worker,
args=(q, lock))
t.daemon = True # dies when the main thread dies
t.start()
print('main done')
in Thread1 got 0
in Thread2 got 1
in Thread2 got 11
in Thread2 got 12
92
in Thread2 got 13
in Thread2 got 14
in Thread2 got 15
in Thread2 got 16
in Thread2 got 17
in Thread2 got 18
in Thread2 got 19
in Thread8 got 5
in Thread4 got 9
in Thread1 got 10
in Thread5 got 2
in Thread6 got 3
in Thread9 got 6
in Thread7 got 4
in Thread10 got 7
in Thread3 got 8
main done
Daemon threads¶
In the above example, daemon threads are used. Daemon threads
are background threads that automatically die when the main
program ends. This is why the in nite loops inside the worker
methods can be exited. Without a daemon process we would have
to use a signalling mechanism such as a threading.Event to stop
the worker. But be careful with daemon processes: They are
abruptly stopped and their resources (e.g. open les or database
transactions) may not be released/completed properly.
Multiprocessing - Advanced
Python 17
In this tutorial we talk about how to use the `multiprocessing`
module in Python.
Call process.join() to tell the program that it should wait for this
process to complete before it continues with the rest of the code.
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
processes = []
num_processes = os.cpu_count()
# number of CPUs on the machine. Usually a good choise
for the number of processes
94
process.start()
def add_100(number):
for _ in range(100):
time.sleep(0.01)
number.value += 1
def add_100_array(numbers):
for _ in range(100):
time.sleep(0.01)
for i in range(len(numbers)):
numbers[i] += 1
if __name__ == "__main__":
95
shared_number = Value('i', 0)
print('Value at beginning:', shared_number.value)
process1 = Process(target=add_100,
args=(shared_number,))
process2 = Process(target=add_100,
args=(shared_number,))
process3 = Process(target=add_100_array,
args=(shared_array,))
process4 = Process(target=add_100_array,
args=(shared_array,))
process1.start()
process2.start()
process3.start()
process4.start()
process1.join()
process2.join()
process3.join()
process4.join()
print('end main')
Value at beginning: 0
Array at beginning: [0.0, 100.0, 200.0]
Value at end: 144
Array at end: [134.0, 237.0, 339.0]
end main
How to use Locks¶
Notice that in the above example, the 2 processes should
increment the shared value by 1 for 100 times. This results in 200
total operations. But why is the end value not 200?
Race condition¶
96
A race condition happened here. A race condition occurs when two
or more processes or threads can access shared data and they try
to change it at the same time. In our example the two processes
have to read the shared value, increase it by 1, and write it back
into the shared variable. If this happens at the same time, the two
processes read the same value, increase it and write it back. Thus,
both processes write the same increased value back into the
shared object, and the value was not increased by 2. See https://
www.python-engineer.com/learn/advancedpython16_threading/ for
a detailed explanation of race conditions.
Important: You should always release the block again after it was
acquired!
In our example the critical code section where the shared variable
is read and increased is now locked. This prevents the second
process from mody ng the shared object at the same time. Not
much has changed in our code. All new changes are commented in
the code below.
# import Lock
from multiprocessing import Lock
from multiprocessing import Process, Value, Array
import time
number.value += 1
if __name__ == "__main__":
# create a lock
lock = Lock()
shared_number = Value('i', 0)
print('Value at beginning:', shared_number.value)
process3 = Process(target=add_100_array,
args=(shared_array, lock))
process4 = Process(target=add_100_array,
args=(shared_array, lock))
process1.start()
process2.start()
process3.start()
process4.start()
process1.join()
process2.join()
98
process3.join()
process4.join()
print('end main')
Value at beginning: 0
Array at beginning: [0.0, 100.0, 200.0]
Value at end: 200
Array at end: [200.0, 300.0, 400.0]
end main
Use the lock as a context manager¶
After lock.acquire() you should never forget to call
lock.release() to unblock the code. You can also use a lock as a
context manager, wich will safely lock and unlock your code. It is
recommended to use a lock this way:
The queue¶
A queue is a linear data structure that follows the First In First Out
(FIFO) principle. A good example is a queue of customers that are
waiting in line, where the customer that came rst is served rst.
# create queue
99
fi
fi
q = Queue()
# add elements
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1
numbers = range(1, 6)
q = Queue()
p1 = Process(target=square, args=(numbers,q))
p2 = Process(target=make_negative, args=(numbers,q))
p1.start()
p2.start()
p1.join()
p2.join()
print('end main')
1
4
9
16
25
-1
-2
-3
-4
-5
end main
Process Pools¶
A process pool object controls a pool of worker processes to
which jobs can be submitted It supports asynchronous results with
timeouts and callbacks and has a parallel map implementation. It
can automatically manage the available processors and split data
into smaller chunks which can then be processed in parallel by
di erent processes. See https://docs.python.org/3.7/library/
multiprocessing.html#multiprocessing.pool for all possible
methods. Important methods are:
101
ff
• map(func, iterable[, chunksize]) : This method chops the
iterable into a number of chunks which it submits to the
process pool as separate tasks. The (approximate) size of
these chunks can be speci ed by setting chunksize to a
positive integer. It blocks until the result is ready.
• close() : Prevents any more tasks from being submitted to
the pool. Once all the tasks have been completed the worker
processes will exit.
• join(): Wait for the worker processes to exit. One must call
close() or terminate() before using join().
• apply(func, args): Call func with arguments args. It blocks
until the result is ready. func is only executed in ONE of the
workers of the pool.
Note: There are also asynchronous variants map_async() and
apply_async() that will not block. They can execute callbacks
when the results are ready.
def cube(number):
return number * number * number
if __name__ == "__main__":
numbers = range(10)
p = Pool()
# or
# result = [p.apply(cube, args=(i,)) for i in numbers]
p.close()
p.join()
print(result)
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
102
fi
Function arguments -
Advanced Python 18
In this article we will talk about function parameters and
function arguments in detail.
We will learn:
# positional arguments
foo(1, 2, 3)
# keyword arguments
foo(a=1, b=2, c=3)
foo(c=3, b=2, a=1) # Note that the order is not important
here
# mix of both
foo(1, b=2, c=3)
# default arguments
def foo(a, b, c, d=4):
print(a, b, c, d)
foo(1, 2, 3, 4)
foo(1, b=2, c=3, d=100)
104
fi
ff
fi
Variable-length arguments (*args and
**kwargs)¶
• If you mark a parameter with one asterisk (*), you can pass
any number of positional arguments to your function (Typically
called *args)
• If you mark a parameter with two asterisks (**), you can pass
any number of keyword arguments to this function (Typically
called **kwargs).
def foo(a, b, *args, **kwargs):
print(a, b)
for arg in args:
print(arg)
for kwarg in kwargs:
print(kwarg, kwargs[kwarg])
1 2
three 3
Forced keyword arguments¶
Sometimes you want to have keyword-only arguments. You can
enforce that with: - If you write '*,' in your function parameter list,
all parameters after that must be passed as keyword arguments. -
Arguments after variable-length arguments must be keyword
arguments.
105
def foo(a, b, *, c, d):
print(a, b, c, d)
def foo1():
x = number # global variable can only be accessed here
print('number in function:', x)
number = 0
foo1()
number = 0
def foo3():
number = 3 # this is a local variable
var = 10
print('var before foo():', var)
foo(var)
print('var after foo():', var)
var before foo(): 10
var after foo(): 10
# mutable objects -> change
def foo(a_list):
a_list.append(4)
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
my_list before foo(): [1, 2, 3]
my_list after foo(): [1, 2, 3, 4]
# immutable objects within a mutable object -> change
def foo(a_list):
a_list[0] = -100
a_list[2] = "Paul"
108
ff
my_list = [1, 2, "Max"]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
my_list before foo(): [1, 2, 'Max']
my_list after foo(): [-100, 2, 'Paul']
# Rebind a mutable reference -> no change
def foo(a_list):
a_list = [50, 60, 70] # a_list is now a new local
variable within the function
a_list.append(50)
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
my_list before foo(): [1, 2, 3]
my_list after foo(): [1, 2, 3]
Be careful with += and = operations for mutable types. The rst
operation has an e ect on the passed argument while the latter has
not:
def bar(a_list):
a_list = a_list + [4, 5] # this rebinds the reference
to a new local variable
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
my_list = [1, 2, 3]
print('my_list before bar():', my_list)
bar(my_list)
print('my_list after bar():', my_list)
my_list before foo(): [1, 2, 3]
my_list after foo(): [1, 2, 3, 4, 5]
my_list before bar(): [1, 2, 3]
my_list after bar(): [1, 2, 3]
109
ff
fi
The Asterisk (*) operator -
Advanced Python 19
This tutorial covers the asterisk sign (`*`) and its di erent use
cases in Python.
The asterisk sign (*) can be used for di erent cases in Python: -
Multiplication and power operations - Creation of list, tuple, or
string with repeated elements - *args , **kwargs , and keyword-
only parameters - Unpacking lists/tuples/dictionaries for function
arguments - Unpacking containers - Merging containers into list /
Merge dictionaries
# power operation
result = 2 ** 4
print(result)
35
16
Creation of list, tuple, or string with
repeated elements¶
# list
zeros = [0] * 10
onetwos = [1, 2] * 5
print(zeros)
print(onetwos)
# tuple
zeros = (0,) * 10
onetwos = (1, 2) * 5
print(zeros)
print(onetwos)
110
ff
ff
# string
A_string = "A" * 10
AB_string = "AB" * 5
print(A_string)
print(AB_string)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
(1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
AAAAAAAAAA
ABABABABAB
*args , **kwargs , and keyword-only
arguments¶
• Use *args for variable-length arguments
• Use **kwargs for variable-length keyword arguments
• Use *, followed by more function parameters to enforce
keyword-only arguments
def my_function(*args, **kwargs):
for arg in args:
print(arg)
for key in kwargs:
print(key, kwargs[key])
my_string = "ABC"
foo(*my_string)
numbers = (1, 2, 3, 4, 5, 6, 7, 8)
print()
print()
112
first, *middle, last = numbers
print(first)
print(middle)
print(last)
[1, 2, 3, 4, 5, 6, 7]
8
1
[2, 3, 4, 5, 6, 7, 8]
1
[2, 3, 4, 5, 6, 7]
8
Merge iterables into a list / Merge
dictionaries¶
This is possible since Python 3.5 thanks to PEP 448 (https://
www.python.org/dev/peps/pep-0448/).
113
fl
# this works:
# dict_c = {**dict_a, **dict_b}
----------------------------------------------------------
-----------------
TypeError Traceback (most
recent call last)
<ipython-input-52-2660fb90a60f> in <module>
1 dict_a = {'one': 1, 'two': 2}
2 dict_b = {3: 3, 'four': 4}
----> 3 dict_c = dict(dict_a, **dict_b)
4 print(dict_c)
5
TypeError: keywords must be strings
Recommended further readings: - https://treyhunner.com/2018/10/
asterisks-in-python-what-they-are-and-how-to-use-them/ - https://
treyhunner.com/2016/02/how-to-merge-dictionaries-in-python/
114
ff
fi
ff
ff
Assignment operation¶
This will only create a new variable with the same reference.
Modifying one will a ect the other.
list_a = [1, 2, 3, 4, 5]
list_b = list_a
list_a[0] = -10
print(list_a)
print(list_b)
[-10, 2, 3, 4, 5]
[-10, 2, 3, 4, 5]
Shallow copy¶
One level deep. Modifying on level 1 does not a ect the other list.
Use copy.copy(), or object-speci c copy functions/copy
constructors.
import copy
list_a = [1, 2, 3, 4, 5]
list_b = copy.copy(list_a)
import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.copy(list_a)
115
ff
fi
ff
ff
Note: You can also use the following to create shallow copies:
# shallow copies
list_b = list(list_a)
list_b = list_a[:]
list_b = list_a.copy()
Deep copies¶
Full independent clones. Use copy.deepcopy().
import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.deepcopy(list_a)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class Company:
def __init__(self, boss, employee):
self. boss = boss
self.employee = employee
company_clone = copy.copy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)
print()
# deep copy will not affect nested objects
boss = Person('Jane', 55)
employee = Person('Joe', 28)
company = Company(boss, employee)
company_clone = copy.deepcopy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)
56
56
55
56
117
Context Managers -
Advanced Python 21
Context managers are a great tool for resource management.
They allow you to allocate and release resources precisely
when you want to.
f = open('notes.txt', 'w')
try:
f.write('some todo...')
finally:
f.close()
We can see that using a context manager and the with statement
is much shorter and more concise.
# error-prone:
lock.acquire()
# do stuff
# lock should always be released!
lock.release()
118
fi
fi
fi
# Better:
with lock:
# do stuff
Implementing a context manager as a
class¶
To support the with statement for our own classes, we have to
implement the __enter__ and __exit__ methods. Python calls
__enter__ when execution enters the context of the with
statement. In here the resource should be acquired and returned.
When execution leaves the context again, __exit__ is called and
the resource is freed up.
class ManagedFile:
def __init__(self, filename):
print('init', filename)
self.filename = filename
def __enter__(self):
print('enter')
self.file = open(self.filename, 'w')
return self.file
with ManagedFile('notes.txt') as f:
print('doing stuff...')
f.write('some todo...')
init notes.txt
enter
doing stuff...
exit
Handling exceptions¶
If an exception occurs, Python passes the type, value, and
traceback to the __exit__ method. It can handle the exception
119
here. If anything other than True is returned by the __exit__
method, then the exception is raised by the with statement.
class ManagedFile:
def __init__(self, filename):
print('init', filename)
self.filename = filename
def __enter__(self):
print('enter')
self.file = open(self.filename, 'w')
return self.file
# No exception
with ManagedFile('notes.txt') as f:
print('doing stuff...')
f.write('some todo...')
print('continuing...')
print()
init notes2.txt
enter
doing stuff...
120
exc: <class 'AttributeError'> '_io.TextIOWrapper' object
has no attribute 'do_something'
exit
----------------------------------------------------------
-----------------
AttributeError Traceback (most
recent call last)
<ipython-input-24-ed1604efb530> in <module>
27 print('doing stuff...')
28 f.write('some todo...')
---> 29 f.do_something()
30 print('continuing...')
AttributeError: '_io.TextIOWrapper' object has no
attribute 'do_something'
We can handle the exception in the __exit__ method and return
True.
class ManagedFile:
def __init__(self, filename):
print('init', filename)
self.filename = filename
def __enter__(self):
print('enter')
self.file = open(self.filename, 'w')
return self.file
with ManagedFile('notes2.txt') as f:
print('doing stuff...')
f.write('some todo...')
f.do_something()
print('continuing...')
init notes2.txt
121
enter
doing stuff...
Exception has been handled
exit
continuing...
Implementing a context manager as a
generator¶
Instead of writing a class, we can also write a generator function
and decorate it with the contextlib.contextmanager decorator.
Then we can also call the function using a with statement. For this
approach, the function must yield the resource in a try statement,
and all the content of the __exit__ method to free up the resource
goes now inside the corresponding finally statement.
@contextmanager
def open_managed_file(filename):
f = open(filename, 'w')
try:
yield f
finally:
f.close()
with open_managed_file('notes.txt') as f:
f.write('some todo...')
The generator rst acquires the resource. It then temporarily
suspends its own execution and yields the resource so it can be
used by the caller. When the caller leaves the with context, the
generator continues to execute and frees up the resource in the
finally statement.
122
fi