Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compose two functions whose outer function supplies arguments to the inner function

I have two similar codes that need to be parsed and I'm not sure of the most pythonic way to accomplish this.

Suppose I have two similar "codes"

secret_code_1 = 'asdf|qwer-sdfg-wert$$otherthing'
secret_code_2 = 'qwersdfg-qw|er$$otherthing'

both codes end with $$otherthing and contain a number of values separated by -

At first I thought of using functools.wrap to separate some of the common logic from the logic specific to each type of code, something like this:

from functools import wraps

def parse_secret(f):
  @wraps(f)
  def wrapper(code, *args):
    _code = code.split('$$')[0]
    return f(code, *_code.split('-'))
  return wrapper

@parse_secret
def parse_code_1b(code, a, b, c):
  a = a.split('|')[0]
  return (a,b,c)

@parse_secret
def parse_code_2b(code, a, b):
  b = b.split('|')[1]
  return (a,b)

However doing it this way makes it kind of confusing what parameters you should actually pass to the parse_code_* functions i.e.

parse_code_1b(secret_code_1)
parse_code_2b(secret_code_2)

So to keep the formal parameters of the function easier to reason about I changed the logic to something like this:

def _parse_secret(parse_func, code):
  _code = code.split('$$')[0]
  return parse_func(code, *_code.split('-'))

def _parse_code_1(code, a, b, c):
  """
  a, b, and c are descriptive parameters that explain
  the different components in the secret code

  returns a tuple of the decoded parts
  """
  a = a.split('|')[0]
  return (a,b,c)

def _parse_code_2(code, a, b):
  """
  a and b are descriptive parameters that explain
  the different components in the secret code

  returns a tuple of the decoded parts
  """
  b = b.split('|')[1]
  return (a,b)

def parse_code_1(code):
  return _parse_secret(_parse_code_1, code)

def parse_code_2(code):
  return _parse_secret(_parse_code_2, code)

Now it's easier to reason about what you pass to the functions:

parse_code_1(secret_code_1)
parse_code_2(secret_code_2)

However this code is significantly more verbose.

Is there a better way to do this? Would an object-oriented approach with classes make more sense here?

repl.it example

like image 597
Tom Avatar asked Dec 28 '16 20:12

Tom


4 Answers

repl.it example

Functional approaches are more concise and make more sense.

We can start from expressing concepts in pure functions, the form that is easiest to compose.

Strip $$otherthing and split values:

parse_secret = lambda code: code.split('$$')[0].split('-')

Take one of inner values:

take = lambda value, index: value.split('|')[index]

Replace one of the values with its inner value:

parse_code = lambda values, p, q: \
  [take(v, q) if p == i else v for (i, v) in enumerate(values)]

These 2 types of codes have 3 differences:

  • Number of values
  • Position to parse "inner" values
  • Position of "inner" values to take

And we can compose parse functions by describing these differences. Split values are keep packed so that things are easier to compose.

compose = lambda length, p, q: \
  lambda code: parse_code(parse_secret(code)[:length], p, q)

parse_code_1 = compose(3, 0, 0)
parse_code_2 = compose(2, 1, 1)

And use composed functions:

secret_code_1 = 'asdf|qwer-sdfg-wert$$otherthing'
secret_code_2 = 'qwersdfg-qw|er$$otherthing'
results = [parse_code_1(secret_code_1), parse_code_2(secret_code_2)]
print(results)
like image 112
DarkKnight Avatar answered Sep 28 '22 04:09

DarkKnight


I believe something like this could work:

secret_codes = ['asdf|qwer-sdfg-wert$$otherthing', 'qwersdfg-qw|er$$otherthing']


def parse_code(code):
    _code = code.split('$$')
    if '-' in _code[0]:
        return _parse_secrets(_code[1], *_code[0].split('-'))
    return _parse_secrets(_code[0], *_code[1].split('-'))


