Editor's note: This guide is part of a series on useful Python tricks. Read more about the series and find links the other guides here.
This is the second of two guides on basic Python tricks. In the first, we learned many useful basic Python tricks. Check it out if you missed it.
In this guide, we will continue to add more basic Python tricks to our arsenal.
Multiple assignment can assign multiple variables at the same time, and it is useful in multiple initialization or swap.
1"""multiple initialization"""
2a, b = 1, 2
3"""swap"""
4a, b = b, a
5# a:2, b:1
1"""next assignment in linked list"""
2# L206: reverse a linked list
3def reverse(head: ListNode) -> ListNode:
4 prev, cur = None, head
5 while cur:
6 cur.next, cur, prev = prev, cur.next, cur
7 return prev
Multiple assignment is a pack-unpack technique.
1a, b = b, a
is equivalent to
1pack = (b, a)
2a = pack[0]
3b = pack[1]
From the implementation mechanism, we can see that the so-called "same time" assignment is actually executing sequentially. So be careful with the assignment order in complex scenarios. See the following two examples.
Wrong Version:
1"""nums[0] has been changed before usage"""
2nums[0], nums[nums[0]] = nums[nums[0]], nums[0]
Right Version:
1nums[nums[0]], nums[0] = nums[0], nums[nums[0]]
Wrong Version:
1"""cur.next has been changed before usage"""
2cur.next, cur.next.next.next, cur.next.next = cur.next.next, cur.next, cur.next.next.next
Right Version:
1cur.next.next.next, cur.next.next, cur.next = cur.next, cur.next.next.next, cur.next.next
Some programmers may feel confused by the negative index because the positive index starts with 0 while the negative index starts with -1.
If you are confused about -
and looking for another way to index backward more understandably, you can try mirror index ~
—a mirror of forward. It starts from rightmost with ~0
, which is more unified.
As I said in this post, ~
actually is a math trick of two's complement binary code, and it is easier to understand in certain situations.
1"""index start from right to left"""
2arr = ["a", "b", "c", "d"]
3arr[~0], arr[~1]
4# output: ('d', 'c')
Here are the typical usages for mirror index ~
:
1"""swap mirror node"""
2def reverse(arr: List[int]) -> None:
3 for i in range(len(arr) // 2):
4 arr[i], arr[~i] = arr[~i], arr[i]
5
6"""find median in a sort list"""
7def median(arr: List[float]) -> float:
8 mid = len(arr) // 2
9 return (arr[mid] + arr[~mid]) / 2
10
11"""deal with mirror pairs"""
12# L246: verify whether a number is strobogrammatic
13# strobogrammatic number looks the same when rotated 180 degrees
14def is_strobogrammatic(num: str) -> bool:
15 return all(num[i] + num[~i] in '696 00 11 88' for i in range(len(num) // 2 + 1))
Python adds an implicit return None
to the end of any function. Therefore, if a function doesn't specify a return value, it returns None
by default.
Put plainly, return None
equals to return
, and also equals to no return at all.
1"""no return equals to return None, more concise"""
2# L226: invert a binary tree
3def invert_tree(root: TreeNode) -> TreeNode:
4 if root:
5 root.left, root.right = invertTree(root.right), invertTree(root.left)
6 return root
When you specify a slice on the left side of the =
operator, that means slice assignment. Slice assignment is a special syntax for list, where you can insert, delete, or replace old slices from another list in a single operation:
1arr = [0, 1, 2, 3, 4, 5]
2
3"""insert"""
4arr[1:1] = [6, 7]
5# arr: [0, 6, 7, 1, 2, 3, 4, 5]
6
7"""delete"""
8arr[1:3] = []
9# arr: [0, 1, 2, 3, 4, 5]
10
11"""replace"""
12arr[1:3] = [6, 7]
13# arr: [0, 6, 7, 3, 4, 5]
14
15"""replace slice with different size"""
16arr[-2:] = [2] * 3
17# arr: [0, 6, 7, 3, 2, 2, 2]
18
19"""replace the entire list"""
20arr[:] = [1, 2, 3]
21# arr: [1, 2, 3]
1"""replace(or batch assignment) by slice assignment"""
2# L280: reorder unsort array in-place such that nums[0] <= nums[1] >= nums[2] <= nums[3]...
3def wiggle_sort(nums: List[int]) -> None:
4 for i in range(len(nums)):
5 nums[i:i+2] = sorted(nums[i:i+2], reverse=i%2)
If you need something like Integer.MAX_VALUE
in Java, you can use float('inf')
, thanks to the dynamic typing of Python.
1"""float('inf'), float('-inf') as the initial value of min_val and max_val"""
2min_val, max_val = float('inf'), float('-inf')
3
4for i in range(10):
5 min_val = min(min_val, i)
6 max_val = max(max_val, i)
7# min_val:0, max_val:9
According to Transforming Code into Beautiful, Idiomatic Python by Raymond Hettinger, for else
/ while else
distinguishes multiple exit points in loops. It is a replacement for certain GOTO
use cases. The more accurate term for else
here is no break.
In my understanding, this is used when you care about both break and non-break logics and allows you to omit the tracking variable.
1found_obj = None
2for obj in objects:
3 if obj.key == search_key:
4 found_obj = obj
5 break
6else: # no break
7 print('no object found')
Python doesn't have a switch
keyword, but there are several ways to emulate switch
. The simple and intuitive way is an 'if-else-if' ladder. However, because of the jump table, a switch statement is much faster than this solution.
A more Pythonic way is to use dictionary mapping (associative array) with lambda
:
1"""emulate switch/case with dict mapping"""
2def op_dict(operator: str, x: float, y: float) -> float:
3 return {
4 '+': lambda: x + y,
5 '-': lambda: x - y,
6 '*': lambda: x * y,
7 '/': lambda: x / y,
8 }.get(operator, lambda: None)()
9
10op_dict('*', 2, 3)
11# output: 6
In another method, you can use the reflection technique (getattr
) to dynamically determine which function needs to be called during runtime.
Here is a good explanation of Python's decorator mechanism:
In Python, functions are the first class objects. This means that they support operations such as being passed as an argument, returned from a function, modified, and assigned to a variable.
Decorators are a very powerful and useful tool in Python since they allow programmers to modify the behavior of function or class. Decorators allow us to wrap another function in order to extend the behavior of wrapped function, without permanently modifying it.
In the following example, we use decorator technique to decorate the Fibonacci function, then enhance it with memoization.
1from functools import wraps
2def memoization(func):
3 cache = {}
4 miss = object()
5
6 @wraps(func)
7 def wrapper(*args):
8 result = cache.get(args, miss)
9 if result is miss:
10 result = func(*args)
11 cache[args] = result
12 return result
13
14 return wrapper
15
16@memoization
17def fib(n):
18 if n < 2:
19 return n
20 return fib(n - 1) + fib(n - 2)
21
22fib(10)
23# output: 55
Python supports modifying list
while traversing.
A suitable usage is to replace queue
in BFS. This is more concise, but uses more memory. However, for this scenario, you need to record the whole path. Here is a better way.
1"""bfs with list, append while iteration"""
2# L582: given n processes, each process has a unique PID (process id) and its PPID (parent process id)
3# kill represents a process you want to kill, return a list of PIDs of processes that will be killed
4def kill_process(pid: List[int], ppid: List[int], kill: int) -> List[int]:
5 d = defaultdict(list)
6 for c, p in zip(pid, ppid):
7 d[p].append(c)
8 bfs = [kill]
9 for i in bfs:
10 bfs += d[i]
11 return bfs
In this guide, we have learned more basic Python tricks, such as multiple assignment, slice assignment, and decorator. I hope some of them will be useful for you.
There are other advanced techniques not mentioned here. This guide simply offers a good starting point to travel into the Python world. You can also download an example notebook, basic.ipynb,(https://github.com/recnac-itna/python_tricks/blob/master/basic.ipynb) from Github.
This guide is one of a series of Python tricks guides:
I hope you enjoyed it. If you have any questions, you're welcome to contact me at [email protected].