Does `yield from` have O(1) time complexity? - python

Consider the following code snippet.
from typing import Iterable
def geometric_progression(
start: float, multiplier: float, num_elements: int
) -> Iterable[float]:
assert num_elements >= 0
if num_elements > 0:
yield start
yield from geometric_progression(
start * multiplier, multiplier, num_elements - 1
)
This function returns the first num_elements of the geometric progression starting with start and multipliying by multiplier each time. It's easy to see that the last element will be passed through one yield-statement and num_elements-1 yield-from-statements. Does this function have O(num_elements) time complexity, or does it have O(num_elements**2) time complexity due to a "ladder" of nested yield-from-statements of depths 0, 1, 2, ..., num_elements-2, num_elements-1?
EDIT: I've come up with a simpler code snippet to demonstrate what I am asking.
from typing import Iterable, Any
def identity_with_nested_yield_from(depth: int, iterable: Iterable[Any]) -> Iterable[Any]:
assert depth >= 1
if depth == 1:
yield from iterable
else:
yield from identity_with_nested_yield_from(depth-1, iterable)
Is this function O(depth + length of iterable), or is it O(depth * length of iterable)?

I could've sworn there was an optimization in place to shortcut these kinds of yield from chains, but testing shows no such optimization, and I couldn't find anything in the places I thought the optimization was implemented either.
The generators on each level of a yield from chain must be suspended and resumed individually to pass yield and send values up and down the chain. Your function has O(num_elements**2) time complexity. Also, it hits a stack overflow once the call stack reaches a depth of 1000.

yield from is formally equivalent to a loop of response = yield child.send(response), plus error propagation and handling. When consumed in iteration, the response is always None and no errors are propagated/handled. This is equivalent to a for loop.
# `yield from child` without error handling/response
for x in child:
yield x
Thus, each yield from has the time/space complexity of iterating its argument. Stacking yield from of a size n child a total of m times thus has a time complexity of O(nm).

Related

Python Program Giving me StopIteration

I have made a function: generatesequence (shown below)
def generatesequence(start: float, itera: float = 1, stop: float = None):
"""
Generate a sequence, that can have a stopping point, starting point.
"""
__num = start
# if sequence has a stopping point
if stop != None:
# if stop is negative
if stop < 0:
# while num is greater than stop (0 < 5, but 0 > -5)
while __num >= stop:
# yield __num variable (yield = return without exiting function)
yield __num
# add iter to __num
__num += itera
else:
while __num <= stop:
yield __num
__num += itera
else:
# if sequence has no stopping point, run forever
while True:
yield __num
__num += itera
I have also made a Sequence Class (also shown below)
class Sequence:
def __init__(self, start, itera, stop):
self.sequence = generatesequence(start, itera, stop)
self.sequencelength = iterlen(self.sequence)
print(self.sequencelength)
def printself(self):
for i in range(self.sequencelength):
print(next(self.sequence))
However, when I run printself on a Sequence instance, it gives me a StopIteration error. How can I fix this?
You don't need to do that with a generator, you can just do the following:
def printself(self):
for i in self.sequence:
print(i)
This way you don't need to calculate the length of the generator beforehand
Caculating length of generator defies the whole purpose of using generator. And it also explains StopIteration.
Unlike list or some data structure that takes O(n) memory space, generator takes O(1) space and it cannot know the length without iterating one by one.
And by calcuating length you have moved the iter for your generator from start to end, and now your iter points at StopIteration.
Now when you access generator afterwards it returns StopIteration.
Actually the whole purpose of generator and the likes is to save memory space for iterables that you know will be iterated at most once. You can not do two or more full iterations on generator. To do that, use list function on generator beforehand and save values in list or similar data structures . Or simply recreate generator after it's been used up (=iterated over).
In short, to fix bug, remove the line where it computes length of generator in init method. And do for loop using
"for i in generator_name: "
syntax
Alternatively you can make a method that makes generator and call it to recreate generator whenever / whereever you need

Yield in recursion multiplayer

