Effective Python Note

This article is composed of some notes from book Effective Python.

Scope Resolution

Python’s scope resolution follows the order below:

  1. current function
  2. enclosing scopes: like a outer function enclosing the current function
  3. global scope
  4. build-in scope
Sample code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
global_var = 'global value'

def outer():
enclosing_var = 'enclosing value'
enclosing_var_2 = 'enclosing value 2'

def inner():
local_var = 'local value'
print('local_var:', local_var)
print('enclosing_var:', enclosing_var)
enclosing_var_2 = 'changed enclosing value 2' # a new variable definition

inner()

print('glocal_var:', global_var)
print('enclosing_var_2:', enclosing_var_2)

outer()


>>>
local_var: local value
enclosing_var: enclosing value
glocal_var: global value
enclosing_var_2: enclosing value 2

Note that the assignment of enclosing_var_2 in function inner is actually a new variable definition, because enclosing_var_2 is not in the current scope. It’s designed to prevent local variables polluting its outer scopes. So we can see value of enclosing_var_2 doesn’t change.

Generator

It’s a function using yield instead of return.

Return an iterator when it gets called.

Every call of next with that iterator will result in code execution to the next yield and the iterator will return what’s passed to the yield.

simple_generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def gen():
for i in range(3):
yield i


it = gen()

print(next(it))
print(next(it))
print(next(it))


>>>
0
1
2

Whenever you want to use a function to compose a list, you can consider using a generator instead, which is a cleaner way.

generator_simple_use_case.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def create_results(num):
results = []
for i in range(num):
results.append(i * 10)
return results


def results_gen(num):
for i in range(num):
yield i * 10


print('Results from create_results:', create_results(3))
print('Results from results_gen:', list(results_gen(3)))


>>>
Results from create_results: [0, 10, 20]
Results from results_gen: [0, 10, 20]

A pitfall
If an iterator is used up (a StopIteration exception has been thrown), you will get no results for iterating it again. And there will be no exceptions.

generator_pitfall.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def results_gen(number):
for i in range(number):
yield i * 10


def normalize(numbers):
total = sum(numbers) # `sum` will use up iterator `numbers`
result = []

for value in numbers: # no things to iterate in `numbers`
percent = value / total * 100
result.append(percent)

return result


it = results_gen(3)
percentages = normalize(it)
print('percentages:', percentages)


>>>
percentages: []

Solutions

  1. Copy content of the iterator to a list and use the list afterward.
  2. Pass a lambda instead of numbers which returns a new generator on every call.
  3. Implement iterator protocol. That is, implement __iter__ as a generator.
generator_pitfall_sol_3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ResultsContainer:
def __init__(self, number):
self.number = number

def __iter__(self):
for i in range(self.number):
yield i * 10


results_container = ResultsContainer(3)
percentages = normalize(results_container)
print('percentages:', percentages)


>>>
percentages: [0.0, 33.33333333333333, 66.66666666666666]

Each traversal of the results_container object will cause it to return a new iterator (calling __iter__ every time), so there won’t be this issue.

Single asterisk used in function definition and function call

*numbers is called optional positional arguments. It indicates that this function can take zero or more than one positional arguments starting from that position.

It should be put after all positional arguments in a function definition.

It will pack those arguments into one tuple to use in the function.

* before nums will unpack any iterable nums so that print_numbers(0, 5, *nums) will be print_numbers(0, 5, 1, 2, 3)

single_asterisk_with_function.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def print_numbers(first_num, *numbers):
print('first_num:', first_num)
print('numbers:', numbers)


nums = [1, 2, 3]
print_numbers(0, 5, *nums)
print_numbers(6)


>>>
first_num: 0
numbers: (5, 1, 2, 3)
first_num: 6
numbers: ()

Use __call__ special method to turn class instances into functions

If we define __call__ in our class, we can turn the class instances into functions. Each call on the instance will invoke calling of its __call__ .

So, when there is a need for a function to preserve some state, you can consider using a class with __call__ method. It’s more readable than a stateful closure.

callable_class_example.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from collections import defaultdict


class MissingCounter:
"""
Provide default color count and record how many missing colors when adding increments.
"""

def __init__(self):
self.added = 0

def __call__(self, *args, **kwargs):
self.added += 1
return 0


missing_counter = MissingCounter()
init_dict = {
'green': 12,
'blue': 3
}
color_count_map = defaultdict(missing_counter, init_dict)

increments = [
('red', 5),
('blue', 17),
('orange', 9)
]
for color, amount in increments:
color_count_map[color] += amount

print('missing_counter is callable:', callable(missing_counter))
print('Missing colors added count:', missing_counter.added)


>>>
missing_counter is callable: True
Missing colors added count: 2
#

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×