Hello! Recently I’ve found that bool(“AN EMPTY GENERATOR”) is True.
code : print(bool(range(0)), bool((i for i in range(0))), bool(list((i for i in range(0)))))
output: False True False
I think it’s a little bit weired, just like bool(“AN EMPTY LIST”) is False, what do you think ?In addition, I’ve tried calling next() on an “empty” generator, it raised StopIteration exception.
Generators are true even if they turn out to be empty because you can’t tell whether it’s empty or not until you try iterating over it, and objects are, by default, true.
Just opining, but maybe it would be useful for a generator supported a push operation which pushes a value onto the front of the generator. That value would be returned before actually touching the underlying generator.
class gen2:
def __init__(self, gen1):
self.pushes = []
self.gen1 = gen1
def __iter__(self):
return self
def push(self, val):
self.pushes.insert(0, val)
def __next__(self):
if self.pushes:
return self.pushes.pop(0)
return next(self.gen1)
x = gen2(i for i in range(10))
x.push(-1)
print(next(x))
print(next(x))
x.push(42)
for i in x:
print(">>", i)
Hi Skip, Inspired by your code.My idea is to create a copy of the original generator and then, when calling the __bool__ function, use the copy to determine if it’s empty.Here is the half-assed code:
import itertools
class gen3:
def __init__(self, gen1):
self.gen1, self._gen2 = itertools.tee(gen1, 2)
self._empty = False
def __iter__(self):
return self
def __next__(self):
r = next(self.gen1, False)
if r is not False:
return r
self._empty = True
raise StopIteration
def __bool__(self):
if self._empty:
return False
try:
next(self._gen2)
return True
except StopIteration:
return False
x = gen3(i for i in range(10))
print(bool(x))
for i in x:
print(i)
print(bool(x))
x1 = gen3(i for i in range(0))
print(bool(x1))
# removed self._empty
import itertools
class gen3:
def __init__(self, gen1):
self.gen1, self._gen2 = itertools.tee(gen1, 2)
def __iter__(self):
return self
def __next__(self):
r = next(self.gen1, False)
if r is not False:
return r
self._gen2 = self.gen1
raise StopIteration
def __bool__(self):
try:
next(self._gen2)
return True
except StopIteration:
return False
x = gen3(i for i in range(10))
print(bool(x))
for i in x:
print(i)
print(bool(x))
x1 = gen3(i for i in range(0))
print(bool(x1))
Shows the lispy roots. Basically nil and empty list are false and False is false and 0 is false. Pretty much everything else is true. It is convenient mostly that it is so.
For a succinct accounting of what is considered false during testing for truth value, we can find official documentation at Truth Value Testing. As stated, that would include, among other things, empty sequences and collections such as '', (), [], {}, set(), and range(0).
According to your answer, I’d like to post the source code I found in Cpython here.
int
PyObject_IsTrue(PyObject *v)
{
Py_ssize_t res;
if (v == Py_True)
return 1;
if (v == Py_False)
return 0;
if (v == Py_None)
return 0;
else if (Py_TYPE(v)->tp_as_number != NULL &&
Py_TYPE(v)->tp_as_number->nb_bool != NULL)
res = (*Py_TYPE(v)->tp_as_number->nb_bool)(v);
else if (Py_TYPE(v)->tp_as_mapping != NULL &&
Py_TYPE(v)->tp_as_mapping->mp_length != NULL)
res = (*Py_TYPE(v)->tp_as_mapping->mp_length)(v);
else if (Py_TYPE(v)->tp_as_sequence != NULL &&
Py_TYPE(v)->tp_as_sequence->sq_length != NULL)
res = (*Py_TYPE(v)->tp_as_sequence->sq_length)(v);
else
return 1;
/* if it is negative, it should be either -1 or -2 */
return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}