making calls to iter and next when iterating through a generator - python

i am writing a function that takes an iterator an int and a padding at the end to be added if what was iterated through has less than n values.. I am able to get the function working completely for the iterator parameters that are not of type generator and if it is it would raise the typerror exception where I would be working on the generator in that block of code. The problem is I am able to yield all values inside the generator but I have not been able to figure out a way to add the padding at the end because the outer for loop interferes. I need to implement this by making calls to iter and next which I have been playing around with but it has not been working... Here is the function ill explain
def n_with_pad(iterable,n,pad=None):
for i in range(n):
try:
yield iterable[i]
except IndexError:
yield pad
except TypeError:
for i in iterable:
yield i
so I were to call this function as follow
for i n_with_pad('function',3):
print(i,end=' ')
i would print: 'f' 'u' 'n'
but adding the pad with iterables that have less than n values would print as follows
for i n_with_pad('abcdefg',10,'?'):
print(i,end=' ')
'a', 'b', 'c', 'd', 'e', 'f', 'g', '?', '?' and '?'
for the second call I am able to get up to
'a', 'b', 'c', 'd', 'e', 'f', 'g'
with the code I have so far but cannot seem to add the ??? to satisfy n-values

I see no benefit to trying the __getitem__ approach and falling back to the iterator protocol. Just use the iterable, that's even the name of the variable!
def n_with_pad(iterable,n,pad=None):
it = iter(iterable)
for _ in range(n):
yield next(it,pad)
demo:
''.join(n_with_pad('function',3,pad='?'))
Out[6]: 'fun'
''.join(n_with_pad('function',10,pad='?'))
Out[7]: 'function??'

Related

Append multiple items to a list on a for loop in python

I have a nested python for loop and need to append 2 times a value, is the code below PEP8 valid? Or there is a better pythonic way to to write the function?
def function():
empty_list = []
my_list = ['a', 'b', 'c']
for letter_1 in my_list:
for letter_2 in my_list:
empty_list.append(letter_1)
empty_list.append(letter_2)
return empty_list
Your code is right and PEP8 compliant. I would remove the my_list from the function block and make it a function's parameter. I would suggest using list.extend() to perform the operation you need in one line. In order to make it a bit more Pythonic I would add typing hints and the function's docstring. The code would look like this:
from typing import List
def function(my_list: List) -> List:
"""Function's docstring.
Args:
my_list (List): List of characters.
Returns:
List: Processed list of characters.
"""
empty_list = []
for a in my_list:
for b in my_list:
empty_list.extend((a, b))
return empty_list
I don't know which IDE you use, but on Visual Studio Code you can download some extensions to generate docstrings automatically from your function's/classes' signature and typing hints. And also, there's extensions to automatically lint Python code to be PEP8 compliant.
I would also add a small test to make sure my function works as expected. Something like this:
assert function(['a', 'b', 'c']) == ['a', 'a', 'a', 'b', 'a', 'c',
'b', 'a', 'b', 'b', 'b', 'c', 'c', 'a', 'c', 'b', 'c', 'c']
Assuming that your desired output is:
Desired output:
['a','a','a','b','a','c', # letter_1, with loop of letter_2
'b','a','b','b','b','c', # letter_2, with loop of letter_2
'c','a','c','b','c','c'] # letter_3, with loop of letter_2
An alternative (more "pythonic"?) way to write your function is to use the itertools library and list comprehensions:
def alt_function(my_list = ['a', 'b', 'c']):
iterable = chain.from_iterable([a+b for a, b in product(my_list, repeat=2)])
return list(iterable)
alt_function()
Output
['a','a','a','b','a','c',
'b','a','b','b','b','c',
'c','a','c','b','c','c']

Getting Nth permutation of a sequence and getting back original using its index and modified sequence

