-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path04_scope_closures.py
More file actions
144 lines (108 loc) · 6.27 KB
/
Copy path04_scope_closures.py
File metadata and controls
144 lines (108 loc) · 6.27 KB
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""
04 — Scope and Closures
THEORY
------
What is it?
Scope determines where a name is visible. Python follows the LEGB rule:
Local → Enclosing → Global → Built-in. A closure is an inner function that
remembers variables from its enclosing scope even after the outer function returns.
Why it matters
Scope bugs (accidental shadowing, unintended mutation) are common in real code.
Closures power factories, decorators, and callbacks. Understanding global,
nonlocal, and LEGB is essential for interviews and debugging.
Key syntax/rules
- Local: names assigned inside a function belong to that function
- global x declares intent to read/write module-level x inside a function
- nonlocal x modifies a variable in the nearest enclosing (non-global) scope
- Inner functions can read enclosing variables; nonlocal needed to reassign them
- Closure: inner function + captured environment returned from outer function
- LEGB lookup order: Local first, then Enclosing, Global, Built-in
When to use
- global: sparingly, for module-level counters or config flags
- nonlocal: when inner function must update outer function's state
- Closures: factory functions (make_multiplier), private state (make_counter)
- Prefer passing arguments over globals for testability
Common mistakes
- Forgetting global/nonlocal when reassigning outer variables — creates a new local instead
- Using global when nonlocal is needed (inside nested functions)
- Closure late-binding in loops — all closures capture the same variable reference
- Shadowing built-ins (list = [...]) hides the built-in list type
PRACTICE
--------
Run: python3 core-python/modules/03-functions-and-modules/04_scope_closures.py
"""
x = "global" # global variable x defined at module level
def demo_scope(): # function that shadows global x with a local
x = "local" # local x shadows the global x inside this function
print(f" Inside function: {x}") # print the local x
counter = 0 # module-level counter starting at 0
def increment(): # function that modifies global counter
global counter # declare intent to modify the global counter
counter += 1 # increment the global counter by 1
def outer(): # outer function demonstrating nonlocal
message = "Hello" # variable in outer function's scope
def inner(): # inner function that modifies outer's message
nonlocal message # declare intent to modify outer's message, not create local
message = "Hello, World!" # update the enclosing scope's message
inner() # call inner to modify message
return message # return the updated message from outer scope
def make_multiplier(factor): # factory function that returns a closure
def multiply(n): # inner function captures factor from enclosing scope
return n * factor # use captured factor even after make_multiplier returns
return multiply # return the inner function (closure)
def make_counter(start=0): # practical closure: counter factory
count = start # private counter variable in enclosing scope
def increment(step=1): # inner increment function with optional step
nonlocal count # modify the enclosed count variable
count += step # add step to private counter
return count # return updated count
return increment # return the increment function (closure)
def main() -> None: # entry point that runs all scope and closure practice sections
print("=" * 50) # print section divider
print("PRACTICE 1 — Local vs global") # section title
print("=" * 50) # close section header
demo_scope() # call function to show local x
print(f"Outside function: {x}") # outside function, global x is unchanged
print("=" * 50) # print section divider
print("PRACTICE 2 — Modify global variable") # section title
print("=" * 50) # close section header
increment() # call increment first time
increment() # call increment second time
print(f"Counter: {counter}") # show updated global counter
print("=" * 50) # print section divider
print("PRACTICE 3 — nonlocal in nested functions") # section title
print("=" * 50) # close section header
print(f"Outer after inner: {outer()}") # show message after inner function modified it
print("=" * 50) # print section divider
print("PRACTICE 4 — Closures") # section title
print("=" * 50) # close section header
times3 = make_multiplier(3) # create closure that remembers factor 3
times5 = make_multiplier(5) # create closure that remembers factor 5
print(f"times3(4) = {times3(4)}, times5(4) = {times5(4)}") # each closure uses its own factor
print("=" * 50) # print section divider
print("PRACTICE 5 — Counter factory") # section title
print("=" * 50) # close section header
counter_fn = make_counter(10) # create a counter starting at 10
print(f"Counter: {counter_fn()}, {counter_fn()}, {counter_fn(5)}") # increment and show values
print("=" * 50) # print section divider
print("PRACTICE 6 — LEGB rule") # section title
print("=" * 50) # close section header
print(f"Built-in len: {len([1, 2, 3])}") # len is a built-in from Built-in scope
print("=" * 50) # print section divider
print("PRACTICE 7 — Practical: rate limiter factory") # section title
print("=" * 50) # close section header
def make_rate_limiter(max_calls: int): # closure-based rate limiter
calls = 0 # private call counter in enclosing scope
def allow() -> bool: # check if another call is allowed
nonlocal calls # modify enclosed calls counter
if calls < max_calls: # still under the limit
calls += 1 # increment call count
return True # allow this call
return False # reject — limit reached
return allow # return the allow checker function
limiter = make_rate_limiter(2) # allow at most 2 calls
print(f"Call 1 allowed: {limiter()}") # first call — True
print(f"Call 2 allowed: {limiter()}") # second call — True
print(f"Call 3 allowed: {limiter()}") # third call — False (limit hit)
if __name__ == "__main__": # run main() only when executed directly, not imported
main() # start all scope and closure practice sections