def _parse_secrets(code, a, b, c=None):
    """
    a, b, and c are descriptive parameters that explain
    the different components in the secret code

    returns a tuple of the decoded parts
    """
    if c is not None:
        return a.split('|')[0], b, c
    return a, b.split('|')[1]


for secret_code in secret_codes:
    print(parse_code(secret_code))

Output:

('asdf', 'sdfg', 'wert')
('qwersdfg', 'er')

I'm not sure about your secret data structure but if you used the index of the position of elements with data that has | in it and had an appropriate number of secret data you could also do something like this and have an infinite(well almost) amount of secrets potentially:

def _parse_secrets(code, *data):
    """
    data is descriptive parameters that explain
    the different components in the secret code

    returns a tuple of the decoded parts
    """
    i = 0
    decoded_secrets = []
    for secret in data:
        if '|' in secret:
            decoded_secrets.append(secret.split('|')[i])
        else:
            decoded_secrets.append(secret)
        i += 1
    return tuple(decoded_secrets)
like image 33
Bill Schumacher Avatar answered Sep 28 '22 05:09

Bill Schumacher


I'm really not sure what exactly you mean. But I came with idea which might be what you are looking for.

What about using a simple function like this:

def split_secret_code(code): 
    return [code] + code[:code.find("$$")].split("-")

And than just use:

parse_code_1(*split_secret_code(secret_code_1))
like image 44
Filip Dobrovolný Avatar answered Sep 28 '22 05:09

Filip Dobrovolný


I'm not sure exactly what constraints you're working with, but it looks like:

  1. There are different types of codes with different rules
  2. The number of dash separated args can vary
  3. Which arg has a pipe can vary

Straightforward Example

This is not too hard to solve, and you don't need fancy wrappers, so I would just drop them because it adds reading complexity.

def pre_parse(code):
    dash_code, otherthing = code.split('$$')
    return dash_code.split('-')

def parse_type_1(code):
    dash_args = pre_parse(code)
    dash_args[0], toss = dash_args[0].split('|')
    return dash_args

def parse_type_2(code):
    dash_args = pre_parse(code)
    toss, dash_args[1] = dash_args[1].split('|')
    return dash_args

# Example call
parse_type_1(secret_code_1)

Trying to answer question as stated

You can supply arguments in this way by using python's native decorator pattern combined with *, which rolls/unrolls positional arguments into a tuple, so you don't need to know exactly how many there are.

def dash_args(code):
    dash_code, otherthing = code.split('$$')
    return dash_code.split('-')

def pre_parse(f):
    def wrapper(code):
        # HERE is where the outer function, the wrapper,
        # supplies arguments to the inner function.
        return f(code, *dash_args(code))
    return wrapper

@pre_parse
def parse_type_1(code, *args):
    new_args = list(args)
    new_args[0], toss = args[0].split('|')
    return new_args

@pre_parse
def parse_type_2(code, *args):
    new_args = list(args)
    toss, new_args[1] = args[1].split('|')
    return new_args

# Example call:
parse_type_1(secret_code_1)

More Extendable Example

If for some reason you needed to support many variations on this kind of parsing, you could use a simple OOP setup, like

class BaseParser(object):
    def get_dash_args(self, code):
        dash_code, otherthing = code.split('$$')
        return dash_code.split('-')

class PipeParser(BaseParser):
    def __init__(self, arg_index, split_index):
        self.arg_index = arg_index
        self.split_index = split_index

    def parse(self, code):
        args = self.get_dash_args(code)
        pipe_arg = args[self.arg_index]
        args[self.arg_index] = pipe_arg.split('|')[self.split_index]
        return args

# Example call
pipe_parser_1 = PipeParser(0, 0)
pipe_parser_1.parse(secret_code_1)
pipe_parser_2 = PipeParser(1, 1)
pipe_parser_2.parse(secret_code_2)
like image 36
Chris Avatar answered Sep 28 '22 04:09

Chris