A namespace is a space that holds names(identifiers). Programmatically speaking, namespaces are dictionary of identifiers(keys) and their objects(values)

There are 4 types of namespaces:

  1. Builtin Namespace:
  • This namespace includes all the built-in names that Python provides, such as print(), len(), and other functions, as well as built-in types like int, str, etc.

2. Global Namespace:

  • This namespace contains names defined at the top level of the script or module. These names are accessible throughout the entire module.

3. Enclosing Namespace:

  • This namespace is applicable only to nested functions. If a function is defined inside another function, the inner function can access names from the outer (enclosing) function’s namespace.

4. Local Namespace:

  • This namespace includes names that are local to a specific function or block of code. It’s the most specific namespace and has the highest priority during name resolution.

Scope and LEGB Rule:

Scope:

  • A scope is a region of a program where a namespace can be directly accessed. In Python, scopes are determined by the structure of the code (e.g., functions, classes, modules).

LEGB Rule:

  • The LEGB rule describes the order in which the interpreter looks for names during variable or function resolution:
    1. Local (L): Search for the name in the local namespace (inside the current function or block).
    2. Enclosing (E): If the name is not found locally, search in the enclosing functions’ namespaces (for nested functions).
    3. Global (G): If the name is still not found, search in the global namespace (at the top level of the module).
    4. Builtin (B): If the name is not found in any of the above, finally search in the built-in namespace.

Remembering the LEGB rule is crucial for understanding how Python resolves names and scopes during program execution. If a name is not found in any of these scopes, a NameError is raised.

Section 1: Global and Local Variables

# Global and Local Variables

# Global variable
a = 2

def temp():
    # Local variable
    b = 3
    print(b)

temp()  # Output: 3
print(a)  # Output: 2

Section 2: Local and Global Variables with Same Name

# Local and Global Variables with Same Name

# Local and global variables with the same name
a = 2

def temp():
    # Local variable with the same name as the global variable
    a = 3
    print(a)

temp()  # Output: 3
print(a)  # Output: 2

Section 3: Local Variable Not Defined Locally

# Local Variable Not Defined Locally

# Local variable not defined locally but present globally
a = 2

def temp():
    # Accessing the global variable
    print(a)

temp()  # Output: 2
print(a)  # Output: 2

Section 4: Editing Global Variable without Declaration

# Editing Global Variable without Declaration

# Editing global variable without declaration
a = 2

def temp():
    try:
        # Attempting to modify the global variable without declaration
        a += 1
        print(a)
    except UnboundLocalError as e:
        print(f"Error: {e}")

temp()  # Output: UnboundLocalError: local variable 'a' referenced before assignment
print(a)

Section 5: Editing Global Variable with ‘global’ Declaration

# Editing Global Variable with 'global' Declaration

# Editing global variable with 'global' declaration
a = 2

def temp():
    # Using 'global' to modify the global variable
    global a
    a += 1
    print(a)

temp()  # Output: 3
print(a)  # Output: 3

Section 6: Global Variable Created Inside Local Scope

# Global Variable Created Inside Local Scope

# Global variable created inside local scope
def temp():
    # Creating a global variable inside the local scope
    global a
    a = 1
    print(a)

temp()  # Output: 1
print(a)  # Output: 1

Section 7: Function Parameter as Local Variable

# Function Parameter as Local Variable

# Function parameter as a local variable
def temp(z):
    # Using the function parameter as a local variable
    print(z)

a = 5
temp(5)  # Output: 5
print(a)  # Output: 5

Section 8: Built-in Scope

# Built-in Scope

# Built-in scope
import builtins
print("Built-in Scope:", dir(builtins))

# To see all the built-ins, you can use 'dir(builtins)' as shown above

Section 9: Renaming Built-ins

# Renaming Built-ins

# Renaming built-ins
L = [1, 2, 3]
print("Max of L:", max(L))  # Output: 3

# Redefining 'max' function
def max():
    print('hello')

print("Max Function:", max(L))  # Output: hello

Section 10: Enclosing Scope

# Enclosing Scope

# Enclosing scope
def outer():
    def inner():
        print(a)
    inner()
    print('outer function')

outer()  # Output: 2 (a is accessed from the enclosing scope)
print('main program')

Section 11: Nonlocal Keyword

# Nonlocal Keyword

# Nonlocal keyword
def outer():
    a = 1
    def inner():
        nonlocal a
        a += 1
        print('inner', a)
    inner()
    print('outer', a)

outer()  # Output: inner 2, outer 2
print('main program')

Decorators

A decorator in python is a function that receives another function as input and adds some functionality(decoration) to and it and returns it.

This can happen only because python functions are 1st class citizens.

There are 2 types of decorators available in python

  • Built in decorators like @staticmethod, @classmethod, @abstractmethod and @property etc
  • User defined decorators that we programmers can create according to our needs

Certainly! I’ve added comments and potential outputs to the modified code:

Certainly! I’ve separated each section into distinct blocks:

Section 1: First-class Functions and Modifying Function Example

# First-class Functions and Modifying Function Example

def modify(func, num):
    return func(num)

def square(num):
    return num**2

result = modify(square, 2)
print("Section 1 Output:", result)  # Output: 4 (2^2)

Section 2: Simple Decorator Example

# Simple Decorator Example

def my_decorator(func):
    def wrapper():
        print('***********************')
        func()
        print('***********************')
    return wrapper

def hello():
    print('hello')

def display():
    print('hello nabin') 

# Decorating 'hello' function
a = my_decorator(hello)
a()  # Output: Decorated hello

# Decorating 'display' function
b = my_decorator(display)
b()  # Output: Decorated hello nabin

Section 3: Decorators with Timer Functionality

# Decorators with Timer Functionality

import time

def timer(func):
    def wrapper(*args):
        start = time.time()
        func(*args)
        print('time taken by', func.__name__, time.time()-start, 'secs')
    return wrapper

# Decorating 'hello' function with timer
@timer
def hello():
    print('hello world')
    time.sleep(2)

# Decorating 'square' and 'power' functions with timer
@timer
def square(num):
    time.sleep(1)
    print(num**2)

@timer
def power(a, b):
    print(a**b)

hello()  # Output: Decorated hello world, time taken by hello 2.0 secs
square(2)  # Output: Decorated 4, time taken by square 1.0 secs
power(2, 3)  # Output: 8, time taken by power 0.0 secs

Section 4: Decorators with Arguments

# Decorators with Arguments

def sanity_check(data_type):
    def outer_wrapper(func):
        def inner_wrapper(*args):
            if type(*args) == data_type:
                func(*args)
            else:
                raise TypeError('This datatype is not allowed') 
        return inner_wrapper
    return outer_wrapper

# Decorating 'square' function with type-checking for integers
@sanity_check(int)
def square(num):
    print(num**2)

# Decorating 'greet' function with type-checking for strings
@sanity_check(str)
def greet(name):
    print('hello', name)

square(2)  # Output: 4
# square("2")  # Raises TypeError: This datatype is not allowed

Leave a Reply

Your email address will not be published. Required fields are marked *