Top Python Interview Questions and Answers (Latest 2024)

author image Hirely
at 05 Jan, 2025

Question: What is the concept of decorators in Python?

Answer:

In Python, a decorator is a special type of function that is used to modify or enhance the behavior of other functions or methods. Decorators allow you to wrap a function with additional functionality without modifying its actual code. This concept is used to add functionality to existing code in a modular and reusable way.


How Decorators Work:

A decorator is essentially a function that takes another function as input and returns a new function that usually extends or alters the behavior of the original function.

Decorator Syntax:

@decorator_function
def some_function():
    # Function body

In this syntax:

  • The @decorator_function is the decorator.
  • The some_function() is the function being decorated.

This is equivalent to:

def some_function():
    # Function body

some_function = decorator_function(some_function)

Basic Example of a Decorator:

Here’s an example of a simple decorator that prints a message before and after the execution of a function:

# Define the decorator function
def my_decorator(func):
    def wrapper():
        print("Before the function call.")
        func()
        print("After the function call.")
    return wrapper

# Use the decorator with the @ symbol
@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

Output:

Before the function call.
Hello!
After the function call.

In this example:

  • my_decorator is the decorator function.
  • say_hello is the function being decorated.
  • When say_hello is called, it is actually wrapped by the wrapper function in my_decorator, which adds additional behavior.

Key Concepts of Decorators:

  1. Functions are First-Class Objects: Functions in Python are first-class objects, which means they can be passed as arguments to other functions, returned as values, and assigned to variables. This is what allows decorators to work.

  2. Higher-Order Functions: A decorator is a higher-order function, meaning it takes another function as an argument and returns a new function.

  3. Closure: In the decorator example, the inner function (wrapper) is a closure. It has access to the outer function’s variables, in this case, the func argument. This is why the decorator can modify or call the original function within the wrapper.


Decorators with Arguments:

Decorators can also be created to accept arguments if needed. To do this, you would define a decorator function that returns a function which can accept arguments.

Here’s an example of a decorator that takes an argument:

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:

Hello, Alice!
Hello, Alice!
Hello, Alice!

In this example:

  • repeat is a decorator that takes an argument n, which specifies how many times to repeat the decorated function call.
  • The greet function is wrapped in wrapper, and it is called n times.

Built-In Decorators in Python:

Python has several built-in decorators that you can use, such as:

  • @staticmethod: Defines a static method in a class.
  • @classmethod: Defines a class method.
  • @property: Defines a property in a class (for getter/setter methods).

Example of using @staticmethod:

class MyClass:
    @staticmethod
    def greet():
        print("Hello from a static method!")

MyClass.greet()  # Output: Hello from a static method!

Decorators with Functions That Take Arguments:

If the decorated function takes arguments, you can use *args and **kwargs to ensure that all arguments are passed correctly:

def decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Arguments passed: {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@decorator
def add(a, b):
    return a + b

print(add(2, 3))  # Output: Arguments passed: (2, 3), {}
                  #         5

In this example:

  • The wrapper function prints the arguments passed to the decorated add function.

Chaining Decorators:

You can apply multiple decorators to a single function. When this happens, the decorators are applied in a bottom-to-top order, i.e., the decorator closest to the function is applied first.

def decorator1(func):
    def wrapper():
        print("Decorator 1")
        return func()
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2")
        return func()
    return wrapper

@decorator1
@decorator2
def say_hello():
    print("Hello!")

say_hello()

Output:

Decorator 1
Decorator 2
Hello!

Here:

  • decorator2 is applied first to say_hello, and then decorator1 is applied to the result of that.

Use Cases for Decorators:

  1. Logging: Log function calls for debugging or monitoring.
  2. Authorization/Authentication: Check if the user is authorized to access a resource.
  3. Caching: Cache the results of expensive function calls to improve performance.
  4. Timing: Measure the time it takes for a function to execute.
  5. Validation: Validate the input arguments of a function.

Summary:

  • A decorator is a function that modifies the behavior of another function without changing its code.
  • Decorators are applied using the @decorator_name syntax.
  • They are useful for adding reusable functionality like logging, timing, validation, etc.
  • Decorators can take arguments, and they can be chained to apply multiple modifications.
  • Python has built-in decorators like @staticmethod, @classmethod, and @property.

Decorators are a powerful feature in Python that allow for clean, modular, and reusable code enhancements.

Question: What are *args and **kwargs in Python?

Answer:

In Python, *args and **kwargs are used to pass a variable number of arguments to a function. They allow you to create functions that can accept an arbitrary number of positional or keyword arguments. They are very useful for writing flexible and reusable functions.


1. *args (Positional Arguments)

*args allows a function to accept any number of positional arguments (arguments that are passed without specifying the parameter names). The args variable is treated as a tuple inside the function, and you can loop through it or access individual elements like any other tuple.

  • Syntax:

    def function_name(*args):
        # Function body
  • Example:

    def add(*args):
        total = 0
        for num in args:
            total += num
        return total
    
    print(add(1, 2, 3))  # Output: 6
    print(add(10, 20))    # Output: 30

In this example:

  • The *args in the add() function allows it to accept any number of arguments. These arguments are accessible inside the function as a tuple, and you can iterate through them or perform operations on them.

  • Key Note: The *args name is not fixed. You can use any name (like *values), but it is conventional to use *args for readability and consistency.


2. **kwargs (Keyword Arguments)

**kwargs allows a function to accept any number of keyword arguments (arguments passed in the form key=value). These arguments are passed as a dictionary inside the function, where the keys are the argument names, and the values are the corresponding argument values.

  • Syntax:

    def function_name(**kwargs):
        # Function body
  • Example:

    def greet(**kwargs):
        for key, value in kwargs.items():
            print(f"{key}: {value}")
    
    greet(name="Alice", age=25)  
    # Output: 
    # name: Alice
    # age: 25

In this example:

  • The **kwargs in the greet() function allows it to accept any number of keyword arguments. These are passed to the function as a dictionary, and you can iterate through them or access the values by key.

  • Key Note: The **kwargs name is also not fixed, but **kwargs is the standard convention.


3. Combining *args and **kwargs

You can use both *args and **kwargs in the same function to accept both positional and keyword arguments. However, the *args parameter must appear before the **kwargs parameter in the function definition.

  • Syntax:

    def function_name(arg1, arg2, *args, kwarg1=None, kwarg2=None, **kwargs):
        # Function body
  • Example:

    def describe_person(name, age, *args, **kwargs):
        print(f"Name: {name}")
        print(f"Age: {age}")
        
        if args:
            print("Other Info:", args)
        
        if kwargs:
            print("Additional Info:", kwargs)
    
    describe_person("Alice", 30, "Engineer", "New York", city="London", country="UK")

Output:

Name: Alice
Age: 30
Other Info: ('Engineer', 'New York')
Additional Info: {'city': 'London', 'country': 'UK'}

In this example:

  • *args accepts positional arguments that are passed as a tuple ("Engineer", "New York").
  • **kwargs accepts keyword arguments that are passed as a dictionary ({"city": "London", "country": "UK"}).

4. Order of Parameters in Function Definition

When defining a function, the order of parameters should follow this pattern:

  1. Regular positional parameters
  2. *args (optional)
  3. Default keyword parameters (optional)
  4. **kwargs (optional)

Here is an example that illustrates this order:

def example_function(a, b, *args, c=10, d=20, **kwargs):
    print(f"a: {a}, b: {b}, args: {args}, c: {c}, d: {d}, kwargs: {kwargs}")

example_function(1, 2, 3, 4, 5, c=30, e="hello", f="world")

Output:

a: 1, b: 2, args: (3, 4, 5), c: 30, d: 20, kwargs: {'e': 'hello', 'f': 'world'}

5. Practical Use Cases of *args and **kwargs

  • *args: Useful when you want to allow a function to accept a variable number of arguments, for example when summing values or concatenating strings.

    Example:

    def concatenate_strings(*args):
        return " ".join(args)
    
    print(concatenate_strings("Hello", "world!"))  # Output: "Hello world!"
  • **kwargs: Useful for situations where you want to pass multiple optional parameters as key-value pairs, such as when configuring a function or passing extra data to an API.

    Example:

    def build_url(base_url, **params):
        url = base_url + "?"
        for key, value in params.items():
            url += f"{key}={value}&"
        return url.rstrip("&")
    
    print(build_url("https://example.com", page=1, size=10))  # Output: "https://example.com?page=1&size=10"

Summary of Key Differences:

Feature*args**kwargs
Type of ArgumentsAccepts a variable number of positional arguments.Accepts a variable number of keyword arguments (key-value pairs).
UsageUsed when the number of arguments is unknown and passed as a tuple.Used when the function needs to accept named arguments dynamically.
Syntax*args (tuple inside the function).**kwargs (dictionary inside the function).

Conclusion:

  • *args and **kwargs provide flexibility in Python function definitions, allowing you to pass an arbitrary number of arguments to a function.
  • *args is used for positional arguments, and **kwargs is used for keyword arguments.
  • These features are commonly used in functions that accept varying numbers of arguments or when extending functions, such as in decorators or in functions that need to accept extra configuration parameters.

Read More

If you can’t get enough from this article, Aihirely has plenty more related information, such as Python interview questions, Python interview experiences, and details about various Python job positions. Click here to check it out.

Related Posts

Trace Job opportunities

Hirely, your exclusive interview companion, empowers your competence and facilitates your interviews.

Get Started Now