Generator comprehension using another generator - python

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

Related

insert element into list, move pre-existing elements

I'm trying to create a defined function (without list/string methods):
so far i have code to update at the given location, but I'm struggling to create code that can insert the variable at list location, and move existing elements over. I want the output to be ['a', 'b', 'c', 'd'] instead of ['a', 'b', 'c', 'e'].
Please note: this is for study, I do want to learn I'd prefer to be pointed in the right direction, not just provided the answer.
def insert(list1, var, i):
counter = 0
for i in my_list:
if counter == i:
my_list[i] = var
counter += 1
return list1
list = ['a', 'b', 'd', 'e']
func = insert(list, 'c', 2)
i've been working on code overnight (sorry for such a late reply i was on a plane)
I've come up with this code and I think it works, even though it's a little crude. can anyone give some input as to whether they see this being an issue or not?
def insert(list1, var, i):
new_list = []
counter = -1
for index in (list1[:-1]):
counter += 1
new_list.append(index)
if counter == (i-1):
new_list.append(var)
return new_list
list2= ['a', 'b', 'c', 'd', 'e', 'f', 'h']
func = insert(list2, 'g', 6)
print(func)
Using slicing, it's fairly easy to do this without use of any list methods at all. If you're using Python 3.5+ with additional unpacking generalizations, it even looks pretty:
def insert(list1, var, i):
# Reassign all elements at and beyond the insertion point to be the new
# value, and all but the last value previously there (so size is unchanged)
list1[i:] = var, *list1[i:-1]
or if mutating the caller's argument in place is bad, and you want to return a new value:
def insert(list1, var, i):
# Unpacking the components sliced as before to a brand new list
return [*list1[:i], var, *list1[i:-1]]
Without the unpacking generalizations, it's not quite as pretty, but still doable, either the argument mutating:
def insert(list1, var, i):
# Uglier without unpacking generalizations
list1[i], list1[i+1:] = var, list1[i:-1]
or new list constructing:
def insert(list1, var, i):
# An extra temporary without unpacking generalizations, but not ugly
return list1[:i] + [var] + list1[i:-1]
If you can slice or index, this won't help, but I wanted to point out the power of slice assignment and the new unpacking generalizations.
If you want a solution using only range and/or .append with no slicing:
def insert(list1, var, i):
for idx in range(len(list1)-1, i, -1):
list1[idx] = list1[idx-1]
list1[i] = var
or non-mutating:
def insert(list1, var, i):
newlist = []
for idx in range(i):
newlist.append(list1[idx])
newlist.append(var)
for idx in range(i, len(list1)-1):
newlist.append(list1[idx])
return newlist
def insert(list1, var, i):
new_list = list1[:i]
new_list.append(var)
new_list.extend(list1[i:])
return new_list
list = ['a', 'b', 'd', 'e']
func = insert(list, 'c', 2)
take the part of list1 up to i. This is called slicing
tack var onto new_list with append
tack on the rest of the list with extend
['a', 'b', 'c', 'd', 'e']

How to return values without breaking the loop? [duplicate]

