I like turkus answer, however examples shown are mostly theoretical and aren't common in day to day coding.
The main practical difference between generator function (with yield) and function which returns generator is that the generator function is lazily evaluated.
Consider this session:
$ python
Python 3.6.0
[GCC 6.3.1 20170109] on linux
>>> def a():
... print('in a')
... yield 0
...
>>> def b():
... print('in b')
... return iter(range(1))
...
>>> aa = a() # Lazy evaluation - nothing printed after this line.
>>> next(aa)
in a
0
>>> next(aa)
Traceback ...
StopIteration
>>> bb = b() # Eager evaluation - whole function is executed after this.
in b
>>> next(bb)
0
>>> next(bb)
Traceback ...
StopIteration
None of them is a golden bullet.
To give you a real example of where this lazy evaluation makes a huge difference in your code check this example.
def get_numbers(x: int):
if x < 0:
raise ValueError("Value cannot be negative")
for i in range(x):
yield i
try:
numbers = get_numbers(-5)
except ValueError:
pass # log or something
else:
print(list(numbers)) # <== ValueError is thrown here!
Here is where lazy evaluation is actually bad for your function. It will throw exception in arguably wrong place because the intention is to make it fail just at the start, not during iteration.
With this implementation you're passing responsibility of triggering the generator function and managing exception to its user which is tedious and somewhat ugly:
import itertools
try:
numbers = get_numbers(-5)
first = next(numbers)
numbers = itertools.chain([first], numbers)
except ValueError:
...
The best way to solve this is to make a function that returns a generator instead a generator function:
def get_numbers(x: int):
if x < 0:
raise ValueError("Value cannot be negative")
return (i for i in range(x)) # I know it can be just `return range(x)`, but I keep it that way to make a point.
As you can see there is no "best way" to do it, both options are viable. It all depends on how you want things to work.
nexton the result ofg(x)?