i just wrote this function and got error from the interpreter "RecursionError: maximum recursion depth exceeded in comparison"
is it possible to use yield in this recursion?
def multiplyer(fir, sec):
if(sec==1):
return fir
else:
return fir+multiplyer(fir, sec-1)
print(multiplyer(5, 2983))
There is no need to use yield at all (and as far as I know, it will not work anyway). Your multiplayer function is simply equivalent to:
def multiplayer(fir,sec):
return fir*sec
furthermore yield will not make much difference since it will still result in a recursion error: after all you will still perform calls 2983 deep (which is usually too much for the call stack). Python also does not support tail recursion optimization (TRO).
yield is used when you want use a generator. A generator produces several (it can be zero, one or more) values. Here however you need a single value (and you need it immediately). Say that you however use yield like:
def multiplyer(fir, sec):
if(sec==1):
yield fir
else:
yield fir+next(multiplyer(fir, sec-1))
print(next(multiplyer(5, 2983)))
it will not make any difference: you will still do the recursion and reach the bound.
You run out of stack space since you let the function call itself 2983 times, which means you store that number of return addresses and arguments on the stack, which is just not reasonable.
If your requirement is to use recursion, you can reduce the recursion depth to O(logn) order by doing this:
def multiplyer(fir, sec):
if sec==1:
return fir
elif sec==0:
return 0
else:
return multiplyer(fir, sec//2) + multiplyer(fir, (sec+1)//2)
print(multiplyer(5, 2983))
Or more efficiently, also reducing the number of recursive calls to O(logn) order:
def multiplyer(fir, sec):
if sec==0:
return 0
elif sec%2 == 0:
return 2 * multiplyer(fir, sec//2)
else:
return fir + 2 * multiplyer(fir, sec//2)
print(multiplyer(5, 2983))
When dealing with recursion, you can check the maximum allowed recursion depth like:
import sys
sys.getrecursionlimit()
and the nice thing is you can also set it:
sys.setrecursionlimit(3000)
However, this says nothing about your code or if it is optimal for what you are trying to achieve.
Don't use recursion in Python where iteration will do. Generators optimize for memory usage, nothing else. They are slower than the alternatives, and do not provide any workarounds for the global recursion limit (which is actually a limit on the size of the call stack; non-recursive calls count towards the limit as well).
# Just demonstrating the conversion from recursion to iteration.
# fir * sec would be the most efficient solution.
def multiplyer(fir, sec):
rv = 0
while sec > 0:
rv += fir
sec -= 1
return rv

python deque understanding

I came across a bit of code in StackOverflow that raised two questions about the way deque works. I don't have enough reputation to ask "in situ", therefore this question:
from collections import deque
from itertools import islice
def sliding_window(iterable, size=2, step=1, fillvalue=None):
if size < 0 or step < 1:
raise ValueError
it = iter(iterable)
q = deque(islice(it, size), maxlen=size)
if not q:
return # empty iterable or size == 0
q.extend(fillvalue for _ in range(size - len(q))) # pad to size
while True:
yield iter(q) # iter() to avoid accidental outside modifications
q.append(next(it))
q.extend(next(it, fillvalue) for _ in range(step - 1))
The code computes a sliding window of a given size over a sequence.
The steps I don't understand are first:
q = deque(islice(it, size), maxlen=size)
What is the use of maxlen here? Isn't islice always going to output an iterable of at most length size?
And second:
yield iter(q) # iter() to avoid accidental outside modifications
why do we need to transform to to iterable to avoid "accidental outside modifications"?
To answer second part of the question, everything in Python is passed by reference. So in case of above generator q is a reference to the original deque hold by the function, so any method that may amend the deque, would break original algorithm of the generation. When you surround q with iter() what you effectively have yielded is an iterator. You can take elements from iterator (read), but you cannot change elements itself or amend the sequence of them (write not allowed). So it's a good practice to protect from accidental damage to the container hold internally be the generator.
To answer the first part of your question, setting maxlen will make the deque not exceed that size as items are added - older items are discarded.

Time complexity of this recursive python k-combination generator function

I was looking for a python k-combination algorithm and found this little beauty here https://stackoverflow.com/a/2837693/553383
Any idea about its T(n) and/or time complexity?
Here is the code that you'll find in above link:
def choose_iter(elements, length):
for i in xrange(len(elements)):
if length == 1:
yield (elements[i],)
else:
for next in choose_iter(elements[i+1:len(elements)], length-1):
yield (elements[i],) + next
def choose(l, k):
return list(choose_iter(l, k))
Assuming this function indeed generates all possible combinations of length k, its time complexity of this function is O(n!/[(n-k)!k!] * k^2).
There are exactly O(n!/[(n-k)!k!]) k-combinations, and we generate each of them.
Let's look on the geration of each. It is done by creating a tuple iteratively. First the 1st element is added, then the 2nd, then the 3rd and so on.
However, cretaing a tuple of length k is O(k), and we actually get O(1+2+...+k) for each tuple creation. Since O(1+2+...+k)=O(k^2), and we do that for each tuple, we can conclude that the total complexity of this function is O(n!/[(n-k)!k!] * k^2).

What is the big-Oh runtime of two recursive O(logn) calls?

def f(L):
if len(L) < 1 billion:
return L
else:
return f(L[:len(L) // 2]) + f(L[len(L) // 2:])
L is a list of size n
I know that if it was a single recursive call, then it would be O(logn), but there are two recursive calls here.
But it started to exhibit more of a O(n) runtime as I began to run it on a visualizer.
In my opinion it should be O(logn+logn) = O(2logn) = O(logn). Am I correct?
Consider how many calls you're doing. At the first level of the recursion you'll do 2 calls. For each of those you'll do two more calls. Etc ... This means that at level i of the recursion you'll have made a total of O(2^i) function calls.
How many levels of the recursion are there? This is just the height of a binary tree with n elements, which is O(log_2 n).
So by the time you reach all the leaves of the recursion you will have done O(2^(log_2 n)) = O(n) function calls.
--
Another way of looking at it is that you eventually have to piece back together the entire list, so how could you possibly do that in less than O(n) time?
Your algorithm as it stands is going to be O(n) if len(L) is at least 1 billion because you will break the list into two, and then add the two halves back together. Both slicing and adding are O(n) operations.
If you want to test the runtime of the two recursive calls,
1. Pass in a start and end index, and call
f(L, start, start+(end-start)//2) + f(L, start+(end-start)//2, end)
2. Return end-start or some other O(1) value when end-start is less than 1 billion

Categories

Resources