This question already has answers here:
How can I use `return` to get back multiple values from a loop? Can I put them in a list?
(2 answers)
Closed 4 months ago.
I want to know how to return values without breaking a loop in Python.
Here is an example:
def myfunction():
list = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(list)
total = 0
for i in list:
if total < 6:
return i #get first element then it breaks
total += 1
else:
break
myfunction()
return will only get the first answer then leave the loop, I don't want that, I want to return multiple elements till the end of that loop.
How can resolve this, is there any solution?
You can create a generator for that, so you could yield values from your generator (your function would become a generator after using the yield statement).
See the topics below to get a better idea of how to work with it:
Generators
What does the "yield" keyword do?
yield and Generators explain's
An example of using a generator:
def simple_generator(n):
i = 0
while i < n:
yield i
i += 1
my_simple_gen = simple_generator(3) # Create a generator
first_return = next(my_simple_gen) # 0
second_return = next(my_simple_gen) # 1
Also you could create a list before the loop starts and append items to that list, then return that list, so this list could be treated as list of results "returned" inside the loop.
Example of using list to return values:
def get_results_list(n):
results = []
i = 0
while i < n:
results.append(i)
i += 1
return results
first_return, second_return, third_return = get_results_list(3)
NOTE: In the approach with list you have to know how many values your function would return in results list to avoid too many values to unpack error
Using a generator is a probable way:
def myfunction():
l = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
total = 0
for i in l:
if total < 6:
yield i #yields an item and saves function state
total += 1
else:
break
g = myfunction()
Now you can access all elements returned with yield i by calling next() on it:
>>> val = next(g)
>>> print(v)
a
>>> v = next(g)
>>> print(v)
b
Or, in a for loop by doing:
>>> for returned_val in myfunction():
... print(returned_val)
a
b
c
d
e
f
What you want is most easily expressed with list slicing:
>>> l = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
>>> l[:6]
# ['a', 'b', 'c', 'd', 'e', 'f']
Alternatively create another list which you will return at the end of the function.
def myfunction():
l = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
ret = []
total = 0
for i in l:
if total < 6:
total += 1
ret.append(i)
return ret
myfunction()
# ['a', 'b', 'c', 'd', 'e', 'f']
A yield statement to create a generator is what you want.
What does the "yield" keyword do in Python?
Then use the next method to get each value returned from the loop.
var = my_func().next()

Accessing a repeated element in the list and printing the element next to it

I have this function, which takes 3 arguments.
1) a list containing strings, 2) search_term and 3) place (optional argument).
Code:
def ls_src(list,search_term,place=1):
if search_term in list:
a = list[list.index(search_term)+1]+':' + '\tThe second element of of search term is ' + (search_term[place])
return a
Now I want to access the element next to search_term, but if the element is repeated in the list, it should also consider the other occurrences of that element, and not the first occurrence of the element only.
If list_search(['a','b','c','a','e'],'a')
then, the function should return 'b' and 'e' both, as they are the elements next to 'a'.
So my question is, how do we access the other occurrences of 'a', and not just the first occurrence.
You need to use enumerate function which helps to get the element along with it's index.
def list_search(l, s):
for i,j in enumerate(l):
if j == s:
print(l[i+1])
list_search(['a','b','c','a','e'],'a')
Output:
b
e
or
There may be a chance of search element also be present at the last, so put the print statement inside try except block.
def list_search(l, s):
for i,j in enumerate(l):
if j == s:
try:
print(l[i+1])
except IndexError:
pass
list_search(['a','b','c','a','e', 'a'],'a')
If you prefer code that is more descriptive you could take an approach like this. It's a little longer but you avoid one character variables.
The other aspect this provides is if the query string follows itself, it won't be returned. That can be changed by removing the last if test.
def search_terms(terms, query):
found = []
count = len(terms)
for index, term in enumerate(terms):
next_index = index + 1
if term == query and next_index < count and terms[next_index] != query:
found.append(terms[next_index])
return found
print search_terms(['a', 'a', 'b', 'c', 'a', 'e', 'a'], 'a')
# ['b', 'e']
You could build the new list using an iterator and the next() function.
def list_search(input_list, search_term, place=1):
terms = iter(input_list)
new_list = []
try:
[new_list.append(terms.next()) for term in terms if term == search_term[place-1]]
except StopIteration:
pass
return new_list
tests = [
(['a','b','c','a','e'], 'a', 1),
(['a','b','c','a','e'], 'b', 1),
(['a','b','c','a','e'], 'ab', 2),
(['a','b','c','a','e'], 'e', 1),
(['a','a','a'], 'b', 1),
(['a','a','a'], 'a', 1)]
for input_list, search_term, place in tests:
print list_search(input_list, search_term, place)
These tests will give you the following results:
['b', 'e']
['c']
['c']
[]
[]
['a']
Code:
def search(l,term):
for num in range(len(l)):
if l[num]==term:
print (l[num+1])
search(['python','html','python','c++','python','java'], 'python')
Output:
html
c++
java

How can list[n] point to list[0]? Getting items not in sequence

