code: def side_effect(func, iterable, chunk_size=None, before=None, after=None):
      
      try:
          if before is not None:
              before()
  
          if chunk_size is None:
              for item in iterable:
                  func(item)
                  yield item
          else:
              for chunk in chunked(iterable, chunk_size):
                  func(chunk)
                  yield from chunk
      finally:
          if after is not None:
              after()
docs: Invoke *func* on each item in *iterable* (or on each *chunk_size* group
      of items) before yielding the item.
  
      `func` must be a function that takes a single argument. Its return value
      will be discarded.
  
      *before* and *after* are optional functions that take no arguments. They
      will be executed before iteration starts and after it ends, respectively.
  
      `side_effect` can be used for logging, updating progress bars, or anything
      that is not functionally "pure."
  
      Emitting a status message:
  
          >>> from more_itertools import consume
          >>> func = lambda item: print('Received {}'.format(item))
          >>> consume(side_effect(func, range(2)))
          Received 0
          Received 1
  
      Operating on chunks of items:
  
          >>> pair_sums = []
          >>> func = lambda chunk: pair_sums.append(sum(chunk))
          >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2))
          [0, 1, 2, 3, 4, 5]
          >>> list(pair_sums)
          [1, 5, 9]
  
      Writing to a file-like object:
  
          >>> from io import StringIO
          >>> from more_itertools import consume
          >>> f = StringIO()
          >>> func = lambda x: print(x, file=f)
          >>> before = lambda: print(u'HEADER', file=f)
          >>> after = f.close
          >>> it = [u'a', u'b', u'c']
          >>> consume(side_effect(func, it, before=before, after=after))
          >>> f.closed
          True