Interview Questions
1. What is the concept behind the 'self' variable?
2. What does self refer to in the __init__ method?
3. When does a class method does not take self as the first argurment?
4. What is the concept behind the __init__ method?
5. Explain the need for Object Oriented Programming when we could do stuff with Functional programing?
6. What is the difference between a function and a method?
7. Apart from attributes and methods, what else can a Python object hold?
In Python, an object can hold several types of data beyond just attributes (data members) and methods (functions). Here’s a breakdown of other things a Python object can hold:
1. Class Variables
- Definition: Variables that are shared among all instances of a class. These are defined within the class but outside any instance methods.
- Usage: Often used to maintain shared state or constants across all instances of a class.
pythonclass MyClass:
shared_variable = "I am shared"
def __init__(self, value):
self.instance_variable = value
2. Static Methods and Class Methods
- Static Methods: Methods that do not operate on an instance of the class. They are bound to the class and not the object instance.
pythonclass MyClass:
@staticmethod
def static_method():
print("This is a static method")
- Class Methods: Methods that are bound to the class itself and not to any particular object instance. They receive the class as the first argument (
cls
).
pythonclass MyClass:
@classmethod
def class_method(cls):
print("This is a class method")
3. Properties (Getter and Setter Methods)
- Properties: Special methods that allow controlled access to an object’s attributes. They are often used to encapsulate data and add logic to getting and setting attribute values.
pythonclass MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = new_value
4. Descriptors
- Definition: Objects that define the behavior of attribute access, specifically through the use of
__get__()
, __set__()
, and __delete__()
methods. Descriptors are a way to create reusable and controlled attribute management.
pythonclass Descriptor:
def __get__(self, instance, owner):
return "Descriptor get method"
class MyClass:
attr = Descriptor()
5. Custom __call__
Method
- Definition: An object can be made callable by defining the
__call__()
method. This allows the object to be called as if it were a function.
pythonclass MyCallable:
def __call__(self, x):
return x * x
obj = MyCallable()
print(obj(5)) # Output: 25
6. Slots
- Definition: A special mechanism (
__slots__
) that restricts the creation of instance attributes to a predefined set, reducing memory usage for instances of the class.
pythonclass MyClass:
__slots__ = ['attr1', 'attr2']
def __init__(self, attr1, attr2):
self.attr1 = attr1
self.attr2 = attr2
7. Magic Methods (Dunder Methods)
- Definition: Special methods that begin and end with double underscores, like
__init__
, __str__
, __repr__
, __eq__
, etc. These methods allow objects to interact with Python’s built-in functions and operators.
pythonclass MyClass:
def __str__(self):
return "String representation of MyClass"
8. Type Annotations
- Definition: While not held directly by the object, Python objects can have type annotations for attributes, which are hints for developers and tools to understand what type of data the attribute should hold.
pythonclass MyClass:
attr: int
def __init__(self, attr: int):
self.attr = attr
9. Metaclasses
- Definition: The class of a class, controlling the creation of classes themselves. Metaclasses define how classes behave.
pythonclass Meta(type):
def __new__(cls, name, bases, attrs):
return super(Meta, cls).__new__(cls, name, bases, attrs)
class MyClass(metaclass=Meta):
pass
10. Annotations (__annotations__
)
- Definition: A dictionary attribute where type annotations are stored. This can be accessed via the
__annotations__
attribute on the class or instance.
pythonclass MyClass:
attr: int = 5
print(MyClass.__annotations__) # Output: {'attr': <class 'int'>}
11. Custom Attributes Added Dynamically
- Definition: Python objects are dynamic, so you can add attributes to instances even after they’ve been created.
pythonclass MyClass:
pass
obj = MyClass()
obj.new_attr = "Dynamically added attribute"
These additional elements allow Python objects to be highly flexible and powerful, supporting a wide range of programming paradigms and advanced behaviors.
8. Can you explain this with an example?
Certainly! Let's explore the various things a Python object can hold by creating an example class that demonstrates these concepts.
Example Class: Car
We'll create a class called Car
that showcases attributes, methods, class variables, static methods, class methods, properties, descriptors, magic methods, and more.
python# Descriptor example
class FuelEfficiency:
def __get__(self, instance, owner):
return instance._mileage / instance._fuel_used
class Car:
# Class variable
vehicle_type = "Automobile"
# __slots__ example to limit attributes
__slots__ = ['make', 'model', 'year', '_mileage', '_fuel_used']
# Annotations for type hints
make: str
model: str
year: int
# Static variable for descriptor
fuel_efficiency = FuelEfficiency()
def __init__(self, make, model, year, mileage, fuel_used):
self.make = make
self.model = model
self.year = year
self._mileage = mileage
self._fuel_used = fuel_used
# Instance method
def drive(self, distance):
self._mileage += distance
print(f"Driven {distance} miles. Total mileage: {self._mileage} miles")
# Static method
@staticmethod
def honk():
print("Honk! Honk!")
# Class method
@classmethod
def from_string(cls, car_string):
make, model, year = car_string.split('-')
return cls(make, model, int(year), 0, 0)
# Property for controlled access to mileage
@property
def mileage(self):
return self._mileage
@mileage.setter
def mileage(self, value):
if value < 0:
raise ValueError("Mileage cannot be negative")
self._mileage = value
# Magic method to represent the object as a string
def __str__(self):
return f"{self.year} {self.make} {self.model}"
# Magic method to make the object callable
def __call__(self, action):
return f"The car is now {action}."
# Creating a car object using the class
my_car = Car("Toyota", "Corolla", 2020, 5000, 200)
# Demonstrating different aspects of the class
# Accessing class variable
print(f"Vehicle Type: {Car.vehicle_type}")
# Using an instance method
my_car.drive(150)
# Using a static method
Car.honk()
# Using a class method to create an instance
new_car = Car.from_string("Honda-Civic-2022")
print(f"New Car: {new_car}")
# Using a property
print(f"Mileage: {my_car.mileage}")
my_car.mileage = 6000 # Setting mileage via the property
print(f"Updated Mileage: {my_car.mileage}")
# Using the descriptor for fuel efficiency
print(f"Fuel Efficiency: {my_car.fuel_efficiency} miles per gallon")
# Using a magic method (__str__) to get a string representation
print(str(my_car))
# Using a magic method (__call__) to make the object callable
print(my_car("accelerating"))
# Adding a dynamic attribute (not allowed due to __slots__)
# my_car.color = "Blue" # This will raise an AttributeError
Explanation of Each Concept
Class Variable (vehicle_type
):
- This variable is shared by all instances of the class. It is accessible through the class itself (
Car.vehicle_type
) or any instance (my_car.vehicle_type
).
Instance Variables (make
, model
, year
, _mileage
, _fuel_used
):
- These are specific to each instance of the
Car
class, holding the state of the object. For example, my_car.make
holds the value "Toyota"
.
Static Method (honk
):
- This method does not depend on the instance's state and can be called on the class itself.
Car.honk()
prints a honk sound.
Class Method (from_string
):
- This method is bound to the class, not the instance. It's used here to create a
Car
object from a string. Car.from_string("Honda-Civic-2022")
creates a new Car
object.
Properties (mileage
):
- Properties provide a controlled way to access and set instance variables. The
mileage
property ensures that the mileage cannot be set to a negative value.
Descriptor (fuel_efficiency
):
- The
FuelEfficiency
descriptor calculates the car's fuel efficiency dynamically based on the mileage and fuel used. The descriptor is accessed like a normal attribute (my_car.fuel_efficiency
).
Magic Methods (__str__
, __call__
):
__str__
: Provides a readable string representation of the object, used when you print the object or convert it to a string.__call__
: Makes the object callable like a function, allowing my_car("accelerating")
to work.
Slots (__slots__
):
- The
__slots__
attribute limits the attributes that can be added to an instance, reducing memory usage. Attempting to add an attribute not listed in __slots__
will raise an AttributeError
.
This example demonstrates how a Python object can hold various types of data and behavior, making it a powerful and flexible construct in object-oriented programming.
9. Can I change a class variable?
And what would it be it's impant on the various instances of this class?
Yes, you can change a class variable in Python. The impact of changing a class variable depends on how you modify it, and it can affect all instances of the class in specific ways. Here's a detailed explanation:
1. Changing the Class Variable via the Class
When you change a class variable directly via the class, the change affects all instances of the class that do not have an overridden value for that variable.
pythonclass Car:
# Class variable
vehicle_type = "Automobile"
def __init__(self, make, model):
self.make = make
self.model = model
# Create two instances
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")
# Access the class variable
print(car1.vehicle_type) # Output: Automobile
print(car2.vehicle_type) # Output: Automobile
# Change the class variable via the class
Car.vehicle_type = "Motor Vehicle"
# Access the class variable again
print(car1.vehicle_type) # Output: Motor Vehicle
print(car2.vehicle_type) # Output: Motor Vehicle
Impact:
- When
Car.vehicle_type
is modified, all instances (car1
, car2
, etc.) reflect the new value, "Motor Vehicle"
, because they are still referencing the class variable. - Since
vehicle_type
is shared across all instances, changing it through the class affects every instance that hasn't overridden this variable.
2. Overriding the Class Variable via an Instance
If you change the class variable via an instance, it doesn't change the class variable itself but instead creates (or overrides) an instance variable with the same name.
pythonclass Car:
# Class variable
vehicle_type = "Automobile"
def __init__(self, make, model):
self.make = make
self.model = model
# Create two instances
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")
# Override the class variable in car1
car1.vehicle_type = "Electric Vehicle"
# Access the class variable
print(car1.vehicle_type) # Output: Electric Vehicle
print(car2.vehicle_type) # Output: Automobile
Impact:
car1.vehicle_type
now points to an instance variable, not the class variable. So car1
shows "Electric Vehicle"
.car2.vehicle_type
still references the class variable, so it continues to show "Automobile"
.- Changing
car1.vehicle_type
does not affect car2
or any other instance.
3. Resetting the Class Variable via the Class
If you reset the class variable after instances have overridden it, only those instances that have not overridden the variable will see the change.
pythonclass Car:
# Class variable
vehicle_type = "Automobile"
def __init__(self, make, model):
self.make = make
self.model = model
# Create two instances
car1 = Car("Toyota", "Corolla")
car2 = Car("Honda", "Civic")
# Override the class variable in car1
car1.vehicle_type = "Electric Vehicle"
# Change the class variable via the class
Car.vehicle_type = "Motor Vehicle"
# Access the class variable
print(car1.vehicle_type) # Output: Electric Vehicle (instance variable, unchanged)
print(car2.vehicle_type) # Output: Motor Vehicle (class variable, changed)
Impact:
car1
still holds its instance variable ("Electric Vehicle"
), so it isn't affected by the change in the class variable.car2
, which didn't override the class variable, now shows the updated value "Motor Vehicle"
.
Summary
- Class-Level Change: Changing a class variable directly via the class affects all instances that have not overridden that variable.
- Instance-Level Override: Changing a class variable via an instance creates an instance-specific variable, leaving the class variable unaffected for other instances.
- Instance Impact: Instances that override the class variable are unaffected by changes to the class variable, while those that don't will reflect any changes made to the class variable.
Monday, August 19, 2024
Intro to Classes and Objects in Python (Video and Interview Questions Inside)
To Watch All Python Videos: YouTube Academy For Python
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment