Unexpected behaviour with Python generator
I was running a piece of code that unexpectedly gave a logic error at one part of the program. When investigating the section, I created a test file to test the set of statements being run and found out an unusual bug that seems very odd.
I tested this simple code:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs filtered
And the output was:
>>>
Yes, nothing. I was expecting the filter comprehension to get items in the array with a count of 2 and output this, but I didn't get that:
# Expected output
>>> [2, 2]
When I commented out the third line to test it once again:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line
print(list(f)) # Outputs filtered
The output was correct (you can test it for yourself):
>>> [2, 2]
At one point I outputted the type of the variable 'f':
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original
print(type(f))
print(list(f)) # Outputs filtered
And I got:
>>> <class 'generator'>
>>>
TL;DR: Why is updating a list in Python changing the output of another generator variable? This seems very odd to me.
python
|
show 1 more comment
I was running a piece of code that unexpectedly gave a logic error at one part of the program. When investigating the section, I created a test file to test the set of statements being run and found out an unusual bug that seems very odd.
I tested this simple code:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs filtered
And the output was:
>>>
Yes, nothing. I was expecting the filter comprehension to get items in the array with a count of 2 and output this, but I didn't get that:
# Expected output
>>> [2, 2]
When I commented out the third line to test it once again:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line
print(list(f)) # Outputs filtered
The output was correct (you can test it for yourself):
>>> [2, 2]
At one point I outputted the type of the variable 'f':
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original
print(type(f))
print(list(f)) # Outputs filtered
And I got:
>>> <class 'generator'>
>>>
TL;DR: Why is updating a list in Python changing the output of another generator variable? This seems very odd to me.
python
2
You keep printinglist(g)
, butg
is an undefined symbol. Perhaps you have a typo, usingg
forf
?
– Prune
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
3
You redefinearray
and your new array is what gets referenced by the lazy generator comprehension.
– jpp
16 hours ago
2
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
2
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago
|
show 1 more comment
I was running a piece of code that unexpectedly gave a logic error at one part of the program. When investigating the section, I created a test file to test the set of statements being run and found out an unusual bug that seems very odd.
I tested this simple code:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs filtered
And the output was:
>>>
Yes, nothing. I was expecting the filter comprehension to get items in the array with a count of 2 and output this, but I didn't get that:
# Expected output
>>> [2, 2]
When I commented out the third line to test it once again:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line
print(list(f)) # Outputs filtered
The output was correct (you can test it for yourself):
>>> [2, 2]
At one point I outputted the type of the variable 'f':
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original
print(type(f))
print(list(f)) # Outputs filtered
And I got:
>>> <class 'generator'>
>>>
TL;DR: Why is updating a list in Python changing the output of another generator variable? This seems very odd to me.
python
I was running a piece of code that unexpectedly gave a logic error at one part of the program. When investigating the section, I created a test file to test the set of statements being run and found out an unusual bug that seems very odd.
I tested this simple code:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs filtered
And the output was:
>>>
Yes, nothing. I was expecting the filter comprehension to get items in the array with a count of 2 and output this, but I didn't get that:
# Expected output
>>> [2, 2]
When I commented out the third line to test it once again:
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line
print(list(f)) # Outputs filtered
The output was correct (you can test it for yourself):
>>> [2, 2]
At one point I outputted the type of the variable 'f':
array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original
print(type(f))
print(list(f)) # Outputs filtered
And I got:
>>> <class 'generator'>
>>>
TL;DR: Why is updating a list in Python changing the output of another generator variable? This seems very odd to me.
python
python
edited 8 hours ago
Ian Kemp
16.6k126798
16.6k126798
asked 16 hours ago
Suraj KothariSuraj Kothari
509214
509214
2
You keep printinglist(g)
, butg
is an undefined symbol. Perhaps you have a typo, usingg
forf
?
– Prune
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
3
You redefinearray
and your new array is what gets referenced by the lazy generator comprehension.
– jpp
16 hours ago
2
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
2
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago
|
show 1 more comment
2
You keep printinglist(g)
, butg
is an undefined symbol. Perhaps you have a typo, usingg
forf
?
– Prune
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
3
You redefinearray
and your new array is what gets referenced by the lazy generator comprehension.
– jpp
16 hours ago
2
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
2
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago
2
2
You keep printing
list(g)
, but g
is an undefined symbol. Perhaps you have a typo, using g
for f
?– Prune
16 hours ago
You keep printing
list(g)
, but g
is an undefined symbol. Perhaps you have a typo, using g
for f
?– Prune
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
3
3
You redefine
array
and your new array is what gets referenced by the lazy generator comprehension.– jpp
16 hours ago
You redefine
array
and your new array is what gets referenced by the lazy generator comprehension.– jpp
16 hours ago
2
2
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
2
2
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago
|
show 1 more comment
8 Answers
8
active
oldest
votes
Pythons generator expressions are late binding (see PEP 289 -- Generator Expressions) (what the other answers call "lazy"):
Early Binding versus Late Binding
After much discussion, it was decided that the first (outermost) for-expression [of the generator expression] should be evaluated immediately and that the remaining expressions be evaluated when the generator is executed.
[...]
However, Python takes a late binding approach to lambda expressions and has no precedent for automatic, early binding. It was felt that introducing a new paradigm would unnecessarily introduce complexity.
After exploring many possibilities, a consensus emerged that binding issues were hard to understand and that users should be strongly encouraged to use generator expressions inside functions that consume their arguments immediately. For more complex applications, full generator definitions are always superior in terms of being obvious about scope, lifetime, and binding.
That means it only evaluates the outermost for
, so it actually binds the value with the name array
in the "subexpression" in array
. But when you iterate over the generator the if array.count
call actually refers to what is currently named array
.
Since it's actually a list
not an array
I changed the variable names in the rest of the answer to be more accurate.
So in your first case the list
you iterate over and the list
you count in will be different. It's as if you used:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
So you check for each element in list1
if it's present twice in list2
.
You can easily verify this by modifying the second list:
>>> lst = [1, 2, 2, 4, 5]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2, 3, 4]
>>> list(f)
[1]
If it iterated over the second list the output should be [1, 1]
but since it iterates over the first list (containing one 1
) but checks the second list (which contains two 1
s) the output is just a single 1
.
Solution using a generator function
There are several possible solutions, I generally prefer not to use "generator expressions" if they aren't iterated over immediately. A simple generator function (as recommended by the PEP - see above) will suffice to make it work correctly:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
And then use it like this:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
A better Solution using a generator function with a Counter
A better solution (avoiding the quadratic runtime behavior because you iterate over the whole array for each element in the array) would be to count (collections.Counter
) the elements once and then do the lookup in constant time (resulting in linear time):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be[2,2]
, if it would only look at the second list the result would be[1, 1]
. That the result is[1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?
– MSeifert
2 hours ago
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
|
show 1 more comment
As others have mentioned Python generators are lazy. When this line is run:
f = (x for x in array if array.count(x) == 2) # Filters original
nothing actually happens yet. You've just declared how the generator function f will work. Array is not looked at yet. Then, you create a new array that replaces the first one, and finally when you call
print(list(f)) # Outputs filtered
the generator now needs the actual values and starts pulling them from the generator f. But at this point, array already refers to the second one, so you get an empty list.
If you need to reassign the list, and can't use a different variable to hold it, consider creating the list instead of a generator in the second line:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explainsarray
inin array
is bound immediately butarray
inarray.count
only later. You could also try to explain tio.run/…
– hkBst
3 hours ago
add a comment |
Others have already explained the root cause of the issue - the generator is binding to the name of the array
local variable, rather than its value.
The most pythonic solution is definitely the list comprehension:
f = [x for x in array if array.count(x) == 2]
However, if there is some reason that you don't want to create a list, you can also force a scope close over array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
What's happening here is that the lambda
captures the reference to array
at the time the line is run, ensuring that the generator sees the variable you expect, even if the variable is later redefined.
Note that this still binds to the variable (reference), not the value, so, for example, the following will print [2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
This is a common pattern in some languages, but it's not very pythonic, so only really makes sense if there's a very good reason for not using the list comprehension (e.g., if array
is very long, or is being used in a nested generator comprehension, and you're concerned about memory).
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
add a comment |
You are not using a generator correctly if this is the primary use of this code. Use a list comprehension instead of a generator comprehension. Just replace the parentheses with brackets. It evaluates to a list if you don't know.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
You are getting this response because of the nature of a generator. You're calling the generator when it't contents will evaluate to
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,list(f)
becomes redundant.
– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
add a comment |
Generators are lazy, they won't be evaluated until you iterate through them. In this case that's at the point you create the list
with the generator as input, at the print
.
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create thelist
it will iterate for you without you needing to do it explicitly.
– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
|
show 1 more comment
The root cause of the problem is that generators are lazy; variables are evaluated each time:
>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
It iterates over the original list and evaluates the condition with the current list. In this case, 4 appeared twice in the new list, causing it to appear in the result. It only appears once in the result because it only appeared once in the original list. The 6s appear twice in the new list, but never appear in the old list and are hence never shown.
Full function introspection for the curious (the line with the comment is the important line):
>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
current = original
filtered = (x for x in current if current.count(x) == count)
current = new
return list(filtered)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_FAST 0 (original)
3 STORE_DEREF 1 (current)
3 6 LOAD_CLOSURE 0 (count)
9 LOAD_CLOSURE 1 (current)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
18 LOAD_CONST 2 ('f.<locals>.<genexpr>')
21 MAKE_CLOSURE 0
24 LOAD_DEREF 1 (current)
27 GET_ITER
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 STORE_FAST 3 (filtered)
4 34 LOAD_FAST 1 (new)
37 STORE_DEREF 1 (current)
5 40 LOAD_GLOBAL 0 (list)
43 LOAD_FAST 3 (filtered)
46 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
3 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 32 (to 38)
6 STORE_FAST 1 (x)
9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant.
12 LOAD_ATTR 0 (count)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 LOAD_DEREF 0 (count)
24 COMPARE_OP 2 (==)
27 POP_JUMP_IF_FALSE 3
30 LOAD_FAST 1 (x)
33 YIELD_VALUE
34 POP_TOP
35 JUMP_ABSOLUTE 3
>> 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)
To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.
The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.
add a comment |
Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:
Look again at your output with the type of f
: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.
Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.
Code to "make it work"
That depends on what you mean by "make it work". If you want f
to be a filtered list, then use a list, not a generator:
f = [x for x in array if array.count(x) == 2] # Filters original
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
add a comment |
Generators are lazy and your newly defined array
is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses ()
by brackets .
Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter
to count values, and keep a copy of your original list:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Notice the second version doesn't even require old_array
and is useful if there is no need to maintain ordering of values in your original array.
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54245618%2funexpected-behaviour-with-python-generator%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
8 Answers
8
active
oldest
votes
8 Answers
8
active
oldest
votes
active
oldest
votes
active
oldest
votes
Pythons generator expressions are late binding (see PEP 289 -- Generator Expressions) (what the other answers call "lazy"):
Early Binding versus Late Binding
After much discussion, it was decided that the first (outermost) for-expression [of the generator expression] should be evaluated immediately and that the remaining expressions be evaluated when the generator is executed.
[...]
However, Python takes a late binding approach to lambda expressions and has no precedent for automatic, early binding. It was felt that introducing a new paradigm would unnecessarily introduce complexity.
After exploring many possibilities, a consensus emerged that binding issues were hard to understand and that users should be strongly encouraged to use generator expressions inside functions that consume their arguments immediately. For more complex applications, full generator definitions are always superior in terms of being obvious about scope, lifetime, and binding.
That means it only evaluates the outermost for
, so it actually binds the value with the name array
in the "subexpression" in array
. But when you iterate over the generator the if array.count
call actually refers to what is currently named array
.
Since it's actually a list
not an array
I changed the variable names in the rest of the answer to be more accurate.
So in your first case the list
you iterate over and the list
you count in will be different. It's as if you used:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
So you check for each element in list1
if it's present twice in list2
.
You can easily verify this by modifying the second list:
>>> lst = [1, 2, 2, 4, 5]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2, 3, 4]
>>> list(f)
[1]
If it iterated over the second list the output should be [1, 1]
but since it iterates over the first list (containing one 1
) but checks the second list (which contains two 1
s) the output is just a single 1
.
Solution using a generator function
There are several possible solutions, I generally prefer not to use "generator expressions" if they aren't iterated over immediately. A simple generator function (as recommended by the PEP - see above) will suffice to make it work correctly:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
And then use it like this:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
A better Solution using a generator function with a Counter
A better solution (avoiding the quadratic runtime behavior because you iterate over the whole array for each element in the array) would be to count (collections.Counter
) the elements once and then do the lookup in constant time (resulting in linear time):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be[2,2]
, if it would only look at the second list the result would be[1, 1]
. That the result is[1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?
– MSeifert
2 hours ago
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
|
show 1 more comment
Pythons generator expressions are late binding (see PEP 289 -- Generator Expressions) (what the other answers call "lazy"):
Early Binding versus Late Binding
After much discussion, it was decided that the first (outermost) for-expression [of the generator expression] should be evaluated immediately and that the remaining expressions be evaluated when the generator is executed.
[...]
However, Python takes a late binding approach to lambda expressions and has no precedent for automatic, early binding. It was felt that introducing a new paradigm would unnecessarily introduce complexity.
After exploring many possibilities, a consensus emerged that binding issues were hard to understand and that users should be strongly encouraged to use generator expressions inside functions that consume their arguments immediately. For more complex applications, full generator definitions are always superior in terms of being obvious about scope, lifetime, and binding.
That means it only evaluates the outermost for
, so it actually binds the value with the name array
in the "subexpression" in array
. But when you iterate over the generator the if array.count
call actually refers to what is currently named array
.
Since it's actually a list
not an array
I changed the variable names in the rest of the answer to be more accurate.
So in your first case the list
you iterate over and the list
you count in will be different. It's as if you used:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
So you check for each element in list1
if it's present twice in list2
.
You can easily verify this by modifying the second list:
>>> lst = [1, 2, 2, 4, 5]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2, 3, 4]
>>> list(f)
[1]
If it iterated over the second list the output should be [1, 1]
but since it iterates over the first list (containing one 1
) but checks the second list (which contains two 1
s) the output is just a single 1
.
Solution using a generator function
There are several possible solutions, I generally prefer not to use "generator expressions" if they aren't iterated over immediately. A simple generator function (as recommended by the PEP - see above) will suffice to make it work correctly:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
And then use it like this:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
A better Solution using a generator function with a Counter
A better solution (avoiding the quadratic runtime behavior because you iterate over the whole array for each element in the array) would be to count (collections.Counter
) the elements once and then do the lookup in constant time (resulting in linear time):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be[2,2]
, if it would only look at the second list the result would be[1, 1]
. That the result is[1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?
– MSeifert
2 hours ago
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
|
show 1 more comment
Pythons generator expressions are late binding (see PEP 289 -- Generator Expressions) (what the other answers call "lazy"):
Early Binding versus Late Binding
After much discussion, it was decided that the first (outermost) for-expression [of the generator expression] should be evaluated immediately and that the remaining expressions be evaluated when the generator is executed.
[...]
However, Python takes a late binding approach to lambda expressions and has no precedent for automatic, early binding. It was felt that introducing a new paradigm would unnecessarily introduce complexity.
After exploring many possibilities, a consensus emerged that binding issues were hard to understand and that users should be strongly encouraged to use generator expressions inside functions that consume their arguments immediately. For more complex applications, full generator definitions are always superior in terms of being obvious about scope, lifetime, and binding.
That means it only evaluates the outermost for
, so it actually binds the value with the name array
in the "subexpression" in array
. But when you iterate over the generator the if array.count
call actually refers to what is currently named array
.
Since it's actually a list
not an array
I changed the variable names in the rest of the answer to be more accurate.
So in your first case the list
you iterate over and the list
you count in will be different. It's as if you used:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
So you check for each element in list1
if it's present twice in list2
.
You can easily verify this by modifying the second list:
>>> lst = [1, 2, 2, 4, 5]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2, 3, 4]
>>> list(f)
[1]
If it iterated over the second list the output should be [1, 1]
but since it iterates over the first list (containing one 1
) but checks the second list (which contains two 1
s) the output is just a single 1
.
Solution using a generator function
There are several possible solutions, I generally prefer not to use "generator expressions" if they aren't iterated over immediately. A simple generator function (as recommended by the PEP - see above) will suffice to make it work correctly:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
And then use it like this:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
A better Solution using a generator function with a Counter
A better solution (avoiding the quadratic runtime behavior because you iterate over the whole array for each element in the array) would be to count (collections.Counter
) the elements once and then do the lookup in constant time (resulting in linear time):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
Pythons generator expressions are late binding (see PEP 289 -- Generator Expressions) (what the other answers call "lazy"):
Early Binding versus Late Binding
After much discussion, it was decided that the first (outermost) for-expression [of the generator expression] should be evaluated immediately and that the remaining expressions be evaluated when the generator is executed.
[...]
However, Python takes a late binding approach to lambda expressions and has no precedent for automatic, early binding. It was felt that introducing a new paradigm would unnecessarily introduce complexity.
After exploring many possibilities, a consensus emerged that binding issues were hard to understand and that users should be strongly encouraged to use generator expressions inside functions that consume their arguments immediately. For more complex applications, full generator definitions are always superior in terms of being obvious about scope, lifetime, and binding.
That means it only evaluates the outermost for
, so it actually binds the value with the name array
in the "subexpression" in array
. But when you iterate over the generator the if array.count
call actually refers to what is currently named array
.
Since it's actually a list
not an array
I changed the variable names in the rest of the answer to be more accurate.
So in your first case the list
you iterate over and the list
you count in will be different. It's as if you used:
list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)
So you check for each element in list1
if it's present twice in list2
.
You can easily verify this by modifying the second list:
>>> lst = [1, 2, 2, 4, 5]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2, 3, 4]
>>> list(f)
[1]
If it iterated over the second list the output should be [1, 1]
but since it iterates over the first list (containing one 1
) but checks the second list (which contains two 1
s) the output is just a single 1
.
Solution using a generator function
There are several possible solutions, I generally prefer not to use "generator expressions" if they aren't iterated over immediately. A simple generator function (as recommended by the PEP - see above) will suffice to make it work correctly:
def keep_only_duplicated_items(lst):
for item in lst:
if lst.count(item) == 2:
yield item
And then use it like this:
lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]
>>> list(f)
[2, 2]
A better Solution using a generator function with a Counter
A better solution (avoiding the quadratic runtime behavior because you iterate over the whole array for each element in the array) would be to count (collections.Counter
) the elements once and then do the lookup in constant time (resulting in linear time):
from collections import Counter
def keep_only_duplicated_items(lst):
cnts = Counter(lst)
for item in lst:
if cnts[item] == 2:
yield item
edited 7 hours ago
answered 7 hours ago
MSeifertMSeifert
74k17140175
74k17140175
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be[2,2]
, if it would only look at the second list the result would be[1, 1]
. That the result is[1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?
– MSeifert
2 hours ago
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
|
show 1 more comment
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be[2,2]
, if it would only look at the second list the result would be[1, 1]
. That the result is[1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?
– MSeifert
2 hours ago
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
1
1
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
This is the only answer that explains all the subtleties involved in the questioned behavior.
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
Your example as given (with result [1]) might only look at the second list. It would be even better if you used something like [1, 1, 2, 2, 3, 4, 5] and [1, 2, 2, 3, 3, 4, 6], with result [2, 2, 3].
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
See for example tio.run/…
– hkBst
3 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be
[2,2]
, if it would only look at the second list the result would be [1, 1]
. That the result is [1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?– MSeifert
2 hours ago
@hkBst Thank you for the additional example. But I'm not sure what you mean with my example being ambguous. I thought in case it would look only at the first list the result would be
[2,2]
, if it would only look at the second list the result would be [1, 1]
. That the result is [1]
shows that it iterates over the first list, but filters based on the second list. Is my thinking incorrect there?– MSeifert
2 hours ago
1
1
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
Wow, that's about as counter-intuitive as it gets. Usually Python is easier to explain than that.
– Mark Ransom
43 mins ago
|
show 1 more comment
As others have mentioned Python generators are lazy. When this line is run:
f = (x for x in array if array.count(x) == 2) # Filters original
nothing actually happens yet. You've just declared how the generator function f will work. Array is not looked at yet. Then, you create a new array that replaces the first one, and finally when you call
print(list(f)) # Outputs filtered
the generator now needs the actual values and starts pulling them from the generator f. But at this point, array already refers to the second one, so you get an empty list.
If you need to reassign the list, and can't use a different variable to hold it, consider creating the list instead of a generator in the second line:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explainsarray
inin array
is bound immediately butarray
inarray.count
only later. You could also try to explain tio.run/…
– hkBst
3 hours ago
add a comment |
As others have mentioned Python generators are lazy. When this line is run:
f = (x for x in array if array.count(x) == 2) # Filters original
nothing actually happens yet. You've just declared how the generator function f will work. Array is not looked at yet. Then, you create a new array that replaces the first one, and finally when you call
print(list(f)) # Outputs filtered
the generator now needs the actual values and starts pulling them from the generator f. But at this point, array already refers to the second one, so you get an empty list.
If you need to reassign the list, and can't use a different variable to hold it, consider creating the list instead of a generator in the second line:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explainsarray
inin array
is bound immediately butarray
inarray.count
only later. You could also try to explain tio.run/…
– hkBst
3 hours ago
add a comment |
As others have mentioned Python generators are lazy. When this line is run:
f = (x for x in array if array.count(x) == 2) # Filters original
nothing actually happens yet. You've just declared how the generator function f will work. Array is not looked at yet. Then, you create a new array that replaces the first one, and finally when you call
print(list(f)) # Outputs filtered
the generator now needs the actual values and starts pulling them from the generator f. But at this point, array already refers to the second one, so you get an empty list.
If you need to reassign the list, and can't use a different variable to hold it, consider creating the list instead of a generator in the second line:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
As others have mentioned Python generators are lazy. When this line is run:
f = (x for x in array if array.count(x) == 2) # Filters original
nothing actually happens yet. You've just declared how the generator function f will work. Array is not looked at yet. Then, you create a new array that replaces the first one, and finally when you call
print(list(f)) # Outputs filtered
the generator now needs the actual values and starts pulling them from the generator f. But at this point, array already refers to the second one, so you get an empty list.
If you need to reassign the list, and can't use a different variable to hold it, consider creating the list instead of a generator in the second line:
f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
edited 16 hours ago
answered 16 hours ago
StevenSteven
1708
1708
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explainsarray
inin array
is bound immediately butarray
inarray.count
only later. You could also try to explain tio.run/…
– hkBst
3 hours ago
add a comment |
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explainsarray
inin array
is bound immediately butarray
inarray.count
only later. You could also try to explain tio.run/…
– hkBst
3 hours ago
4
4
This is incorrect. As stackoverflow.com/a/54249614/5600363 explains
array
in in array
is bound immediately but array
in array.count
only later. You could also try to explain tio.run/…– hkBst
3 hours ago
This is incorrect. As stackoverflow.com/a/54249614/5600363 explains
array
in in array
is bound immediately but array
in array.count
only later. You could also try to explain tio.run/…– hkBst
3 hours ago
add a comment |
Others have already explained the root cause of the issue - the generator is binding to the name of the array
local variable, rather than its value.
The most pythonic solution is definitely the list comprehension:
f = [x for x in array if array.count(x) == 2]
However, if there is some reason that you don't want to create a list, you can also force a scope close over array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
What's happening here is that the lambda
captures the reference to array
at the time the line is run, ensuring that the generator sees the variable you expect, even if the variable is later redefined.
Note that this still binds to the variable (reference), not the value, so, for example, the following will print [2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
This is a common pattern in some languages, but it's not very pythonic, so only really makes sense if there's a very good reason for not using the list comprehension (e.g., if array
is very long, or is being used in a nested generator comprehension, and you're concerned about memory).
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
add a comment |
Others have already explained the root cause of the issue - the generator is binding to the name of the array
local variable, rather than its value.
The most pythonic solution is definitely the list comprehension:
f = [x for x in array if array.count(x) == 2]
However, if there is some reason that you don't want to create a list, you can also force a scope close over array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
What's happening here is that the lambda
captures the reference to array
at the time the line is run, ensuring that the generator sees the variable you expect, even if the variable is later redefined.
Note that this still binds to the variable (reference), not the value, so, for example, the following will print [2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
This is a common pattern in some languages, but it's not very pythonic, so only really makes sense if there's a very good reason for not using the list comprehension (e.g., if array
is very long, or is being used in a nested generator comprehension, and you're concerned about memory).
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
add a comment |
Others have already explained the root cause of the issue - the generator is binding to the name of the array
local variable, rather than its value.
The most pythonic solution is definitely the list comprehension:
f = [x for x in array if array.count(x) == 2]
However, if there is some reason that you don't want to create a list, you can also force a scope close over array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
What's happening here is that the lambda
captures the reference to array
at the time the line is run, ensuring that the generator sees the variable you expect, even if the variable is later redefined.
Note that this still binds to the variable (reference), not the value, so, for example, the following will print [2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
This is a common pattern in some languages, but it's not very pythonic, so only really makes sense if there's a very good reason for not using the list comprehension (e.g., if array
is very long, or is being used in a nested generator comprehension, and you're concerned about memory).
Others have already explained the root cause of the issue - the generator is binding to the name of the array
local variable, rather than its value.
The most pythonic solution is definitely the list comprehension:
f = [x for x in array if array.count(x) == 2]
However, if there is some reason that you don't want to create a list, you can also force a scope close over array
:
f = (lambda array=array: (x for x in array if array.count(x) == 2))()
What's happening here is that the lambda
captures the reference to array
at the time the line is run, ensuring that the generator sees the variable you expect, even if the variable is later redefined.
Note that this still binds to the variable (reference), not the value, so, for example, the following will print [2, 2, 4, 4]
:
array = [1, 2, 2, 4, 5] # Original array
f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4) # This *will* be captured
array = [5, 6, 1, 2, 9] # Updates original to something else
print(list(f)) # Outputs [2, 2, 4, 4]
This is a common pattern in some languages, but it's not very pythonic, so only really makes sense if there's a very good reason for not using the list comprehension (e.g., if array
is very long, or is being used in a nested generator comprehension, and you're concerned about memory).
answered 8 hours ago
sapisapi
6,62963161
6,62963161
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
add a comment |
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
Useful answer for showing how to override the default behavior!
– hkBst
3 hours ago
add a comment |
You are not using a generator correctly if this is the primary use of this code. Use a list comprehension instead of a generator comprehension. Just replace the parentheses with brackets. It evaluates to a list if you don't know.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
You are getting this response because of the nature of a generator. You're calling the generator when it't contents will evaluate to
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,list(f)
becomes redundant.
– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
add a comment |
You are not using a generator correctly if this is the primary use of this code. Use a list comprehension instead of a generator comprehension. Just replace the parentheses with brackets. It evaluates to a list if you don't know.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
You are getting this response because of the nature of a generator. You're calling the generator when it't contents will evaluate to
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,list(f)
becomes redundant.
– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
add a comment |
You are not using a generator correctly if this is the primary use of this code. Use a list comprehension instead of a generator comprehension. Just replace the parentheses with brackets. It evaluates to a list if you don't know.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
You are getting this response because of the nature of a generator. You're calling the generator when it't contents will evaluate to
You are not using a generator correctly if this is the primary use of this code. Use a list comprehension instead of a generator comprehension. Just replace the parentheses with brackets. It evaluates to a list if you don't know.
array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]
print(f)
#[2, 2]
You are getting this response because of the nature of a generator. You're calling the generator when it't contents will evaluate to
edited 16 hours ago
answered 16 hours ago
JabaJaba
6,979175394
6,979175394
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,list(f)
becomes redundant.
– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
add a comment |
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,list(f)
becomes redundant.
– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
Thank you. I seem to have used the wrong brackets. But in general using a generator comprehension seems odd.
– Suraj Kothari
16 hours ago
With your change,
list(f)
becomes redundant.– Mark Ransom
16 hours ago
With your change,
list(f)
becomes redundant.– Mark Ransom
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
Lol @Mark Ransom, copy paste got me, I edited.
– Jaba
16 hours ago
1
1
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
@SurajKothari It is not odd, it's a great tool! It just takes some time to wrap the ole brain around. Do some research you'll find that generators are amazing!
– Jaba
16 hours ago
1
1
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
This does not explain the observed behavior and so does not answer the question.
– hkBst
3 hours ago
add a comment |
Generators are lazy, they won't be evaluated until you iterate through them. In this case that's at the point you create the list
with the generator as input, at the print
.
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create thelist
it will iterate for you without you needing to do it explicitly.
– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
|
show 1 more comment
Generators are lazy, they won't be evaluated until you iterate through them. In this case that's at the point you create the list
with the generator as input, at the print
.
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create thelist
it will iterate for you without you needing to do it explicitly.
– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
|
show 1 more comment
Generators are lazy, they won't be evaluated until you iterate through them. In this case that's at the point you create the list
with the generator as input, at the print
.
Generators are lazy, they won't be evaluated until you iterate through them. In this case that's at the point you create the list
with the generator as input, at the print
.
edited 16 hours ago
answered 16 hours ago
Mark RansomMark Ransom
223k29281508
223k29281508
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create thelist
it will iterate for you without you needing to do it explicitly.
– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
|
show 1 more comment
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create thelist
it will iterate for you without you needing to do it explicitly.
– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
When am I iterating through them. Am I meant to?
– Suraj Kothari
16 hours ago
@SurajKothari when you create the
list
it will iterate for you without you needing to do it explicitly.– Mark Ransom
16 hours ago
@SurajKothari when you create the
list
it will iterate for you without you needing to do it explicitly.– Mark Ransom
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
Also which list? When I declare the first one, or re-assign the second?
– Suraj Kothari
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
What first & second? You define only one list, at the final line of your code.
– Prune
16 hours ago
1
1
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
This could have been my own answer, but it is incorrect (see MSeifert's answer) or try to explain tio.run/…
– hkBst
3 hours ago
|
show 1 more comment
The root cause of the problem is that generators are lazy; variables are evaluated each time:
>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
It iterates over the original list and evaluates the condition with the current list. In this case, 4 appeared twice in the new list, causing it to appear in the result. It only appears once in the result because it only appeared once in the original list. The 6s appear twice in the new list, but never appear in the old list and are hence never shown.
Full function introspection for the curious (the line with the comment is the important line):
>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
current = original
filtered = (x for x in current if current.count(x) == count)
current = new
return list(filtered)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_FAST 0 (original)
3 STORE_DEREF 1 (current)
3 6 LOAD_CLOSURE 0 (count)
9 LOAD_CLOSURE 1 (current)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
18 LOAD_CONST 2 ('f.<locals>.<genexpr>')
21 MAKE_CLOSURE 0
24 LOAD_DEREF 1 (current)
27 GET_ITER
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 STORE_FAST 3 (filtered)
4 34 LOAD_FAST 1 (new)
37 STORE_DEREF 1 (current)
5 40 LOAD_GLOBAL 0 (list)
43 LOAD_FAST 3 (filtered)
46 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
3 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 32 (to 38)
6 STORE_FAST 1 (x)
9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant.
12 LOAD_ATTR 0 (count)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 LOAD_DEREF 0 (count)
24 COMPARE_OP 2 (==)
27 POP_JUMP_IF_FALSE 3
30 LOAD_FAST 1 (x)
33 YIELD_VALUE
34 POP_TOP
35 JUMP_ABSOLUTE 3
>> 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)
To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.
The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.
add a comment |
The root cause of the problem is that generators are lazy; variables are evaluated each time:
>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
It iterates over the original list and evaluates the condition with the current list. In this case, 4 appeared twice in the new list, causing it to appear in the result. It only appears once in the result because it only appeared once in the original list. The 6s appear twice in the new list, but never appear in the old list and are hence never shown.
Full function introspection for the curious (the line with the comment is the important line):
>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
current = original
filtered = (x for x in current if current.count(x) == count)
current = new
return list(filtered)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_FAST 0 (original)
3 STORE_DEREF 1 (current)
3 6 LOAD_CLOSURE 0 (count)
9 LOAD_CLOSURE 1 (current)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
18 LOAD_CONST 2 ('f.<locals>.<genexpr>')
21 MAKE_CLOSURE 0
24 LOAD_DEREF 1 (current)
27 GET_ITER
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 STORE_FAST 3 (filtered)
4 34 LOAD_FAST 1 (new)
37 STORE_DEREF 1 (current)
5 40 LOAD_GLOBAL 0 (list)
43 LOAD_FAST 3 (filtered)
46 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
3 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 32 (to 38)
6 STORE_FAST 1 (x)
9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant.
12 LOAD_ATTR 0 (count)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 LOAD_DEREF 0 (count)
24 COMPARE_OP 2 (==)
27 POP_JUMP_IF_FALSE 3
30 LOAD_FAST 1 (x)
33 YIELD_VALUE
34 POP_TOP
35 JUMP_ABSOLUTE 3
>> 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)
To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.
The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.
add a comment |
The root cause of the problem is that generators are lazy; variables are evaluated each time:
>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
It iterates over the original list and evaluates the condition with the current list. In this case, 4 appeared twice in the new list, causing it to appear in the result. It only appears once in the result because it only appeared once in the original list. The 6s appear twice in the new list, but never appear in the old list and are hence never shown.
Full function introspection for the curious (the line with the comment is the important line):
>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
current = original
filtered = (x for x in current if current.count(x) == count)
current = new
return list(filtered)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_FAST 0 (original)
3 STORE_DEREF 1 (current)
3 6 LOAD_CLOSURE 0 (count)
9 LOAD_CLOSURE 1 (current)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
18 LOAD_CONST 2 ('f.<locals>.<genexpr>')
21 MAKE_CLOSURE 0
24 LOAD_DEREF 1 (current)
27 GET_ITER
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 STORE_FAST 3 (filtered)
4 34 LOAD_FAST 1 (new)
37 STORE_DEREF 1 (current)
5 40 LOAD_GLOBAL 0 (list)
43 LOAD_FAST 3 (filtered)
46 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
3 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 32 (to 38)
6 STORE_FAST 1 (x)
9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant.
12 LOAD_ATTR 0 (count)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 LOAD_DEREF 0 (count)
24 COMPARE_OP 2 (==)
27 POP_JUMP_IF_FALSE 3
30 LOAD_FAST 1 (x)
33 YIELD_VALUE
34 POP_TOP
35 JUMP_ABSOLUTE 3
>> 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)
To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.
The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.
The root cause of the problem is that generators are lazy; variables are evaluated each time:
>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
It iterates over the original list and evaluates the condition with the current list. In this case, 4 appeared twice in the new list, causing it to appear in the result. It only appears once in the result because it only appeared once in the original list. The 6s appear twice in the new list, but never appear in the old list and are hence never shown.
Full function introspection for the curious (the line with the comment is the important line):
>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
current = original
filtered = (x for x in current if current.count(x) == count)
current = new
return list(filtered)
>>> from dis import dis
>>> dis(f)
2 0 LOAD_FAST 0 (original)
3 STORE_DEREF 1 (current)
3 6 LOAD_CLOSURE 0 (count)
9 LOAD_CLOSURE 1 (current)
12 BUILD_TUPLE 2
15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
18 LOAD_CONST 2 ('f.<locals>.<genexpr>')
21 MAKE_CLOSURE 0
24 LOAD_DEREF 1 (current)
27 GET_ITER
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 STORE_FAST 3 (filtered)
4 34 LOAD_FAST 1 (new)
37 STORE_DEREF 1 (current)
5 40 LOAD_GLOBAL 0 (list)
43 LOAD_FAST 3 (filtered)
46 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
3 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 32 (to 38)
6 STORE_FAST 1 (x)
9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant.
12 LOAD_ATTR 0 (count)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 LOAD_DEREF 0 (count)
24 COMPARE_OP 2 (==)
27 POP_JUMP_IF_FALSE 3
30 LOAD_FAST 1 (x)
33 YIELD_VALUE
34 POP_TOP
35 JUMP_ABSOLUTE 3
>> 38 LOAD_CONST 0 (None)
41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)
To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.
The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.
edited 11 hours ago
answered 12 hours ago
Solomon UckoSolomon Ucko
6391719
6391719
add a comment |
add a comment |
Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:
Look again at your output with the type of f
: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.
Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.
Code to "make it work"
That depends on what you mean by "make it work". If you want f
to be a filtered list, then use a list, not a generator:
f = [x for x in array if array.count(x) == 2] # Filters original
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
add a comment |
Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:
Look again at your output with the type of f
: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.
Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.
Code to "make it work"
That depends on what you mean by "make it work". If you want f
to be a filtered list, then use a list, not a generator:
f = [x for x in array if array.count(x) == 2] # Filters original
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
add a comment |
Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:
Look again at your output with the type of f
: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.
Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.
Code to "make it work"
That depends on what you mean by "make it work". If you want f
to be a filtered list, then use a list, not a generator:
f = [x for x in array if array.count(x) == 2] # Filters original
Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:
Look again at your output with the type of f
: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.
Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.
Code to "make it work"
That depends on what you mean by "make it work". If you want f
to be a filtered list, then use a list, not a generator:
f = [x for x in array if array.count(x) == 2] # Filters original
edited 15 hours ago
answered 16 hours ago
PrunePrune
43.2k143456
43.2k143456
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
add a comment |
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
I somewhat understand. Could you show some code to make it work, because I need to re-assign the same list again in the main code.
– Suraj Kothari
16 hours ago
add a comment |
Generators are lazy and your newly defined array
is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses ()
by brackets .
Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter
to count values, and keep a copy of your original list:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Notice the second version doesn't even require old_array
and is useful if there is no need to maintain ordering of values in your original array.
add a comment |
Generators are lazy and your newly defined array
is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses ()
by brackets .
Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter
to count values, and keep a copy of your original list:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Notice the second version doesn't even require old_array
and is useful if there is no need to maintain ordering of values in your original array.
add a comment |
Generators are lazy and your newly defined array
is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses ()
by brackets .
Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter
to count values, and keep a copy of your original list:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Notice the second version doesn't even require old_array
and is useful if there is no need to maintain ordering of values in your original array.
Generators are lazy and your newly defined array
is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses ()
by brackets .
Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter
to count values, and keep a copy of your original list:
from collections import Counter
array = [1, 2, 2, 4, 5] # original array
counts = Counter(array) # count each value in array
old_array = array.copy() # make copy
array = [5, 6, 1, 2, 9] # updates array
# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]
# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]
Notice the second version doesn't even require old_array
and is useful if there is no need to maintain ordering of values in your original array.
answered 13 hours ago
jppjpp
95.9k2157109
95.9k2157109
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54245618%2funexpected-behaviour-with-python-generator%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
You keep printing
list(g)
, butg
is an undefined symbol. Perhaps you have a typo, usingg
forf
?– Prune
16 hours ago
Yh that is what I meant sorry
– Suraj Kothari
16 hours ago
3
You redefine
array
and your new array is what gets referenced by the lazy generator comprehension.– jpp
16 hours ago
2
Slap "strange" onto the title and you're sure to get a few upvotes.
– coldspeed
9 hours ago
2
This is a variation of the question of "late binding" of python closures. The generator is essentially acting like a closure here. (I'm not sure why the answers are so focused on laziness... that, I think, is obvious to anyone using a generator.)
– Mateen Ulhaq
8 hours ago