I'm not sure about the term of this technique. I want to create a list where if I try to access an element outside the list range, the list would "loop itself". Example of the behaviour I want to achieve:
>>> musical_scale = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
>>> musical_scale[2]
E
>>> musical_scale[7]
C
>>> musical_scale[-1]
B
I guess I could write a class which does this, but I thought there might be a more correct way of doing it.
Creating a subclass of List would be a pretty useful way of doing this. Something like this, perhaps:
class modList(list):
def __getitem__(self, i):
if len(self) == 0:
raise IndexError # Or do something else if you want, like return []
i = i % len(self) # self.__len__() works also
return super(modList, self).__getitem__(i) # In Python 3, super().__getitem__(i)
If you want to do slicing, it's a little more complicated, but similar. Found this while looking through StackOverflow:
def __getitem__(self, i):
if isinstance(i, int):
if len(self) == 0:
raise IndexError
i = i % len(self)
return super(modList, self).__getitem__(i) # list.__getitem__(i) works too
elif isinstance(i, slice):
if len(self) == 0:
return []
start = i.start % len(self)
stop = i.stop % len(self)
step = i.step
return super(modList, self).__getItem__(slice(start, stop, step))
else:
raise TypeError("invalid index")
Though this slice modding could give you a situation like [3:2], which will return an empty list. Basically slicing is hard and you'll need to decide how you want to implement it, but hopefully this is a start.
http://www.stackoverflow.com/questions/3911483/python-slice-how-to-i-know-the-python-slice-but-how-can-i-use-built-in-slice-ob
(Thanks #JonClements for all the suggestions in chat.)
EDIT: now we have some handlers for if you have an empty list. #wim suggests raising an error for single accesses, and returning [] for slices. Really it's up to you what you want to do but that seemed sensible to me, so that's what I've included in this answer.
EDIT EDIT: if you're using Python 2.x, I believe you also need to override __getslice__.
Use the modulus operator % to "loop back" after indexing past the end of the list
>>> musical_scale = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
def getValue(x, l):
return l[x % len(l)]
>>> getValue(0, musical_scale)
'C'
>>> getValue(9, musical_scale)
'E'
In case you'd like to extend the list class to handle getting and setting...
class UnsatList(list):
def __getitem__(self, index):
try:
return list.__getitem__(self, index % len(self))
except ZeroDivisionError:
raise IndexError('list assignment index out of range')
def __setitem__(self, index, value):
try:
return list.__setitem__(self, index % len(self), value)
except ZeroDivisionError:
raise IndexError('list assignment index out of range')
if __name__ == '__main__':
test_list = UnsatList([1,2,3,4])
assert test_list[0] == 1
assert test_list[4] == 1
assert test_list[8] == 1
assert test_list[-8] == 1
test_list[4] = 5
assert test_list[0] == 5
assert test_list[4] == 5
assert test_list[8] == 5
assert test_list[-8] == 5
Here is a solution, but remember The Zen of Python: Flat is better than nested.
You can write for example:
a = [1, [2]]
a[1].append(a)
Then your list a has "infinite depth", and you can loop on these two elements by repeatedly doing:
a[0]
a = a[1]
You can't do that without playing with depth, because unlike e.g. Lisp, Python lists are varying-length arrays, not linked-lists.
Here are little functions to do the whole stuff automatically:
def loopify(a):
r = b = []
for x in a:
b.append(x)
b.append([])
c = b
b = b[1]
c[1] = r
return r
def loopgen(a):
while True:
yield a[0]
a = a[1]
Then with your example:
musical_scale = loopify(['C', 'D', 'E', 'F', 'G', 'A', 'B'])
g = loopgen(musical_scale)
[next(g) for i in range(20)]
This yields:
['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'A']
Of course, it's also possible to use mod or to keep track of the current index and reseting it to 0 when you overflow your list. Other answers show you how to do that.
Also notice that the linked-list approach does not permit random access, only sequential access (thus the generator above). If you need to access you list randomly, modular arithmetic is the most appropriate way.

making calls to iter and next when iterating through a generator

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??'

Categories