In Django, middleware compoennts process requests and responses through various methods:
process_request: Executes when a request comes in, handling authenticationprocess_view: Runs after URL routing matches a view functionprocess_exception: Triggered when an exception occursprocess_template_response: Executes during template renderingprocess_response: Runs when a response is returned
FBV vs CBV in Django
FBV (Function-Based Views) and CBV (Class-Based Views) are fundamentally similar approaches to handling requests in Django. CBV offers several advantages:
- Improved code reusability through object-oriented programming
- Support for Mixins and multiple inheritance
- Ability to handle different HTTP methods with separate functions instead of conditional statements
- Better code organization and readability
Django Request Object Creation
The Django request object is created in the WSGI handler:
class WSGIHandler(base.BaseHandler):
request = self.request_class(environ)
When a request reaches the WSGIHandler class, it executes the method and encapsulates the environ parameter into a request object.
Adding Decorators to CBV
To add decorators to Class-Based Views in Django:
from django.utils.decorators import method_decorator
@method_decorator(authenticate_user)
def post(self, request):
# Handle POST request
pass
Django ORM Methods
Common Django ORM methods include:
all(): Returns all objects in the queryset
filter(**kwargs): Returns objects matching the given criteria
get(**kwargs): Returns a single object matching the criteria; raises MultipleObjectsReturned or DoesNotExist if multiple or no objects found
exclude(**kwargs): Returns objects that don't match the given criteria
order_by(*field): Sorts the queryset by specified fields
count(): Returns the number of objects in the queryset
first(): Returns the first object in the queryset
exists(): Returns True if the queryset contains any objects
select_related vs prefetch_related
When dealing with foreign key relationships:
select_relatedperforms a SQL join to fetch related objects in a single queryprefetch_relatedexecutes separate queries for each table and then combines the results in Python
Use select_related for foreign key and one-to-one relationships, and prefetch_related for many-to-many and reverse foreign key relationships.
Django CSRF Implementation
Django's CSRF protection works as follows:
- When responding to a client's first request, Django generates a random token, stores it in the session, and sends it to the client in a cookie
- For subsequent requests (like form submissions), the client includes this token in the request data or headers
- The server validates that the token from the request matches the one stored in the session
Configuring Redis Cache in Django
Yes, Django can use Redis for caching. Here's how to configure it:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
# "PASSWORD": "your_password",
}
}
}
Purpose of Name in Django URL Routing
The name parameter in Django URL patterns allows you to reference URLs by name rather than hardcoding paths. This provides flexibility to change URL patterns without updating templates or views that reference them.
Django REST Framework Components
Key components in Django REST Framework include:
- Authentication
- Permissions (Authorization)
- Throttling (Rate limiting)
- Versioning
- Parsers
- Serializers
- Pagination
- Routers
- Views
- Renderers
Django REST Framework Authentication Flow
The authentication process in DRF follows these steps:
- When a user attempts to log in, the login class's as_view() method is called, entering the APIView class's dispatch method
- The initialize_request method executes, encapsulating the request and authentication objects
- The initial method calls perform_authentication, wich runs the user method
- The user method then executes _authenticate(), which handles the actual authentication
Handling Large Files with Limited Memory
Question: How would you process a 10GB file with only 4GB of RAM, modifying only the get_lines function?
from mmap import mmap
def get_lines(file_path):
with open(file_path, "r+") as file:
memory_map = mmap(file.fileno(), 0)
start_position = 0
for index, character in enumerate(memory_map):
if character == b"\n":
yield memory_map[start_position:index+1].decode()
start_position = index+1
if __name__ == "__main__":
for line in get_lines("large_file.txt"):
print(line)
Key considerations: The file is too large to fit in memory, so we need to process it in chunks. We must track our position between reads and balance chunk size to avoid excessive I/O operations.
Directory Traversal Function
Complete the function to print all file paths in a directory and its subdirectories:
import os
def print_directory_contents(path):
"""
This function takes a directory path as input
and prints the paths of all files in the directory
and its subdirectories.
"""
for child in os.listdir(path):
child_path = os.path.join(path, child)
if os.path.isdir(child_path):
print_directory_contents(child_path)
else:
print(child_path)
Day of Year Calculation
Write a function to determine the day of the year for a given date:
import datetime
def day_of_year():
year = int(input("Enter year: "))
month = int(input("Enter month: "))
day = int(input("Enter day: "))
target_date = datetime.date(year=year, month=month, day=day)
year_start = datetime.date(year=year, month=1, day=1)
return (target_date - year_start).days + 1
Dictionary Sorting by Value
Sort the following dictionary by its values: d = {'a': 24, 'g': 52, 'i': 12, 'k': 33}
sorted(d.items(), key=lambda item: item[1])
String to Dictionary Conversion
Convert the string "k:1|k1:2|k2:3|k3:4" to a dictionary:
input_string = "k:1|k1:2|k2:3|k3:4"
def string_to_dict(s):
result = {}
for item in s.split('|'):
key, value = item.split(':')
result[key] = int(value)
return result
# Using dictionary comprehension
result_dict = {k:int(v) for item in input_string.split("|") for k, v in (item.split(":"), )}
Python Built-in Data Structures
Python provides several built-in data structures:
- Integer (int)
- Floating-point (float)
- Complex numbers (complex)
- String (str)
- List (list)
- Tuple (tuple)
- Dictionary (dict)
- Set (set)
Note: In Python 3, there's no separate long type; int has unlimited precision.
Singleton Pattern Implementations
Two ways to implement the singleton pattern in Python:
Using a decorator:
def singleton(cls):
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class DatabaseConnection:
pass
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True
Using a base class:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
class ConfigurationManager(Singleton):
pass
config1 = ConfigurationManager()
config2 = ConfigurationManager()
print(config1 is config2) # True
Sum of Numbers 1-100 in One Line
sum(range(1, 101))
is vs == in Python
is compares object identity (whether two variables point to the same object in memory), while == compares object equality (whether two objects have the same value). By default, == calls the __eq__ method of the object.
Finding Most Frequent Words in a Text
import re
from collections import Counter
def find_most_frequent_words(file_path, count=10):
with open(file_path) as file:
# Normalize text: replace non-alphanumeric characters with spaces
normalized_text = re.sub(r"\W+", " ", file.read())
words = normalized_text.split()
# Count word frequencies and return most common
word_counts = Counter(words)
return [word for word, _ in word_counts.most_common(count)]
Python Memory Management
Python uses three main mechanisms for memory management:
- Reference counting: Each object keeps track of how many references point to it. When the count reaches zero, the object is deallocated.
- Garbage collection: Handles circular references that reference counting can't resolve.
- Memory pools: Python manages memory in pools to reduce overhead from frequent allocation and deallocation.
Optimization techniques include manual garbage collection, adjusting garbage collection thresholds, and avoiding circular references.
Lambda Functions in Python
Lambda functions are anonymous functions defined with the lambda keyword. They can take any number of arguments but can only have one expression. Benefits include:
- Concise syntax for simple functions
- Useful for functional programming constructs like map, filter, and reduce
- Handy as callback functions
Example:
multiply = lambda x, y: x * y
print(multiply(5, 3)) # Output: 15
Understanding Design Patterns
Design patterns are reusable solutions to common programming problems. They represent best practices evolved from experienced developers' collective wisdom. Common patterns include:
- Factory Pattern
- Singleton Pattern
- Observer Pattern
- Strategy Pattern
- Decorator Pattern
Singleton Pattern Use Cases
The singleton pattern is useful in scenarios involving:
- Resource sharing (e.g., database connections, thread pools)
- Configuration management
- Logging systems
- Cache management
- Device drivers (where only one instance should control a resource)
Performance Timer Decorator
import time
from functools import wraps
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Function {func.__name__} executed in {end_time - start_time:.6f} seconds")
return result
return wrapper
@measure_time
def process_data():
# Simulate data processing
time.sleep(1)
return "Processing complete"
print(process_data())
Closures in Python
A closure occurs when a nested function references a value from its enclosing scope. The closure function remembers the values from the enclosing lexical scope even when the program flow is no longer in that scope.
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_three = make_multiplier(3)
print(times_three(10)) # Output: 30
Generators vs Iterators
Iterators are objects that implement the iterator protocol (__iter__ and __next__ methods). They represent a stream of data and return one element at a time.
Generators are a special type of iterator created using functions with the yield keyword. They automatically implement the iterator protocol and maintain their state between calls.
Key differences:
- Generators are simpler to write (using yield instead of implementing __iter__ and __next__)
- Generators are more memory-efficient as they generate values on demand
- Generators automatically raise StopIteration when exhausted
Grouping Numbers in Tuples
Create groups of three numbers from 1 to N:
N = 100
result = [[num for num in range(1, N+1)][i:i+3] for i in range(0, N, 3)]
print(result)
Yield Keyword in Python
The yield keyword turns a function into a generator. When a function contains yield, it becomes a generator function that returns a generator iterator. The generator maintains its state between calls, allowing it to produce a sequence of values over time rather than computing them all at once. This is memory-efficient for large sequences.
def fibonacci(n):
a, b = 0, 1
count = 0
while count < n:
yield a
a, b = b, a + b
count += 1
for num in fibonacci(10):
print(num)
Understanding Processes, Threads, and Coroutines
Processes are independent execution units with their own memory space. They are the unit of resource allocation and provide true parallelism but have higher overhead for creation and communication.
Threads are lightweight execution units within a process. They share the same memory space, making communication easier but requiring synchronization. Threads provide concurrency within a single process.
Coroutines are even lighter-weight units of execution that are managed cooperatively by the program rather than the operating system. They are ideal for I/O-bound tasks and can handle thousands of concurrent operations with minimal overhead.
Python Async Use Cases
Asynchronous programming in Python is particularly useful for:
- I/O-bound operations (network requests, database queries, file operations)
- Applications that need to handle many concurrent connections
- Real-time applications (chat servers, live updates)
- Web scraping and crawling
- Applications where responsiveness is critical
Thread Competition in Python
Thread competition occurs when multiple threads attempt to access shared resources simultaneously, potentially leading to race conditions. Since threads share the same memory space, concurrent access to shared data can result in inconsistent states if not properly synchronized.
Lock Types in Python
Python provides several synchronization primitives:
- Lock: A basic mutual exclusion lock that can be acquired by only one thread at a time
- RLock (Reentrant Lock): Allows a thread to acquire the same lock multiple times
- Semaphore: Allows a fixed number of threads to access a resource
- Event: Allows threads to wait for a specific event to occur
- Condition: Combines a lock with a wait/notify mechanism
Understanding Deadlocks
A deadlock occurs when two or more threads are blocked forever, each waiting for a resource held by the other. The classic "dining philosophers" problem illustrates this scenario. Deadlocks happen when four conditions are met simultaneously:
- Mutual exclusion: Resources cannot be shared
- Hold and wait: Threads hold resources while waiting for others
- No preemption: Resources cannot be forcibly taken
- Circular wait: A circular chain of threads exists where each holds a resource needed by the next