Search code examples
pythonpython-decorators

Python use decorator to implement context manager


I am trying to make two decorators with parameters. First creates a list with element x and calls func. second is simply calling the first by passing a parameter from a dict.

def first(x=1):
    def wrapped(func):
        l = [x]
        func(l)
        print(l)
    return wrapped

def second(d={'x':10}):
    return first(x=d['x'])

third function simply modifies the list passed in. I want to make any of the four decorators below possible by simply calling third(). How should I modify my code?

##@second
##@second({'x':100})
##@first
##@first(x=10)
def third(l):
    l.append(-1)

third()

For example:

## With @first,
## I am expecting to get [1, -1].
## With @first(x=10),
## I am expecting to get [10, -1].
## With @second,
## I am expecting to get [10, -1].
## With @second({x:100}),
## I am expecting to get [100, -1].

The upper code is an abstraction of my problem. My real problem is that I want a decorator that handles opening and closing connection for me, so that I only need to write code for handling the connection.

And the connection needs parameters, which is first. I want the parameters to be passed in a different way, which is the second. third is what I am gonna do with the connection. I want third to be called like a normal function, and it also handles the opening and closing connection using a decorator. Sorry if decorator should not be used this way, but I really want to practice using it.

---Update---

What I want to achieve is basically the following:

def context_manager(username='user', password='password'):
    conn = OpenConnection()
    func(conn)
    CloseConnection()

def context_manager2(d={'username': 'user', 'password': 'password'}):
    content_manager(username=d['username'], password=d['password'])

# @context_manager
# @context_manager('username', '123456')
# @context_manager2
# @context_manager2(d={'username': 'username', 'password': '123456'})
def execute(conn):
    pass

I want to make any of the four decorators possible and still be able to call execute in a way like execute()


Solution

  • Looks like you maybe just need a primer on what a decorator is. A decorator is a function that accepts a function as its only argument, and returns a function in its place. They often take the form of:

    def decorator(f):
        def wrapped(*args, **kwargs):
            # it's important to accept any arguments to wrapped, thus *args and **kwargs
            # because then you can wrap _any_ function and simply pass its arguments on.
            print("Inside the wrapped function")
    
            retval = f(*args, **kwargs)  # pass the arguments through to the decorated function
    
            print("Exiting the wrapped function")
    
            return retval
    
        return wrapped
    

    This lets you do something like:

    @decorator
    def my_increment(x):
        print("Calculating...")
        return x + 1
    
    # actually equivalent to
    def my_increment(x):
        print("Calculating...")
        return x + 1
    
    my_increment = decorator(my_increment)
    

    and expect results like:

    >>> print(my_increment(3))
    Inside the wrapped function
    Calculating...
    Exiting the wrapped function
    4
    

    Notably: my_increment becomes the decorated function at runtime, not at call time. You can't call my_increment without the decorator functionality.


    What you're attempting to do doesn't look anything like what you'd use a decorator for. This looks like function chaining to me.

    def first(x=1):
        return [x]
    
    def second(d=None):
        if d is None:
            d = {'x':10}  # why do this? https://stackoverflow.com/q/1132941/3058609
        return first(d['x'])
    
    def third(lst):
        return lst + [-1]
    

    and call it like:

    # With @first,
    # I am expecting to get [1, -1].
    third(first())  # [1, -1]
    
    # With @first(x=10),
    # I am expecting to get [10, -1].
    third(first(10))  # [10, -1]
    
    # With @second,
    # I am expecting to get [10, -1].
    third(second())  # [10, -1]
    
    # With @second({x:100}),
    # I am expecting to get [100, -1].
    third(second({'x':100}))  # [100, -1]
    

    Note also that decorators can take parameters, but then you're talking about (bear with me...) a function that takes parameters that returns a function which takes a function and returns a function. You're just abstracting one more layer. Imagine:

    def decorator_abstracted(msg):
        """Takes a message and returns a decorator"""
    
        # the below is almost the exact same code as my first example
        def decorator(f):
            def wrapped(*args, **kwargs):
                print(msg)
                retval = f(*args, **kwargs)
                print("exiting " + msg)
                return retval
    
            return wrapped
        return decorator
    

    Now your code could be

    @decorator_abstracted("a decorator around my_increment")
    def my_increment(x):
        print('Calculating...')
        return x + 1