I. Python Basics
1. Variables and Data Types
- Dynamic Typing: Variable types are determined at runtime, and their types can be changed.
2. List vs. Tuple
Feature | List | Tuple |
---|---|---|
Mutability | Mutable | Immutable |
Syntax | [] |
() |
As Dict Key | Cannot | Can |
3. Dictionary and Set
# Dictionary operations
d = {'a': 1}
d.get('b', 0) # Safe access, returns 0 if key 'b' does not exist
# Set operations
s = {1,2,3}
s.add(4) # Add element
II. Core Concepts
1. Deep vs. Shallow Copy
import copy
lst = [1, [2,3]]
shallow = lst.copy() # Shallow copy
deep = copy.deepcopy(lst) # Deep copy
2. Function Argument Pitfalls
# Incorrect example
def func(a, lst=[]): # Default list will persist across calls
lst.append(a)
return lst
print(func(1)) # Outputs [1]
print(func(2)) # Outputs [1, 2] instead of the expected [2]
print(func(3)) # Outputs [1, 2, 3] instead of the expected [3]
# Correct example
def func(a, lst=None):
if lst is None:
lst = [] # A new list is created for each call
lst.append(a)
return lst
print(func(1)) # Outputs [1]
print(func(2)) # Outputs [2]
print(func(3, [1, 2])) # Outputs [1, 2, 3]
The root cause of this issue is that default arguments in Python are evaluated once when the function is defined, not each time the function is called.
3. Decorator Template
@decorator1
def my_function(a, b):
return a + b
# Equivalent to:
# my_function = decorator1(my_function)
Example: Logger Decorator
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}, Arguments: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
add(3, 5)
# Output:
# Calling function: add, Arguments: (3, 5), {}
# Function add returned: 8
III. Object-Oriented Programming (OOP)
Class Structure Example
class MyClass:
class_var = 0 # Class variable
def __init__(self, x):
self.x = x # Instance variable
@staticmethod
def static_method():
pass
@classmethod
def class_method(cls):
pass
IV. Common Pitfalls
1. Loop Variable Leakage (Closure Issue)
funcs = [lambda x: x + i for i in range(3)]
# The 'i' in all lambda functions will be its final value, 2
print([f(0) for f in funcs]) # Outputs [2,2,2], not the expected [0,1,2]
Why does this happen?
Closure Characteristics:
- The lambda function creates a closure, which captures the variable
i
itself, not its value at the time of creation. - Python closures are "late binding," meaning they look up the variable's value when the function is called, not when it's defined.
- The lambda function creates a closure, which captures the variable
Loop Execution Process:
- When the list comprehension executes, the value of
i
becomes 0, then 1, then 2. - However, the lambda functions are not executed immediately; they are only created.
- All lambda functions refer to the same variable
i
.
- When the list comprehension executes, the value of
Situation at Actual Call Time:
- After the loop finishes, the final value of
i
is 2. - When we call
f(0)
, all lambda functions look up the current value ofi
, which is 2. - Therefore, each function calculates
0 + 2
, resulting in 2.
- After the loop finishes, the final value of
How to solve this issue?
Use a factory function (a form of closure).
def make_adder(i):
return lambda x: x + i
funcs = [make_adder(i) for i in range(3)]
print([f(0) for f in funcs]) # Outputs [0,1,2]
- By using a factory function
make_adder
, a new scope is created for each call. - Each lambda captures the
i
from its respective (different) scope.
Quick Recap / Cheat Sheet
- Mutable objects: list/dict/set
- Immutable objects: int/float/str/tuple
- Shallow copy only copies the outermost layer.
- Decorators are essentially nested functions (or higher-order functions).
- GIL (Global Interpreter Lock) is a mutex that ensures only one thread executes Python bytecode at any given time.