Make conditional "with" preciner

Any advice on what is the correct "pythonic" way for the next function. Should I split it into two functions?

def readSomething(fp=None):
    if fp:
        return fp.read(100)
    else:
        with open('default.txt', 'r') as fp:
            return fp.read(100)

      


I need something like this because a function readSomething

can be called from another function that may or may not open the same file.

For example, it might be named like this in some places:

def doSomethingWithSameFile():
    with open('default.txt') as fp:
         preample = fp.read(10)
         more_data = readSomething(fb)
         ...

      

or as elsewhere:

def init():
    data = readSomething()
    ...

      

+3


source to share


5 answers


I don't think this is the right solution, but I think this is what you want.

import contextlib

def readSomething(fp=None):
    with contextlib.ExitStack() as stack:
        if not fp:
            fp = stack.enter_context(open('default.txt'))
        return fp.read(100)

      




I get the impression that you are going to duplicate this logic across many functions such as readSomething()

, so I would recommend putting the ExitStack code in a decorator and wrap the functions where you need this behavior.

You can also use a decorator. I am not using this code, so the syntax below is almost certainly incomplete, but the general idea is worth it:

import functools

def fallback_to_default(fn):
    @functools.wraps(fn)
    def new_fn(fp=None, *args, **kwargs):
        with contextlib.ExitStack() as stack:
            if not fp:
                fp = stack.enter_context(open('default.txt'))
            return fn(fp, *args, **kwargs)
    return new_fn

@fallback_to_default
def readSomething(fp=None):
    return fp.read(100)

      

+4


source


To summarize the problem in plain language:

  • You may be given a handle to an open file, after which you want to leave it open, since it is the responsibility of the caller to close that resource.
  • You may need to open your own resource and it is your responsibility to close it.

This is a problem with accepting heterogeneous argument types in Python. You can do this, but it can make your code a little more ugly at times.



Context managers are just syntactic sugar for try / finally:

def readSomething(fp=None):
    close_fp = False
    if fp is None:
        fp = open('default.txt')
        close_fp = True
    try:
        return fp.read(100)
    finally:
        if close_fp:
            fp.close()

      

To make it prettier than that, consider changing the interfaces so that you don't have to handle both read data and resource management from the same function — refactoring so that your functions have one responsibility.

+3


source


You can define a custom context manager that only does something if None is passed to it, but that might be overkill:

class ContextOrNone(object):
    def __init__(self, obj, fn, *args, **kwargs):
        if obj is not None:
            self.obj = obj
            self.cleanup = False
        else:
            self.obj = fn(*args, **kwargs)
            self.cleanup = True
    def __enter__(self):
        return self.obj
    def __exit__(self, ex_type, ex_val, traceback):
        if self.cleanup:
            self.obj.__exit__(ex_type, ex_val, traceback)

      

Or, using contextlib.contextmanager

:

from contextlib import contextmanager

@contextmanager
def ContextOrNone(obj, fn, *args, **kwargs):
    was_none = obj is None
    try:
        if was_none:
            obj = fn(*args, **kwargs)
        yield obj
    finally:
        if was_none:
            obj.__exit__()

      

Once you have defined this, you can define it readSomething

as:

def readSomething(fp=None):
    with ContextOrNone(fp, open, 'default.txt', 'r') as fp:
        return fp.read(100)

      

+3


source


Honestly, the most Pythonic version of your code is probably exactly what you already have, other than a slightly cleaned up one:

def readSomething(fp=None):
    if fp:
        return fp.read(100)
    with open('default.txt') as fp:
        return fp.read(100)

      

This retains your original intent and functionality. It's clear and easy to read. Of course this has a bit of repetition. If your example has been simplified to the point that the repetitive part is too grotesque for you, then hoist it into your own function:

def complicatedStuff(buf, sz):
    # Obviously more code will go here.
    return buf.read(sz)

def readSomething(fp=None):
    if fp:
        return complicatedStuff(fp, 100)
    with open('default.txt') as fp:
        return complicatedStuff(fp, 100)

      

It's not Pythonic to jump over a lot of hoops so as not to repeat a bit.

+2


source


This does not use with

and close the default or file passed as argument, but it might be an option.

def readSomething(fp=None):
    if fp is None:
        fp = open('default.txt')
    return (fp.read(100), fp.close)

      

0


source







All Articles