3

Is it possible to have a function which will produce a generator when passed a list, but will then return when given individual values?

Take this for example:

def duty2015(x):

  if type(x) in [list, np.ndarray]:
    for xi in x:
      yield new_duty(xi)

  else:

    sd = 0
    if x <= 120000:
      return sd
    elif x <= 250000:
      return (x-125000) * 0.02
    elif x <= 925000:
      return 2500 + (x-250000)*0.05
    elif  x <= 1500000:
      return 36250 + (x-925000)*0.1
    else:
      return 93750 + (x-1500000)*0.12

obviously this doesn't work, and i get the SyntaxError: 'return' with argument inside generator error.

I realise i could do something like this instead:

def duty2015(x):

  if type(x) in [list, np.ndarray]:
    for xi in x:
      for result in duty2015(xi):
        yield result

  else:

    sd = 0
    if x <= 125000:
      yield sd
    elif x <= 250000:
      yield (x-125000) * 0.02
    elif x <= 925000:
      yield 2500 + (x-250000)*0.05
    elif  x <= 1500000:
      yield 36250 + (x-925000)*0.1
    else:
      yield 93750 + (x-1500000)*0.12

but then when i call it on individual items it gives me a generator, which i'd rather only get when it's called on larger items.

Obviously i could do it as a list, but again this is not optimal.

For the comments in answers below, would something like this be better then:

def duty_new(x, generator=False):

  if type(x) in [list, np.ndarray]:
    if generator:
      return (duty_new(xi) for xi in x)
    else:
      return [duty_new(xi) for xi in x]

  else:

    sd = 0
    if x <= 125000:
      return sd
    elif x <= 250000:
      return (x-125000) * 0.02
    elif x <= 925000:
      return 2500 + (x-250000)*0.05
    elif  x <= 1500000:
      return 36250 + (x-925000)*0.1
    else:
      return 93750 + (x-1500000)*0.12

So that under normal use, it would have a predictable behaviour or returning the same type of argument as is passed to it (for sensible arguments at least, and probably something so that it doesn't just iterate through numpy arrays), but if a generator were needed it could be explicitly asked for?

3
  • Please show us how you use duty2015(). The thing is, that generator and function is hard to use in same context. Commented Jan 30, 2015 at 13:15
  • No, you cannot have both generator and normal function in one single function. This is decided when the function is actually compiled. In Python 3 though you can return value with return statement, they are passed to the StopIteration exception which you can later access using .args argument of the caught exception. But that will be an ugly way to do this I guess. In the solutions posted below you'll have to still check the type of returned item. Commented Jan 30, 2015 at 13:26
  • @glglgl that was a typo! Commented Jan 30, 2015 at 13:26

4 Answers 4

5

Return a generator expression, like this

def duty2015(x):

  if isinstance(x, list) or isinstance(x, np.ndarray):
      return (result for xi in x for result in duty2015(xi))
  else:
     ...
     ...

Now, whenever you call duty2015 with a single element you will get individual value, otherwise you will get a generator expression, which has to be iterated with next protocol.

Personally, I feel that your second version is good and it is consistent, because it makes duty2015 a generator function and as Martijn Pieters mentioned in the comments it doesn't have its caller guess what it got back, better stick to that.

Note: Your code in the first version and second version are different. I choose the code in the second version to show the idea.

Sign up to request clarification or add additional context in comments.

2 Comments

Yes, consistency in what a type of object a function returns is important; don't have your caller guessing at what to expect back.
@MartijnPieters I agree with this, and thinking about it, i wanted to have it returning the same type of object as was passed to it, but then have the problem that if i give it a big list, then it will have to compute the whole list (obviously a list in this example is never going to be that big, but just wanted to see if there was a more sensible way of dealing with it - having it return the same type of object, except in some circumstances where it returns a generator is probable not the best idea)
2

How about this way, return a generator? :

def new_fun(x):
  return x + 1

def duty2015(x):

  if type(x) is list:
    return (new_fun(i) for i in x)

  else:

    sd = 0
    if x <= 120000:
      return sd
    elif x <= 250000:
      return (x-125000) * 0.02
    elif x <= 925000:
      return 2500 + (x-250000)*0.05
    elif  x <= 1500000:
      return 36250 + (x-925000)*0.1
    else:
      return 93750 + (x-1500000)*0.12

print duty2015([1,2,3])
print (1)

Output:

<generator object <genexpr> at 0x10be06b40>
1

So that you can use return to get a generator object.

Comments

0

You can return the result of another function which is a generator, like this:

def dolist(li):
    for el in li:
        yield el * 2

def either(x):
    if isinstance(x, list):
        return dolist(x)
    else:
        return x * 2

print either(4) # prints 8
print list(either([2,3])) # prints [4,6]

1 Comment

I would have done it this way, but i didn't want to create a second function - it's more of a curiosity thing.
0

You can return a generator from within a function. Either via a

def duty2015(x):

  if isinstance(x, list) or isinstance(x, np.ndarray):
    return (new_duty(xi) for xi in x)
  else:
    ...

or with a helper generator function:

def duty_gen(x):
    for xi in x:
        yield new_duty(xi)

def duty2015(x):

    if isinstance(x, list) or isinstance(x, np.ndarray):
        return duty_gen(x)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.