I know the most popular permutation algorithms (thanks to wonderful question/answer on SO, and other related sites, such as Wikipedia, etc), but I recently wanted to see if I could get the Nth permutation without exhausting the whole permutation space.
Factorial comes to mind, so I ended up looking at posts such as this one that implements the unrank and rank algorithm, as well as many, many other ones. (Here as mentioned, I take into account other sites as "post")
I stumbled upon this ActiveState recipe which seems like it fit what I wanted to do, but it doesn't support doing the reverse (using the result of the function and reusing the index to get back the original sequence/order).
I also found a similar and related answer on SO: https://stackoverflow.com/a/38166666/12349101
But the same problem as above.
I tried and made different versions of the unrank/rank implementation(s) above, but they require that the sorted sequence be passed as well as the index given by the rank function. If a random (even within the range of the total permutation count) is given, it won't work (most of the time I tried at least).
I don't know how to implement this and I don't think I saw anyone on SO doing this yet. Is there any existing algorithm or way to do this/approach this?
To make things clearer:
Here is the Activestate recipe I posted above (at least the one posted in the comment):
from functools import reduce
def NPerms (seq):
"computes the factorial of the length of "
return reduce(lambda x, y: x * y, range (1, len (seq) + 1), 1)
def PermN (seq, index):
"Returns the th permutation of (in proper order)"
seqc = list (seq [:])
result = []
fact = NPerms (seq)
index %= fact
while seqc:
fact = fact // len (seqc)
choice, index = index // fact, index % fact
result += [seqc.pop (choice)]
return result
As mentioned, this handles doing part of what I mentioned in the title, but I don't know how to get back the original sequence/order using both the result of that function + the same index used.
Say I use the above on a string such as hello world inside a list:
print(PermN(list("hello world"), 20))
This output: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'd', 'r', 'o', 'l']
Now to see if this can go back to the original using the same index + result of the above:
print(PermN(['h', 'e', 'l', 'l', 'o', ' ', 'w', 'd', 'r', 'o', 'l'], 20))
Output: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'l', 'r', 'd', 'o']
I think this does what you want, and has the benefit that it doesn't matter what the algorithm behind PermN is:
def NmreP(seq,index):
# Copied from PermN
seqc = list (seq [:])
result = []
fact = NPerms (seq)
index %= fact
seq2 = list(range(len(seq))) # Make a list of seq indices
fwd = PermN(seq2,index) # Arrange them as PermN would
result = [0]*len(seqc) # Make an array to fill with results
for i,j in enumerate(fwd): # For each position, find the element in seqc in the position this started from
result[j] = seqc[i]
return result

Generator comprehension using another generator

I am trying to write a method that returns a Generator. The end result of these two methods is to get a combination of two lists in the form: 'A #1', 'B #1', ..., 'F #9'
FLATS = ['A', 'B', 'C', 'D', 'E', 'F']
def generate_nums() -> Generator[str, None, None]:
prefix = '#'
for num in range(10):
code = ''.join([prefix, str(num)])
yield code
def generate_room_numbers() -> Generator[str, None, None]:
room_nums = generate_nums()
yield (' '.join([flat_name, room_num]) for room_num in room_nums for flat_name in FLATS)
if __name__ == "__main__":
result = generate_room_numbers()
result = next(result) # I hate this. How do I get rid of this?
for room in result:
print(room)
This gives me the correct outcome. Although, my annoyance is the line result = next(result). Is there a better way to do this? I looked at this answer as well as the yield from syntax but I can barely understand generators enough as it is.
It will be best if the yield statement is put inside an explicit loop rather than trying to yield a generator.
Your generate_room_numbers should look like this:
def generate_room_numbers():
for flat_name in FLATS:
room_nums = generate_nums()
for room_num in room_nums:
yield (' '.join([flat_name, room_num]))
Note that the generate_nums() is called inside the flat_name loop, because you cannot repeatedly iterate over the same iterator that it returns; after iterating through it, it is exhausted and generate_nums will raise StopIteration every time (so that iterating produces an empty sequence).
(If generate_nums is expensive, then you could of course do nums = list(generate_nums()) outside the flat_name loop and then iterate over that inside the loop, but if this requires potentially a lot of memory, then it could defeat much of the point in using a generator in the first place.)
The rest of your code is unchanged except that the result = next(result) in the main code is removed, but for convenience, here is the whole thing:
FLATS = ['A', 'B', 'C', 'D', 'E', 'F']
def generate_nums():
prefix = '#'
for num in range(10):
code = ''.join([prefix, str(num)])
yield code
def generate_room_numbers():
for flat_name in FLATS:
room_nums = generate_nums()
for room_num in room_nums:
yield (' '.join([flat_name, room_num]))
if __name__ == "__main__":
result = generate_room_numbers()
# result = next(result) <<==== NOT NEEDED ANY MORE
for room in result:
print(room)
You could use a generator expression and a f-string:
FLATS = ['A', 'B', 'C', 'D', 'E', 'F']
room_numbers = (f'{letter} #{i}' for i in range(1, 10) for letter in FLATS)
for room in room_numbers:
print(room)
Output:
A #1
B #1
C #1
.
.
.
D #9
E #9
F #9

