Author avatar

Recnac

Python Tricks - Basic - Part 2

Recnac

  • Nov 23, 2019
  • 10 Min read
  • 13 Views
  • Nov 23, 2019
  • 10 Min read
  • 13 Views
Data
Python

Introduction

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

Multiple assignment can assign multiple variables at the same time, and it is useful in multiple initialization or swap.

1
2
3
4
5
"""multiple initialization"""
a, b = 1, 2
"""swap"""
a, b = b, a
# a:2, b:1
python

Inspiring Example

1
2
3
4
5
6
7
"""next assignment in linked list"""
# L206: reverse a linked list
def reverse(head: ListNode) -> ListNode:
    prev, cur = None, head
    while cur:
        cur.next, cur, prev = prev, cur.next, cur
    return prev
python

Further Understanding

Multiple assignment is a pack-unpack technique.

1
a, b = b, a
python

is equivalent to

1
2
3
pack = (b, a)
a = pack[0]
b = pack[1]
python

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.

Swap Array Elements

Wrong Version:

1
2
"""nums[0] has been changed before usage"""
nums[0], nums[nums[0]] = nums[nums[0]], nums[0]
python

Right Version:

1
nums[nums[0]], nums[0] = nums[0], nums[nums[0]]
python

Swap Linked List Nodes

Wrong Version:

1
2
"""cur.next has been changed before usage"""
cur.next, cur.next.next.next, cur.next.next = cur.next.next, cur.next, cur.next.next.next
python

Right Version:

1
cur.next.next.next, cur.next.next, cur.next = cur.next, cur.next.next.next, cur.next.next
python

Mirror Index

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
2
3
4
"""index start from right to left"""
arr = ["a", "b", "c", "d"]
arr[~0], arr[~1]
# output: ('d', 'c')
python

Inspiring Examples

Here are the typical usages for mirror index ~:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""swap mirror node"""
def reverse(arr: List[int]) -> None:
    for i in range(len(arr) // 2):
        arr[i], arr[~i] = arr[~i], arr[i]

"""find median in a sort list"""
def median(arr: List[float]) -> float:
    mid = len(arr) // 2
    return (arr[mid] + arr[~mid]) / 2

"""deal with mirror pairs"""
# L246: verify whether a number is strobogrammatic
# strobogrammatic number looks the same when rotated 180 degrees
def is_strobogrammatic(num: str) -> bool:
    return all(num[i] + num[~i] in '696 00 11 88' for i in range(len(num) // 2 + 1)) 
python

return None

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.

Inspiring Example

1
2
3
4
5
6
"""no return equals to return None, more concise"""
# L226: invert a binary tree
def invert_tree(root: TreeNode) -> TreeNode:
    if root:
        root.left, root.right = invertTree(root.right), invertTree(root.left)
        return root
python

Slice Assignment

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arr = [0, 1, 2, 3, 4, 5]

"""insert"""
arr[1:1] = [6, 7]
# arr: [0, 6, 7, 1, 2, 3, 4, 5]

"""delete"""
arr[1:3] = []
# arr: [0, 1, 2, 3, 4, 5]

"""replace"""
arr[1:3] = [6, 7]
# arr: [0, 6, 7, 3, 4, 5]

"""replace slice with different size"""
arr[-2:] = [2] * 3
# arr: [0, 6, 7, 3, 2, 2, 2]

"""replace the entire list"""
arr[:] = [1, 2, 3]
# arr: [1, 2, 3]
python

Inspiring Example

1
2
3
4
5
"""replace(or batch assignment) by slice assignment"""
# L280: reorder unsort array in-place such that nums[0] <= nums[1] >= nums[2] <= nums[3]...
def wiggle_sort(nums: List[int]) -> None:
    for i in range(len(nums)):
        nums[i:i+2] = sorted(nums[i:i+2], reverse=i%2)
python

Maximum/Minimum Integer

If you need something like Integer.MAX_VALUE in Java, you can use float('inf'), thanks to the dynamic typing of Python.

1
2
3
4
5
6
7
"""float('inf'), float('-inf') as the initial value of min_val and max_val"""
min_val, max_val = float('inf'), float('-inf')

for i in range(10):
    min_val = min(min_val, i)
    max_val = max(max_val, i)
# min_val:0, max_val:9
python

for else / while else

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.

1
2
3
4
5
6
7
found_obj = None
for obj in objects:
    if obj.key == search_key:
        found_obj = obj
        break
else:	# no break
    print('no object found')
python

Emulate switch

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
2
3
4
5
6
7
8
9
10
11
"""emulate switch/case with dict mapping"""
def op_dict(operator: str, x: float, y: float) -> float:
     return {
         '+': lambda: x + y,
         '-': lambda: x - y,
         '*': lambda: x * y,
         '/': lambda: x / y,
     }.get(operator, lambda: None)()

op_dict('*', 2, 3)
# output: 6
python

In another method, you can use the reflection technique (getattr) to dynamically determine which function needs to be called during runtime.

Decorator

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.

Inspiring Example

In the following example, we use decorator technique to decorate the Fibonacci function, then enhance it with memoization.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import wraps
def memoization(func):
    cache = {}
    miss = object()
 
    @wraps(func)
    def wrapper(*args):
        result = cache.get(args, miss)
        if result is miss:
            result = func(*args)
            cache[args] = result
        return result
 
    return wrapper
 
@memoization
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

fib(10)
# output: 55
python

Modify While Iteration

Python supports modifying list while traversing.

Inspiring Example

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
2
3
4
5
6
7
8
9
10
11
"""bfs with list, append while iteration"""
# L582: given n processes, each process has a unique PID (process id) and its PPID (parent process id)
# kill represents a process you want to kill, return a list of PIDs of processes that will be killed
def kill_process(pid: List[int], ppid: List[int], kill: int) -> List[int]:
    d = defaultdict(list)
    for c, p in zip(pid, ppid):
        d[p].append(c)
    bfs = [kill]
    for i in bfs:
        bfs += d[i]
    return bfs
python

Conclusion

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]

0