Advanced Oops Interview Question 1
Advanced Oops Interview Question 1
Interview Questions
(Practice Project)
1. What is a metaclass in Python?
Answer:
In Python, a metaclass is a class that defines the behavior of other classes. It's often described as "a class of a
class" because it's used to create and customize classes themselves. When you define a class, Python uses a
metaclass to create that class object. By default, Python uses the `type` metaclass, but you can create custom
metaclasses to modify class creation behavior.
```python
class MyMetaclass(type):
class MyClass(metaclass=MyMetaclass):
pass
``
In this example, `MyMetaclass` adds a custom attribute to any class that uses it as its metaclass.
Answer:
To define a class using a custom metaclass, you use the `metaclass` keyword argument in the class definition.
```python
class MyMetaclass(type):
class MyClass(metaclass=MyMetaclass):
# Class definition
pass
```
You can also use the metaclass for all classes in a module by setting the `__metaclass__` attribute at the
module level (in Python 2) or by using the `metaclass` parameter in the base class for Python 3.
PW Skills
```python
class LoggingMetaclass(type):
if callable(attr_value):
attrs[attr_name] =
cls.log_method(attr_value)
@staticmethod
def log_method(method):
return wrapper
class MyClass(metaclass=LoggingMetaclass):
def some_method(self):
print("Doing something")
obj = MyClass()
obj.some_method()
# Output:
# Doing something
```
In this example, the `LoggingMetaclass` adds logging functionality to all methods of classes that use it.
Answer:
Multiple inheritance is a feature in object-oriented programming where a class can inherit attributes and
methods from more than one parent class. This allows a derived class to combine and reuse code from
multiple base classes.
In Python, multiple inheritance is implemented by listing all parent classes in the class definition, separated by
commas. Here's a basic example:
```python
class A:
def method_a(self):
print("Method A")
class B:
def method_b(self):
print("Method B")
def method_c(self):
print("Method C")
obj = C()
```
PW Skills
In this example, class `C` inherits from both `A` and `B`, so it has access to methods from both parent classes.
Multiple inheritance can be powerful, but it also introduces complexity, especially when dealing with method
resolution order (MRO) and the potential for naming conflicts between parent classes.
Answer:
Both `@classmethod` and `@staticmethod` are decorators used to define methods that don't require access to
instance-specific data, but they have some key differences:
`@classmethod`
Receives the class as the implicit first argument (conventionally named `cls`
Can access and modify class stat
Can be called on both the class and its instance
Often used for alternative constructors or methods that operate on the class itself
`@staticmethod`
Doesn't receive any implicit first argumen
Cannot access or modify class or instance state (unless explicitly passed
Can be called on both the class and its instance
Behaves like a plain function that's defined inside the class for namespace purposes
```python
class MyClass:
class_attribute = 0
self.instance_attribute = value
@classmethod
def class_method(cls):
cls.class_attribute += 1
return cls.class_attribute
@staticmethod
return x + y
def instance_method(self):
return self.instance_attribute
# Usage
print(MyClass.class_method()) # Output: 1
obj = MyClass(5)
print(obj.class_method()) # Output: 2
print(obj.instance_method()) # Output: 5
```
PW Skills
In this example, `class_method` can modify the class state, `static_method` behaves like a regular function, and
`instance_method` requires an instance to be called.
Answer:
To stack multiple decorators on a single function, you simply apply them one after another, with the topmost
decorator being applied last. The order of decorators matters, as each decorator modifies the function returned
by the decorator below it.
```python
def decorator1(func):
print("Decorator 1 - Before")
print("Decorator 1 - After")
return result
return wrapper
def decorator2(func):
print("Decorator 2 - Before")
print("Decorator 2 - After")
return result
return wrapper
@decorator1
@decorator2
def my_function():
print("Original function")
my_function()
```
Output:
```
Decorator 1 - Before
Decorator 2 - Before
Original function
Decorator 2 - After
Decorator 1 - After
```
PW Skills
In this example, `decorator2` is applied first, then `decorator1`. The execution order is:
1. `decorator1` starts
2. `decorator2` starts
4. `decorator2` finishes
5. `decorator1` finishes
You can stack as many decorators as needed, and they will be applied in the order they are listed, from bottom
to top.
Answer:
In a metaclass, `__new__` and `__init__` serve different purposes and are called at different times during
class creation:
`__new__`
```python
class MyMetaclass(type):
cls.initialized = True
class MyClass(metaclass=MyMetaclass):
x = 1
y = 2
print(MyClass.X) # Output: 1
```
PW Skills
In this example, `__new__` modifies the class attributes (converting keys to uppercase) before the class is
created, while `__init__` adds a new attribute after the class has been created.
Answer:
The C3 linearization algorithm is used by Python to determine the Method Resolution Order (MRO) in cases of
multiple inheritance. It ensures a consistent and predictable order for method lookup. The algorithm works as
follows:
1. Start with the class for which you're computing the MRO.
2. For each parent class (from left to right in the inheritance list):
a. Add the parent class to the MRO if it's not already there and all its parents are already in the MRO.
4. If any classes remain, raise an error (this indicates an impossible inheritance structure).
Here's an example:
```python
class A: pass
print(D.mro())
```
In this case, the C3 algorithm ensures that `B` comes before `C` in `D`'s MRO, respecting the order in which they
were inherited.
8. What is the diamond problem in multiple inheritance, and how does Python resolve it?
Answer:
The diamond problem occurs in multiple inheritance when a class inherits from two classes that have a
common ancestor. This creates an ambiguity about which implementation of a method to use if it's defined in
multiple parent classes.
```
PW Skills
A
/ \
B C
\ /
```
If `B` and `C` both override a method from `A`, and `D` inherits from both `B` and `C`, which version of the
method should `D` use?
Python resolves this using the C3 linearization algorithm to determine the Method Resolution Order (MRO). The
MRO provides a consistent order for method lookup, avoiding ambiguity.
Here's an example:
```python
class A:
def method(self):
print("A's method")
class B(A):
def method(self):
print("B's method")
class C(A):
def method(self):
print("C's method")
pass
d = D()
print(D.mro())
```
In this case, `B`'s method is called because `B` comes before `C` in the MRO. The `super()` function can be used to
call methods from parent classes in a way that respects the MRO.
9. Write a metaclass that adds a `log` method to all classes using it.
Answer:
Here's an implementation of a metaclass that adds a `log` method to all classes using it:
PW Skills
```python
import logging
class LoggingMetaclass(type):
logging.info(f"{self.__class__.__name__}:
{message}")
attrs['log'] = log
# Example usage
class MyClass(metaclass=LoggingMetaclass):
def some_method(self):
# Set up logging
logging.basicConfig(level=logging.INFO)
obj = MyClass()
obj.some_method()
```
This metaclass adds a `log` method to every class that uses it. The `log` method uses Python's built-in `logging`
module to log messages with the class name as a prefix.
```
```
This metaclass allows any class using it to easily log messages without having to implement the logging
functionality in each class individually.
Answer:
Memoization is an optimization technique that stores the results of expensive function calls and returns the
cached result when the same inputs occur again. Decorators are an excellent way to implement memoization
in Python. Here's an example:
PW Skills
```python
def memoize(func):
cache = {}
@wraps(func)
return cache[key]
return wrapper
# Example usage
@memoize
def fibonacci(n):
if n < 2:
return n
```
In this example, the `memoize` decorator creates a cache dictionary. When the decorated function is called, it
checks if the result for the given arguments is already in the cache. If so, it returns the cached result. If not, it
calls the function, stores the result in the cache, and then returns it.
This implementation dramatically speeds up recursive functions like Fibonacci, reducing time complexity from
O(2^n) to O(n).
Answer:
To create a decorator that takes arguments, you need to add an extra level of nesting. The outermost function
takes the decorator arguments, the middle function is the actual decorator, and the innermost function is the
wrapper around the decorated function. Here's the general structure:
```python
def decorator(func):
@wraps(func)
pass
return wrapper
return decorator
```
PW Skills
Here's a practical example of a decorator that repeats a function a specified number of times:
```python
def repeat(times):
def decorator(func):
@wraps(func)
for _ in range(times):
return result
return wrapper
return decorator
# Usage
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
```
In this example, `repeat` is a decorator factory that takes an argument `times`. It returns the actual decorator,
which then wraps the function to be decorated.
Answer:
The `functools.wraps` decorator is used to preserve the metadata of the original function when it's decorated.
When you create a decorator, you're essentially replacing the original function with a new one (the wrapper).
This can lead to loss of important metadata such as the function's name, docstring, and argument list.
Here's an example to illustrate the problem and how `functools.wraps` solves it:
```python
def decorator_without_wraps(func):
return wrapper
PW Skills
def decorator_with_wraps(func):
@wraps(func)
return wrapper
@decorator_without_wraps
def hello_world():
print("Hello, World!")
@decorator_with_wraps
def goodbye_world():
print("Goodbye, World!")
```
As you can see, without `@wraps`, the decorated function loses its original name and docstring. With `@wraps`,
these are preserved, which is crucial for introspection, debugging, and generating accurate documentation.
Answer:
The singleton pattern ensures that a class has only one instance and provides a global point of access to that
instance. Here's how you can implement it using a metaclass:
```python
class Singleton(type):
_instances = {}
cls._instances[cls] = super().__call__(*args,
**kwargs)
return cls._instances[cls]
class MyClass(metaclass=Singleton):
def __init__(self):
self.value = None
self.value = value
# Usage
a = MyClass()
b = MyClass()
a.set_value(5)
print(b.value) # Output: 5
```
PW Skills
In this implementation, the `Singleton` metaclass overrides the `__call__` method. When a class using this
metaclass is instantiated, `__call__` checks if an instance already exists. If not, it creates one; otherwise, it
returns the existing instance.
This ensures that no matter how many times you try to create an instance of `MyClass`, you always get the
same object.
14. Write a class decorator that counts the number of instances created.
Answer:
Here's a class decorator that counts the number of instances created for a class:
```python
def instance_counter(cls):
original_init = cls.__init__
instance_count = 0
nonlocal instance_count
instance_count += 1
self.instance_number = instance_count
cls.__init__ = new_init
def get_instance_count(cls):
return instance_count
cls.get_instance_count =
classmethod(get_instance_count)
return cls
# Usage
@instance_counter
class MyClass:
self.value = value
# Create instances
obj1 = MyClass(1)
obj2 = MyClass(2)
obj3 = MyClass(3)
print(obj1.instance_number) # Output: 1
print(obj2.instance_number) # Output: 2
print(obj3.instance_number) # Output: 3
print(MyClass.get_instance_count()) # Output: 3
```
2. It defines a new `__init__` method that increments the instance count and assigns an instance number to
each new instance.
3. It adds a class method `get_instance_count` to retrieve the total number of instances created.
This implementation allows you to track how many instances have been created and gives each instance a
unique number.
PW Skills
15. Explain how to use `super()` in a multiple inheritance scenario.
Answer:
`super()` is a built-in function in Python that's used to call methods from parent classes. In a multiple
inheritance scenario, `super()` becomes particularly useful as it follows the Method Resolution Order (MRO) to
determine which parent class's method to call.
```python
class A:
def method(self):
print("A's method")
class B(A):
def method(self):
print("B's method")
super().method()
class C(A):
def method(self):
print("C's method")
super().method()
def method(self):
print("D's method")
super().method()
d = D()
d.method()
print(D.mro())
```
Output:
```
D's method
B's method
C's method
A's method
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
```
In this example:
`super()` ensures that all methods in the inheritance chain are called in the correct order, following the MRO.
This is particularly useful for cooperative multiple inheritance, where each class in the inheritance hierarchy
needs to do its part and then pass control to the next class.
Using `super()` in this way helps avoid the diamond problem and ensures that each method in the inheritance
chain is called exactly once, in the order specified by the MRO.
PW Skills