Generator not closing over data as expected

Sorry if the title is poorly worded, I'm not sure how to phrase it. I have a function that basically iterates over the 2nd dimension of a 2 dimensional iterable. The following is a simple reproduction:
words = ['ACGT', 'TCGA']
def make_lists():
for i in range(len(words[0])):
iter_ = iter([word[i] for word in words])
yield iter_
lists = list(make_lists())
for list_ in lists:
print(list(list_))
Running this outputs:
['A', 'T']
['C', 'C']
['G', 'G']
['T', 'A']
I would like to yield generators instead of having to evaluate words, in case words is very long, so I tried the following:
words = ['ACGT', 'TCGA']
def make_generators():
for i in range(len(words[0])):
gen = (word[i] for word in words)
yield gen
generators = list(make_iterator())
for gen in generators:
print(list(gen))
However, running outputs:
['T', 'A']
['T', 'A']
['T', 'A']
['T', 'A']
I'm not sure exactly what's happening. I suspect it has something to do with the generator comprehension not closing over its scope when yielded, so they're all sharing. If I create the generators inside a separate function and yield the return from that function it seems to work.
i is a free variable for those generators now, and they are now going to use its last value, i.e 3. In simple words, they know from where they are supposed to fetch the value of i but are not aware of actual value of i when they were created. So, something like this:
def make_iterator():
for i in range(len(words[0])):
gen = (word[i] for word in words)
yield gen
i = 0 # Modified the value of i
will result in:
['A', 'T']
['A', 'T']
['A', 'T']
['A', 'T']
Generator expressions are implemented as function scope, on the other hand a list comprehension runs right away and can fetch the value of i during that iteration itself.(Well list comprehensions are implemented as function scope in Python 3, but the difference is that they are not lazy)
A fix will be to use a inner function that captures the actual value of i in each loop using a default argument value:
words = ['ACGT', 'TCGA']
def make_iterator():
for i in range(len(words[0])):
# default argument value is calculated at the time of
# function creation, hence for each generator it is going
# to be the value at the time of that particular iteration
def inner(i=i):
return (word[i] for word in words)
yield inner()
generators = list(make_iterator())
for gen in generators:
print(list(gen))
You may also want to read:
What do (lambda) function closures capture?
Python internals: Symbol tables, part 1

how to correctly modify the iterator of a loop in python from within the loop

what I basically need is to check every element of a list and if some criteria fit I want to remove it from the list.
So for example let's say that
list=['a','b','c','d','e']
I basically want to write (in principle and not the actual code I try to implement)
If an element of the list is 'b' or 'c' remove it from the list and take the next.
But
for s in list:
if s=='b' or s=='c':
list.remove(s)
fails because when 'b' is removed the loop takes 'd' and not 'c' as the next element. So is there a way to do that faster than storing the elements in a separate list and removing them afterwards?
Thanks.
The easier way is to use a copy of the list - it can be done with a slice that extends "from the beginning" to the "end" of the list, like this:
for s in list[:]:
if s=='b' or s=='c':
list.remove(s)
You have considered this, and this is simple enough to be in your code, unless this list is really big, and in a critical part of the code (like, in the main loop of an action game). In that case, I sometimes use the following idiom:
to_remove = []
for index, s in enumerate(list):
if s == "b" or s == "c":
to_remove.append(index)
for index in reversed(to_remove):
del list[index]
Of course you can resort to a while loop instead:
index = 0
while index < len(list):
if s == "b" or s == "c":
del list[index]
continue
index += 1
Its better not to reinvent things which are already available. Use filter functions and lambda in these cases. Its more pythonic and looks cleaner.
filter(lambda x:x not in ['b','c'],['a','b','c','d','e'])
alternatively you can use list comprehension
[x for x in ['a','b','c','d','e'] if x not in ['b','c']]
This is exactly what itertools.ifilter is designed for.
from itertools import ifilter
ifilter(lambda x: x not in ['b', 'c'], ['a', 'b', 'c', 'd', 'e'])
will give you back a generator for your list. If you actually need a list, you can create it using one of the standard techniques for converting a generator to a list:
list(ifilter(lambda x: x not in ['b', 'c'], ['a', 'b', 'c', 'd', 'e']))
or
[x for x in ifilter(lambda x: x not in ['b', 'c'], ['a', 'b', 'c', 'd', 'e'])]
If you are ok with creating a copy of the list you can do it like this (list comprehension):
[s for s in list if s != 'b' and s != 'c']

Categories