Collection of Python Interview Questions
Q: Static vs Class methods?
Static methods and class methods are both methods that are associated with a class rather than an instance of the class.
Static Method:
- A static method is a method that belongs to the class rather than an instance of the class.
- It is defined using the
@staticmethod
decorator. - It does not have access to the instance or its attributes.
- It is called on the class, not on an instance of the class.
pythonclass MyClass: @staticmethod def my_static_method(): # Code for static method
Class Method:
- A class method is a method that takes the class itself as its first argument.
- It is defined using the
@classmethod
decorator. - It has access to the class and its attributes, but not to the instance.
- It is called on the class, not on an instance of the class.
pythonclass MyClass: @classmethod def my_class_method(cls): # Code for class method
In summary, static methods are independent of class instances and class methods have access to the class itself.
Q: What is MRO?
MRO stands for Method Resolution Order in Python. It defines the order in which classes are searched when looking for a method in the inheritance hierarchy. The MRO plays a crucial role in multiple inheritance scenarios.
In Python, the C3 linearization algorithm is used to determine the MRO. The MRO is calculated based on the following principles:
Depth-First Search:
- The MRO starts with the derived class and then follows the chain of base classes in a depth-first manner.
Left-to-Right:
- In case of multiple inheritance (a class inheriting from more than one class), the MRO follows a left-to-right order as specified in the class definition.
For example:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
# MRO for class D: D -> B -> C -> A
print(D.mro())
In the example above, the MRO for class D
is [D, B, C, A]
. This means that when looking for a method in class D
, it will first check in D
, then in B
, then in C
, and finally in A
.
Understanding the MRO is essential for resolving method and attribute lookup in complex class hierarchies.
Q: Python module vs package?
In Python, both modules and packages are organizational units for code, but they serve different purposes.
Module:
- A module is a single Python file that contains code, functions, and variables.
- It is a way to organize related code into a file to make it reusable and maintainable.
- You can create a module by saving a Python script with a
.py
extension.
Example of a module (
my_module.py
):python# my_module.py def my_function(): print("Hello from my function in my_module")
You can then use this module in another script:
python# main_script.py import my_module my_module.my_function()
Package:
- A package is a way of organizing related modules into a directory hierarchy.
- It contains a special file called
__init__.py
to indicate that the directory should be treated as a package. - Packages help in organizing larger codebases and avoid naming conflicts.
Example of a package:
my_package/ ├── __init__.py ├── module1.py └── module2.py
Contents of
module1.py
:python# module1.py def function1(): print("Function 1 from module1")
Contents of
module2.py
:python# module2.py def function2(): print("Function 2 from module2")
You can then use these modules within the package:
python# main_script.py from my_package import module1, module2 module1.function1() module2.function2()
In summary, a module is a single file containing Python code, while a package is a collection of related modules organized in a directory hierarchy. The __init__.py
file distinguishes a directory as a package.
Q: What is the purpose of a single underscore variable in Python?
In Python, a single underscore (_
) has a specific purpose and meaning, but it can be used in different contexts. Here are some common use cases:
Unused Variable:
- A single underscore is often used as a variable name when the variable is intentionally not going to be used. This convention is a way to indicate to other programmers (and to tools like linters) that the variable is intentionally ignored.
python# Unused variable _ = my_function_that_returns_a_value()
Last Result in the Interpreter:
- In an interactive Python session (like the Python REPL or IPython), the single underscore
_
is automatically assigned to the result of the last expression.
python>>> 2 + 3 5 >>> _ # Represents the result of the last expression (5) 5
- In an interactive Python session (like the Python REPL or IPython), the single underscore
Internationalization (gettext):
- In the context of internationalization and localization (using the
gettext
module), the single underscore is often used as a shorthand for marking strings for translation.
pythonfrom gettext import gettext as _ message = _("This is a translatable string")
However, in practice, double underscores are more commonly used for this purpose.
- In the context of internationalization and localization (using the
It's important to note that using a single underscore as a variable name is a convention and not a strict rule enforced by the Python interpreter. Programmers use it to convey specific meanings in their code.
Q: __init__
vs __new__
methods in Python?
In Python, both __init__
and __new__
are special methods, but they serve different purposes in the object creation process.
__new__
Method:- The
__new__
method is responsible for creating a new instance of a class. - It is a static method (a method bound to the class and not the instance) and is called before the
__init__
method. - The primary purpose of
__new__
is to create and return a new instance of the class. It takes the class as its first argument, followed by any additional arguments that were passed to the class constructor. - If
__new__
is not defined in a class, it defaults to theobject.__new__
method, which creates a new instance of the class.
Example:
pythonclass MyClass: def __new__(cls, *args, **kwargs): # Custom logic for creating a new instance instance = super().__new__(cls) # Additional initialization can be done here if needed return instance
- The
__init__
Method:- The
__init__
method is responsible for initializing the attributes of an instance after it has been created by__new__
. - It is an instance method and takes the newly created instance (
self
) along with any additional arguments that were passed to the class constructor. - The primary purpose of
__init__
is to set up the initial state of the object, assigning values to attributes or performing other initialization tasks.
Example:
pythonclass MyClass: def __init__(self, arg1, arg2): # Initialization logic self.arg1 = arg1 self.arg2 = arg2
- The
In summary, __new__
is responsible for creating a new instance of the class, and __init__
is responsible for initializing the attributes of that instance. In most cases, you'll only need to define the __init__
method unless you have specific requirements for customizing the object creation process.
Q: Why are full values shared between two objects?
In Python, when two objects share the same values, it is usually because they are referencing the same object in memory, not because the values themselves are shared. This behavior is a result of how Python handles certain types of objects, especially immutable objects.
Let's distinguish between mutable and immutable objects:
Mutable Objects:
- Objects whose state can be changed after creation are mutable.
- Examples include lists, dictionaries, and custom objects.
pythonlist1 = [1, 2, 3] list2 = list1 # Both list1 and list2 reference the same list object
In this case, if you modify
list1
,list2
will reflect the changes because they point to the same list object.Immutable Objects:
- Objects whose state cannot be changed after creation are immutable.
- Examples include strings, tuples, and numeric types.
pythonstring1 = "hello" string2 = string1 # Both string1 and string2 reference the same string object
Although strings are immutable, the reference (
string2
) is shared, and both variables point to the same string object.
This behavior is more evident with immutable objects because modifying their values actually creates new objects. It's important to understand that this sharing of values is specific to certain types of objects and does not apply universally across all types in Python.
If you want to create a new object with the same values but independent of the original object, you can use techniques like slicing (for sequences) or the copy
module.
# Creating a new list with the same values
new_list = list(original_list)
In summary, the sharing of values between two objects in Python typically occurs when they reference the same mutable or immutable object in memory. Understanding the mutability or immutability of objects helps in grasping this behavior.
Q: Explain Python's garbage collection mechanism
Python's garbage collection manages memory by using reference counting and a cyclic garbage collector for handling circular references. The gc
module provides manual control over garbage collection, with gc.collect()
triggering the process. CPython, the main Python implementation, employs a generational garbage collector. While automatic garbage collection is efficient, programmers should be aware of potential memory issues.
Q: What is the global interpreter lock and why is it an issue with an example?
The Global Interpreter Lock (GIL) is a mutex (or lock) that protects access to Python objects, preventing multiple native threads from executing Python bytecodes at once. In other words, it allows only one thread to execute Python bytecode in the interpreter at any given time, even on multi-core systems.
Why the GIL is an Issue:
Concurrency Limitation:
- The GIL limits the execution of multiple threads simultaneously, impacting the ability to take full advantage of multi-core processors.
- It becomes a bottleneck for CPU-bound and multithreaded applications as it restricts parallel execution.
Performance Implications:
- Although Python supports threading, due to the GIL, threads are not as effective for parallelizing CPU-bound tasks.
- The GIL doesn't hinder performance for I/O-bound tasks (tasks waiting for external resources) as much because the lock is released during I/O operations.
Example:
Consider a simple CPU-bound task that calculates the square of each element in a list using multiple threads:
import threading
def square_numbers(numbers):
global result
for number in numbers:
result.append(number * number)
result = []
numbers = [1, 2, 3, 4, 5]
# Create two threads to square the numbers concurrently
thread1 = threading.Thread(target=square_numbers, args=(numbers,))
thread2 = threading.Thread(target=square_numbers, args=(numbers,))
# Start the threads
thread1.start()
thread2.start()
# Wait for both threads to finish
thread1.join()
thread2.join()
print(result)
Due to the GIL, the threads will not execute concurrently when performing the CPU-bound task. As a result, the performance improvement typically associated with multithreading in other languages may not be realized in this Python example.
It's important to note that the GIL is specific to the CPython interpreter, and other Python implementations like Jython or IronPython do not have a GIL. Additionally, for I/O-bound tasks, asynchronous programming using asyncio
and async/await
can be a more effective approach than traditional multithreading.
Q: What are iterators?
In Python, an iterator is an object that implements the iterator protocol, which consists of the methods __iter__()
and __next__()
(or __iter__()
and __getitem__()
for older-style iterators). Iterators are used to represent a stream of data and facilitate iteration over elements in a sequence, container, or collection. They allow you to loop over a set of values, one at a time, without having to know the underlying details of the data structure.
Here are the key components of iterators:
__iter__()
Method:- The
__iter__()
method returns the iterator object itself. - It is called when you use the
iter()
function on an object.
- The
__next__()
Method:- The
__next__()
method returns the next element from the iterator. - It is called when you use the
next()
function on an iterator.
- The
StopIteration Exception:
- When there are no more elements to return, the
__next__()
method should raise theStopIteration
exception to signal the end of the iteration.
- When there are no more elements to return, the
Iterable:
- An iterable is an object that can be iterated over, and it typically implements the
__iter__()
method. - Iterables may or may not be iterators themselves.
- An iterable is an object that can be iterated over, and it typically implements the
Example of a simple iterator:
class MyIterator:
def __init__(self, data):
self.data = data
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.data):
result = self.data[self.index]
self.index += 1
return result
else:
raise StopIteration
# Using the iterator
my_list = [1, 2, 3, 4, 5]
my_iterator = MyIterator(my_list)
for item in my_iterator:
print(item)
In this example, MyIterator
is an iterator for a list. The __iter__
method returns the iterator object (self
), and the __next__
method returns the next element from the list until there are no more elements to return.
In practice, many Python objects, such as lists, tuples, dictionaries, and strings, are iterable and can be used directly in for
loops. Iterators provide a way to customize the iteration behavior for your own objects.
Q: What do you understand about generators in Python?
Generators in Python are a way to create iterators using a special kind of function. They allow you to iterate over a potentially large sequence of data without creating the entire sequence in memory, which is particularly useful for working with large datasets or infinite sequences.
Key characteristics of generators:
Function with
yield
:- Generators are created using functions that contain the
yield
keyword. - When a generator function is called, it returns an iterator but does not start executing immediately. The function is paused at the
yield
statement.
- Generators are created using functions that contain the
Lazy Evaluation:
- Values are generated one at a time and are only computed when requested.
- This is in contrast to normal functions that compute and return the entire result at once.
Stateful Execution:
- The generator function retains its local state between successive calls.
- When the generator is resumed after a call to
yield
, it continues execution from where it was paused.
Example:
pythondef simple_generator(): yield 1 yield 2 yield 3 # Using the generator my_generator = simple_generator() print(next(my_generator)) # Output: 1 print(next(my_generator)) # Output: 2 print(next(my_generator)) # Output: 3
Infinite Sequences:
- Generators can represent infinite sequences, such as counting numbers or a stream of data, without consuming infinite memory.
pythondef infinite_counter(): count = 0 while True: yield count count += 1 # Using the infinite counter counter = infinite_counter() print(next(counter)) # Output: 0 print(next(counter)) # Output: 1 # ...
Generators are particularly useful when dealing with large datasets, streaming data, or when the entire sequence is not needed at once. They provide a memory-efficient and elegant way to work with sequences in Python. Additionally, the yield
keyword allows generators to maintain their state between calls, making them suitable for scenarios where maintaining state is important.
Q: What is a monkey patching in Python?
Monkey patching in Python refers to the dynamic modification of a module or class during runtime. It involves altering or extending the behavior of a module or class, typically for the purpose of fixing bugs, adding new features, or modifying existing functionality. Monkey patching is powerful but should be used judiciously, as it can lead to code that is harder to understand and maintain.
Key points about monkey patching:
Dynamic Modification:
- Monkey patching involves making changes to code at runtime, often by directly modifying the attributes or methods of classes or objects.
Common Use Cases:
- Fixing Bugs: Monkey patching can be used to fix bugs in third-party libraries or modules without modifying their source code.
- Adding Functionality: It allows developers to add new functionality to existing classes or modules without subclassing.
- Testing: Monkey patching is sometimes used in testing to replace or mock certain behaviors temporarily.
Example:
python# Original class class MyClass: def original_method(self): return "Original method" # Monkey patching: Adding a new method def new_method(self): return "Patched method" MyClass.patched_method = new_method # Using the modified class obj = MyClass() print(obj.original_method()) # Output: Original method print(obj.patched_method()) # Output: Patched method
Considerations:
- Monkey patching can lead to code that is harder to understand and maintain, as it introduces changes outside the regular development process.
- It can cause compatibility issues with future versions of the patched code or with other modules that interact with it.
- It's important to document and communicate the use of monkey patching in a codebase to ensure that other developers are aware of the modifications.
While monkey patching can be a powerful tool, it's generally recommended to use it with caution and explore other alternatives such as subclassing, decorators, or more structured ways of extending or modifying functionality, especially when working on larger projects or collaborating with other developers.
Q: What’s the difference between deep and shallow copy in Python?
In Python, the concepts of shallow copy and deep copy refer to creating copies of objects, particularly complex objects like lists or dictionaries. The distinction lies in how nested objects within the original are handled.
Shallow Copy:
- A shallow copy creates a new object but does not create copies of nested objects. Instead, it copies references to the nested objects.
- Changes made to the nested objects will be reflected in both the original and the shallow copy.
- In Python, you can use the
copy
module'scopy()
function or the object's owncopy()
method to create a shallow copy.
pythonimport copy original_list = [1, [2, 3], 4] # Using copy() method for shallow copy shallow_copy_list = original_list.copy() # or using copy() function shallow_copy_list = copy.copy(original_list)
Deep Copy:
- A deep copy creates a new object and recursively creates copies of all nested objects, ensuring that changes in nested objects do not affect the original or other copies.
- In Python, you can use the
copy
module'sdeepcopy()
function to create a deep copy.
pythonimport copy original_list = [1, [2, 3], 4] # Using deepcopy() function for deep copy deep_copy_list = copy.deepcopy(original_list)
Example:
import copy
original_list = [1, [2, 3], 4]
# Shallow copy
shallow_copy_list = copy.copy(original_list)
# Deep copy
deep_copy_list = copy.deepcopy(original_list)
# Modify the nested list
original_list[1][0] = 99
# Changes are reflected in shallow copy but not in deep copy
print(original_list) # Output: [1, [99, 3], 4]
print(shallow_copy_list) # Output: [1, [99, 3], 4]
print(deep_copy_list) # Output: [1, [2, 3], 4]
In the example, modifying the nested list in the original list affects the shallow copy, but the deep copy remains unchanged.
In summary, the key difference is how nested objects are treated. Shallow copy creates new objects but copies references to nested objects, while deep copy creates new objects and recursively copies all nested objects, ensuring complete independence between the original and the copy.
Q: How will you define polymorphism in Python?
Polymorphism in Python refers to the ability of different objects to be treated as instances of a common type. It allows objects of different classes to be used interchangeably based on their common interface, methods, or attributes. There are two main types of polymorphism in Python:
Compile-Time Polymorphism (Static Binding):
- Also known as method overloading.
- It involves defining multiple methods in a class with the same name but different parameter types or a different number of parameters.
- The correct method is selected during compilation based on the method signature.
pythonclass MathOperations: def add(self, x, y): return x + y def add(self, x, y, z): return x + y + z math_obj = MathOperations() result1 = math_obj.add(2, 3) # Calls the first add method result2 = math_obj.add(2, 3, 4) # Calls the second add method
Run-Time Polymorphism (Dynamic Binding):
- Also known as method overriding.
- It involves defining a method in the subclass that already exists in the superclass.
- The correct method is selected during runtime based on the actual type of the object.
pythonclass Animal: def sound(self): pass class Dog(Animal): def sound(self): return "Woof!" class Cat(Animal): def sound(self): return "Meow!" # Polymorphic behavior def make_sound(animal): return animal.sound() dog = Dog() cat = Cat() print(make_sound(dog)) # Output: Woof! print(make_sound(cat)) # Output: Meow!
In the example, the make_sound
function takes any object of type Animal
and calls its sound
method. The correct sound
method is determined at runtime based on the actual type of the object passed.
Polymorphism enhances code flexibility and readability by allowing objects of different types to be treated uniformly through a common interface. It is a fundamental concept in object-oriented programming that supports code reuse and extensibility.
Q: What is a closure in Python?
In Python, a closure is a function object that has access to variables in its lexical scope, even when the function is called outside that scope. This means that a closure can "close over" variables from its outer function, retaining access to those variables even after the outer function has finished execution.
Key characteristics of closures:
Nested Function:
- A closure involves a nested function (a function defined within another function).
Access to Outer Function's Variables:
- The inner function has access to the variables of its outer (enclosing) function, even after the outer function has completed execution.
Immutable Closure:
- Closures capture variables by reference, not by value. This means if the enclosed variables are mutable, changes to them will be reflected in the closure. However, reassignment of the variable within the outer function does not affect the closure.
Example of a Closure:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure_instance = outer_function(10)
result = closure_instance(5)
print(result) # Output: 15
In this example, inner_function
is a closure because it has access to the x
variable from its outer function, outer_function
, even though outer_function
has already finished execution. When closure_instance
is called with 5
, it adds 5
to the captured value of x
(10
), resulting in 15
.
Closures are often used to create functions with behavior dependent on some initial setup or configuration. They provide a way to achieve data encapsulation and help manage the scope of variables in a clean and modular way.
Q: Explain multithreading in Python
Multithreading in Python involves the concurrent execution of multiple threads within a single process. Threads are lightweight sub-processes that share the same memory space, allowing for parallel execution of tasks. Python provides a built-in threading
module for working with threads.
However, it's important to note that due to the Global Interpreter Lock (GIL) in the standard CPython implementation, true parallel execution of threads is limited. The GIL allows only one thread to execute Python bytecode at a time, impacting the parallelism benefits of multiple threads, especially in CPU-bound tasks. For I/O-bound tasks, multithreading can still provide advantages.
Here's an overview of multithreading in Python using the threading
module:
Creating Threads:
- Threads are created by instantiating the
Thread
class from thethreading
module. - You can define a target function that the thread will execute.
pythonimport threading def my_function(): # Code to be executed by the thread my_thread = threading.Thread(target=my_function)
- Threads are created by instantiating the
Starting Threads:
- Threads are started by calling the
start()
method on the thread object. - The
start()
method initiates the execution of the target function in a separate thread.
pythonmy_thread.start()
- Threads are started by calling the
Joining Threads:
- The
join()
method is used to wait for the thread to complete its execution before proceeding further in the main thread.
pythonmy_thread.join()
- The
Thread Safety:
- Thread safety is crucial when working with shared resources. It involves using locks or other synchronization mechanisms to prevent race conditions and ensure data consistency.
Thread Pools:
- Python provides the
concurrent.futures
module for working with thread pools, allowing you to submit tasks to a pool of threads.
pythonfrom concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor() as executor: results = executor.map(my_function, iterable_of_arguments)
- Python provides the
Multithreading in Python is particularly beneficial for I/O-bound tasks where threads can execute independently, waiting for external resources, such as network or file I/O. For CPU-bound tasks, alternative approaches like multiprocessing or asynchronous programming may be more suitable due to the limitations imposed by the GIL.
Q: asyncio vs multithreading?
Concurrency Model:
Asyncio
is a single-threaded, single-process design. It uses coroutines and an event loop to manage tasks.Multithreading
involves multiple threads of execution within a single process. Each thread runs independently and can execute different parts of your program concurrently.
Use Cases:
Asyncio
is ideal for I/O-bound tasks, especially when you're dealing with many connections and each connection doesn't need to do much work.Multithreading
is beneficial for CPU-bound tasks and can also be used for I/O-bound tasks if the number of connections is limited.
Performance:
Asyncio
can handle many open connections concurrently, making it suitable for building high-performance network servers.Multithreading
can become less efficient with a large number of threads due to the overhead of context switching.
Ease of Use:
Asyncio
can be more complex to understand and implement due to its asynchronous nature.Multithreading
can be easier to understand and implement, but it can be prone to synchronization issues.
Here's a simple example to illustrate the difference:
# Multithreading Example
import threading
import time
def print_nums():
for i in range(5):
time.sleep(1)
print(i)
def print_hello():
for _ in range(5):
time.sleep(1)
print("Hello")
t1 = threading.Thread(target=print_nums)
t2 = threading.Thread(target=print_hello)
t1.start()
t2.start()
# Asyncio Example
import asyncio
async def print_nums():
for i in range(5):
await asyncio.sleep(1)
print(i)
async def print_hello():
for _ in range(5):
await asyncio.sleep(1)
print("Hello")
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(print_nums(), print_hello()))
loop.close()
In both examples, print_nums
and print_hello
run concurrently. However, the multithreading example uses threads, while the asyncio example uses coroutines¹².
Q: Explain with statement in python
The with
statement in Python is used to simplify resource management by providing a convenient way to acquire and release resources, such as files, sockets, or locks. It ensures that certain operations are properly set up and cleaned up, even if an exception occurs during the execution of the block of code.
The general syntax of the with
statement is as follows:
with expression [as variable]:
# Code block
Here's how the with
statement works:
Acquiring Resources:
- The expression following the
with
keyword is expected to return a context manager object. A context manager is an object that defines the methods__enter__()
and__exit__()
.
pythonwith open("example.txt", "r") as file: # Code to read from the file
In this example, the
open()
function returns a file object, which acts as a context manager. The file is automatically opened when entering thewith
block.- The expression following the
Code Execution:
- The indented code block following the
with
statement is executed. This block represents the body of thewith
statement and is where you work with the acquired resources.
pythonwith open("example.txt", "r") as file: content = file.read() # Code to process the file content
- The indented code block following the
Automatic Cleanup:
- After the code block is executed, the
__exit__()
method of the context manager is called. This method is responsible for releasing or cleaning up any resources acquired in the__enter__()
method.
pythonwith open("example.txt", "r") as file: content = file.read() # Code to process the file content # The file is automatically closed at this point, regardless of whether an exception occurred.
- After the code block is executed, the
Handling Exceptions:
- The
with
statement also handles exceptions that may occur within the code block. If an exception occurs, the__exit__()
method is still called, allowing for proper cleanup.
pythontry: with open("example.txt", "r") as file: content = file.read() # Code that may raise an exception except SomeException as e: # Exception handling
- The
The with
statement is particularly useful when working with resources that require explicit setup and cleanup procedures. It enhances code readability and reduces the likelihood of resource leaks by ensuring that cleanup operations are consistently performed, even in the presence of exceptions.
Q: Explain decorators in python
In Python, decorators are a powerful and flexible way to modify or extend the behavior of functions or methods. Decorators allow you to wrap a function with additional functionality without changing its source code directly. They are often used for tasks such as logging, memoization, access control, and more.
The syntax for using a decorator involves placing the decorator symbol (@decorator_name
) above the function definition. The decorator can be a function or a class.
Here's a basic overview of how decorators work:
Decorator Function:
- A decorator is a function that takes another function as its argument and returns a new function that usually extends or modifies the behavior of the original function.
pythondef my_decorator(func): def wrapper(): print("Something is happening before the function is called.") func() print("Something is happening after the function is called.") return wrapper
Applying the Decorator:
- Use the
@decorator_name
syntax to apply a decorator to a function.
python@my_decorator def say_hello(): print("Hello!") say_hello()
This is equivalent to
say_hello = my_decorator(say_hello)
.- Use the
Chaining Decorators:
- You can apply multiple decorators to a single function, and they will be applied in the order they appear.
python@decorator1 @decorator2 @decorator3 def my_function(): # Function code
Passing Arguments to Decorators:
- Decorators can accept arguments, allowing for more flexibility.
pythondef parametrized_decorator(param): def decorator(func): def wrapper(): print(f"Decorator parameter: {param}") func() return wrapper return decorator @parametrized_decorator("some_value") def my_function(): print("Hello from my_function!") my_function()
In this example,
parametrized_decorator
is a decorator factory that returns a decorator based on the provided parameter.
Decorators are widely used in Python for various purposes, including code organization, code reuse, and aspect-oriented programming. Common use cases include logging, timing, access control, and memoization. Understanding decorators is essential for writing clean and modular code.
Q: map
, filter
, and reduce
functions
In functional programming, Python's map
, filter
, and reduce
functions are powerful tools that allow for concise and expressive manipulation of data. Here's a brief overview of each:
map
Function:Purpose:
map
applies a given function to all items in an iterable (e.g., a list) and returns a new iterable with the results.Example:
pythonnumbers = [1, 2, 3, 4, 5] squared_numbers = map(lambda x: x**2, numbers)
This will result in
squared_numbers
containing[1, 4, 9, 16, 25]
.
filter
Function:Purpose:
filter
constructs a list from those elements of the iterable for which a function returns true.Example:
pythonnumbers = [1, 2, 3, 4, 5] even_numbers = filter(lambda x: x % 2 == 0, numbers)
This will result in
even_numbers
containing[2, 4]
.
reduce
Function:Purpose:
reduce
is not a built-in function in Python 3, but it can be imported from thefunctools
module. It successively applies a binary function to the items of an iterable, reducing it to a single accumulated result.Example:
pythonfrom functools import reduce numbers = [1, 2, 3, 4, 5] sum_all = reduce(lambda x, y: x + y, numbers)
This will result in
sum_all
containing15
(the sum of all elements).
When discussing these functions in an interview, it's essential to demonstrate not only the syntax but also an understanding of how they fit into functional programming paradigms. Emphasize the immutability of data, the avoidance of side effects, and the benefits of writing more declarative and concise code.
Q: Monolithic vs Microservice architecture
Monolithic and microservice architectures are two different approaches to structuring applications. Here's a detailed comparison:
Monolithic Architecture:
- A monolithic application is built as a single, unified unit.
- All functionalities of a project exist in a single codebase.
- It's often easier to develop and deploy due to its simplicity.
- It's ideal for smaller, less complex applications or for businesses with limited resources.
- However, it becomes too large and difficult to manage over time.
- Any change requires updating the entire stack, making updates restrictive and time-consuming.
- A single bug in any module can bring down the entire application.
Microservice Architecture:
- A microservices architecture is a collection of smaller, independently deployable services.
- Each service handles a small portion of the functionality and data.
- It's more appropriate for larger and more intricate applications that demand greater scalability and flexibility.
- It allows for the use of different technologies and languages across services.
- It's failure-resistant and fault-tolerant.
- However, it's more complex to develop and requires managing inter-service communication.
- It can also introduce challenges related to data consistency and managing distributed systems.
In summary, the choice between monolithic and microservices architectures depends on the specific requirements of your project.
Q: Mean, Median, and Mode in Python
Please refer to this.
Q: What is the purpose of asterisk (*
) & forward slash (/
) in function arguments?
asterisk (*
) and forward slash (/
) controls how you pass values to the function.
- Arguments before
/
are positional-only. - Arguments between
/
and*
can be positional or keyword. - Arguments after
*
are keyword-only.
def my_func(position_only, /, positional_or_keyword, *, keyword_only):
print(position_only, positional_or_keyword, keyword_only)
my_func(1, 2, keyword_only=3) # ✅ Valid
my_func(1, positional_or_keyword=2, keyword_only=3) # ✅ Valid
my_func(position_only=1, positional_or_keyword=2, keyword_only=3) # ❌ Invalid
my_func(1, 2, 3) # ❌ Invalid
You can even use *
or /
alone in function arguments.
def my_func(*, keyword_only):
print(keyword_only)
def my_func(position_only, /):
print(position_only)