Issue #16148: implemented PEP 424

This commit is contained in:
Armin Ronacher 2012-10-06 14:03:24 +02:00
parent ef08fb1f04
commit aa9a79d279
14 changed files with 161 additions and 82 deletions

View File

@ -342,6 +342,13 @@ is considered sufficient for this determination.
returned. This is the equivalent to the Python expression ``len(o)``. returned. This is the equivalent to the Python expression ``len(o)``.
.. c:function:: Py_ssize_t PyObject_LengthHint(PyObject *o, Py_ssize_t default)
Return an estimated length for the object *o*. First trying to return its
actual length, then an estimate using ``__length_hint__``, and finally
returning the default value. On error ``-1`` is returned. This is the
equivalent to the Python expression ``operator.length_hint(o, default)``.
.. c:function:: PyObject* PyObject_GetItem(PyObject *o, PyObject *key) .. c:function:: PyObject* PyObject_GetItem(PyObject *o, PyObject *key)
Return element of *o* corresponding to the object *key* or *NULL* on failure. Return element of *o* corresponding to the object *key* or *NULL* on failure.

View File

@ -235,6 +235,12 @@ their character equivalents.
.. XXX: find a better, readable, example .. XXX: find a better, readable, example
.. function:: length_hint(obj, default=0)
Return an estimated length for the object *o*. First trying to return its
actual length, then an estimate using ``__length_hint__``, and finally
returning the default value.
The :mod:`operator` module also defines tools for generalized attribute and item The :mod:`operator` module also defines tools for generalized attribute and item
lookups. These are useful for making fast field extractors as arguments for lookups. These are useful for making fast field extractors as arguments for
:func:`map`, :func:`sorted`, :meth:`itertools.groupby`, or other functions that :func:`map`, :func:`sorted`, :meth:`itertools.groupby`, or other functions that

View File

@ -403,9 +403,8 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/
PyAPI_FUNC(Py_ssize_t) PyObject_Length(PyObject *o); PyAPI_FUNC(Py_ssize_t) PyObject_Length(PyObject *o);
#define PyObject_Length PyObject_Size #define PyObject_Length PyObject_Size
#ifndef Py_LIMITED_API PyAPI_FUNC(int) _PyObject_HasLen(PyObject *o);
PyAPI_FUNC(Py_ssize_t) _PyObject_LengthHint(PyObject *o, Py_ssize_t); PyAPI_FUNC(Py_ssize_t) PyObject_LengthHint(PyObject *o, Py_ssize_t);
#endif
/* /*
Guess the size of object o using len(o) or o.__length_hint__(). Guess the size of object o using len(o) or o.__length_hint__().

View File

@ -1,4 +1,5 @@
import unittest import unittest
import operator
import sys import sys
import pickle import pickle
@ -168,15 +169,13 @@ class TestReversed(unittest.TestCase, PickleTest):
x = range(1) x = range(1)
self.assertEqual(type(reversed(x)), type(iter(x))) self.assertEqual(type(reversed(x)), type(iter(x)))
@support.cpython_only
def test_len(self): def test_len(self):
# This is an implementation detail, not an interface requirement # This is an implementation detail, not an interface requirement
from test.test_iterlen import len
for s in ('hello', tuple('hello'), list('hello'), range(5)): for s in ('hello', tuple('hello'), list('hello'), range(5)):
self.assertEqual(len(reversed(s)), len(s)) self.assertEqual(operator.length_hint(reversed(s)), len(s))
r = reversed(s) r = reversed(s)
list(r) list(r)
self.assertEqual(len(r), 0) self.assertEqual(operator.length_hint(r), 0)
class SeqWithWeirdLen: class SeqWithWeirdLen:
called = False called = False
def __len__(self): def __len__(self):
@ -187,7 +186,7 @@ class TestReversed(unittest.TestCase, PickleTest):
def __getitem__(self, index): def __getitem__(self, index):
return index return index
r = reversed(SeqWithWeirdLen()) r = reversed(SeqWithWeirdLen())
self.assertRaises(ZeroDivisionError, len, r) self.assertRaises(ZeroDivisionError, operator.length_hint, r)
def test_gc(self): def test_gc(self):

View File

@ -45,31 +45,21 @@ import unittest
from test import support from test import support
from itertools import repeat from itertools import repeat
from collections import deque from collections import deque
from builtins import len as _len from operator import length_hint
n = 10 n = 10
def len(obj):
try:
return _len(obj)
except TypeError:
try:
# note: this is an internal undocumented API,
# don't rely on it in your own programs
return obj.__length_hint__()
except AttributeError:
raise TypeError
class TestInvariantWithoutMutations(unittest.TestCase): class TestInvariantWithoutMutations(unittest.TestCase):
def test_invariant(self): def test_invariant(self):
it = self.it it = self.it
for i in reversed(range(1, n+1)): for i in reversed(range(1, n+1)):
self.assertEqual(len(it), i) self.assertEqual(length_hint(it), i)
next(it) next(it)
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
self.assertRaises(StopIteration, next, it) self.assertRaises(StopIteration, next, it)
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
class TestTemporarilyImmutable(TestInvariantWithoutMutations): class TestTemporarilyImmutable(TestInvariantWithoutMutations):
@ -78,12 +68,12 @@ class TestTemporarilyImmutable(TestInvariantWithoutMutations):
# length immutability during iteration # length immutability during iteration
it = self.it it = self.it
self.assertEqual(len(it), n) self.assertEqual(length_hint(it), n)
next(it) next(it)
self.assertEqual(len(it), n-1) self.assertEqual(length_hint(it), n-1)
self.mutate() self.mutate()
self.assertRaises(RuntimeError, next, it) self.assertRaises(RuntimeError, next, it)
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
## ------- Concrete Type Tests ------- ## ------- Concrete Type Tests -------
@ -92,10 +82,6 @@ class TestRepeat(TestInvariantWithoutMutations):
def setUp(self): def setUp(self):
self.it = repeat(None, n) self.it = repeat(None, n)
def test_no_len_for_infinite_repeat(self):
# The repeat() object can also be infinite
self.assertRaises(TypeError, len, repeat(None))
class TestXrange(TestInvariantWithoutMutations): class TestXrange(TestInvariantWithoutMutations):
def setUp(self): def setUp(self):
@ -167,14 +153,15 @@ class TestList(TestInvariantWithoutMutations):
it = iter(d) it = iter(d)
next(it) next(it)
next(it) next(it)
self.assertEqual(len(it), n-2) self.assertEqual(length_hint(it), n - 2)
d.append(n) d.append(n)
self.assertEqual(len(it), n-1) # grow with append self.assertEqual(length_hint(it), n - 1) # grow with append
d[1:] = [] d[1:] = []
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
self.assertEqual(list(it), []) self.assertEqual(list(it), [])
d.extend(range(20)) d.extend(range(20))
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
class TestListReversed(TestInvariantWithoutMutations): class TestListReversed(TestInvariantWithoutMutations):
@ -186,32 +173,41 @@ class TestListReversed(TestInvariantWithoutMutations):
it = reversed(d) it = reversed(d)
next(it) next(it)
next(it) next(it)
self.assertEqual(len(it), n-2) self.assertEqual(length_hint(it), n - 2)
d.append(n) d.append(n)
self.assertEqual(len(it), n-2) # ignore append self.assertEqual(length_hint(it), n - 2) # ignore append
d[1:] = [] d[1:] = []
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
self.assertEqual(list(it), []) # confirm invariant self.assertEqual(list(it), []) # confirm invariant
d.extend(range(20)) d.extend(range(20))
self.assertEqual(len(it), 0) self.assertEqual(length_hint(it), 0)
## -- Check to make sure exceptions are not suppressed by __length_hint__() ## -- Check to make sure exceptions are not suppressed by __length_hint__()
class BadLen(object): class BadLen(object):
def __iter__(self): return iter(range(10)) def __iter__(self):
return iter(range(10))
def __len__(self): def __len__(self):
raise RuntimeError('hello') raise RuntimeError('hello')
class BadLengthHint(object): class BadLengthHint(object):
def __iter__(self): return iter(range(10)) def __iter__(self):
return iter(range(10))
def __length_hint__(self): def __length_hint__(self):
raise RuntimeError('hello') raise RuntimeError('hello')
class NoneLengthHint(object): class NoneLengthHint(object):
def __iter__(self): return iter(range(10)) def __iter__(self):
return iter(range(10))
def __length_hint__(self): def __length_hint__(self):
return None return NotImplemented
class TestLengthHintExceptions(unittest.TestCase): class TestLengthHintExceptions(unittest.TestCase):

View File

@ -1723,9 +1723,8 @@ class TestVariousIteratorArgs(unittest.TestCase):
class LengthTransparency(unittest.TestCase): class LengthTransparency(unittest.TestCase):
def test_repeat(self): def test_repeat(self):
from test.test_iterlen import len self.assertEqual(operator.length_hint(repeat(None, 50)), 50)
self.assertEqual(len(repeat(None, 50)), 50) self.assertEqual(operator.length_hint(repeat(None), 12), 12)
self.assertRaises(TypeError, len, repeat(None))
class RegressionTests(unittest.TestCase): class RegressionTests(unittest.TestCase):

View File

@ -410,6 +410,31 @@ class OperatorTestCase(unittest.TestCase):
self.assertEqual(operator.__ixor__ (c, 5), "ixor") self.assertEqual(operator.__ixor__ (c, 5), "ixor")
self.assertEqual(operator.__iconcat__ (c, c), "iadd") self.assertEqual(operator.__iconcat__ (c, c), "iadd")
def test_length_hint(self):
class X(object):
def __init__(self, value):
self.value = value
def __length_hint__(self):
if type(self.value) is type:
raise self.value
else:
return self.value
self.assertEqual(operator.length_hint([], 2), 0)
self.assertEqual(operator.length_hint(iter([1, 2, 3])), 3)
self.assertEqual(operator.length_hint(X(2)), 2)
self.assertEqual(operator.length_hint(X(NotImplemented), 4), 4)
self.assertEqual(operator.length_hint(X(TypeError), 12), 12)
with self.assertRaises(TypeError):
operator.length_hint(X("abc"))
with self.assertRaises(ValueError):
operator.length_hint(X(-2))
with self.assertRaises(LookupError):
operator.length_hint(X(LookupError))
def test_main(verbose=None): def test_main(verbose=None):
import sys import sys
test_classes = ( test_classes = (

View File

@ -848,8 +848,6 @@ class TestBasicOps(unittest.TestCase):
for v in self.set: for v in self.set:
self.assertIn(v, self.values) self.assertIn(v, self.values)
setiter = iter(self.set) setiter = iter(self.set)
# note: __length_hint__ is an internal undocumented API,
# don't rely on it in your own programs
self.assertEqual(setiter.__length_hint__(), len(self.set)) self.assertEqual(setiter.__length_hint__(), len(self.set))
def test_pickling(self): def test_pickling(self):

View File

@ -208,6 +208,31 @@ _tscmp(const unsigned char *a, const unsigned char *b,
return (result == 0); return (result == 0);
} }
PyDoc_STRVAR(length_hint__doc__,
"length_hint(obj, default=0) -> int\n"
"Return an estimate of the number of items in obj.\n"
"This is useful for presizing containers when building from an\n"
"iterable.\n"
"\n"
"If the object supports len(), the result will be\n"
"exact. Otherwise, it may over- or under-estimate by an\n"
"arbitrary amount. The result will be an integer >= 0.");
static PyObject *length_hint(PyObject *self, PyObject *args)
{
PyObject *obj;
Py_ssize_t defaultvalue = 0, res;
if (!PyArg_ParseTuple(args, "O|n:length_hint", &obj, &defaultvalue)) {
return NULL;
}
res = PyObject_LengthHint(obj, defaultvalue);
if (res == -1 && PyErr_Occurred()) {
return NULL;
}
return PyLong_FromSsize_t(res);
}
PyDoc_STRVAR(compare_digest__doc__, PyDoc_STRVAR(compare_digest__doc__,
"compare_digest(a, b) -> bool\n" "compare_digest(a, b) -> bool\n"
"\n" "\n"
@ -366,6 +391,8 @@ spam2(ge,__ge__, "ge(a, b) -- Same as a>=b.")
{"_compare_digest", (PyCFunction)compare_digest, METH_VARARGS, {"_compare_digest", (PyCFunction)compare_digest, METH_VARARGS,
compare_digest__doc__}, compare_digest__doc__},
{"length_hint", (PyCFunction)length_hint, METH_VARARGS,
length_hint__doc__},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };

View File

@ -64,49 +64,67 @@ PyObject_Length(PyObject *o)
} }
#define PyObject_Length PyObject_Size #define PyObject_Length PyObject_Size
int
_PyObject_HasLen(PyObject *o) {
return (Py_TYPE(o)->tp_as_sequence && Py_TYPE(o)->tp_as_sequence->sq_length) ||
(Py_TYPE(o)->tp_as_mapping && Py_TYPE(o)->tp_as_mapping->mp_length);
}
/* The length hint function returns a non-negative value from o.__len__() /* The length hint function returns a non-negative value from o.__len__()
or o.__length_hint__(). If those methods aren't found or return a negative or o.__length_hint__(). If those methods aren't found. If one of the calls
value, then the defaultvalue is returned. If one of the calls fails, fails this function returns -1.
this function returns -1.
*/ */
Py_ssize_t Py_ssize_t
_PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue) PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)
{ {
_Py_IDENTIFIER(__length_hint__); _Py_IDENTIFIER(__length_hint__);
PyObject *ro, *hintmeth; Py_ssize_t res = PyObject_Length(o);
Py_ssize_t rv; if (res < 0 && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_TypeError)) {
/* try o.__len__() */
rv = PyObject_Size(o);
if (rv >= 0)
return rv;
if (PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_TypeError))
return -1; return -1;
}
PyErr_Clear(); PyErr_Clear();
} }
else {
/* try o.__length_hint__() */ return res;
hintmeth = _PyObject_LookupSpecial(o, &PyId___length_hint__);
if (hintmeth == NULL) {
if (PyErr_Occurred())
return -1;
else
return defaultvalue;
} }
ro = PyObject_CallFunctionObjArgs(hintmeth, NULL); PyObject *hint = _PyObject_LookupSpecial(o, &PyId___length_hint__);
Py_DECREF(hintmeth); if (hint == NULL) {
if (ro == NULL) { if (PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_TypeError))
return -1; return -1;
PyErr_Clear(); }
return defaultvalue; return defaultvalue;
} }
rv = PyLong_Check(ro) ? PyLong_AsSsize_t(ro) : defaultvalue; PyObject *result = PyObject_CallFunctionObjArgs(hint, NULL);
Py_DECREF(ro); Py_DECREF(hint);
return rv; if (result == NULL) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
PyErr_Clear();
return defaultvalue;
}
return -1;
}
else if (result == Py_NotImplemented) {
Py_DECREF(result);
return defaultvalue;
}
if (!PyLong_Check(result)) {
PyErr_Format(PyExc_TypeError, "Length hint must be an integer, not %s",
Py_TYPE(result)->tp_name);
Py_DECREF(result);
return -1;
}
defaultvalue = PyLong_AsSsize_t(result);
Py_DECREF(result);
if (defaultvalue < 0 && PyErr_Occurred()) {
return -1;
}
if (defaultvalue < 0) {
PyErr_Format(PyExc_ValueError, "__length_hint__() should return >= 0");
return -1;
}
return defaultvalue;
} }
PyObject * PyObject *
@ -1687,7 +1705,7 @@ PySequence_Tuple(PyObject *v)
return NULL; return NULL;
/* Guess result size and allocate space. */ /* Guess result size and allocate space. */
n = _PyObject_LengthHint(v, 10); n = PyObject_LengthHint(v, 10);
if (n == -1) if (n == -1)
goto Fail; goto Fail;
result = PyTuple_New(n); result = PyTuple_New(n);

View File

@ -2282,7 +2282,7 @@ bytearray_extend(PyByteArrayObject *self, PyObject *arg)
return NULL; return NULL;
/* Try to determine the length of the argument. 32 is arbitrary. */ /* Try to determine the length of the argument. 32 is arbitrary. */
buf_size = _PyObject_LengthHint(arg, 32); buf_size = PyObject_LengthHint(arg, 32);
if (buf_size == -1) { if (buf_size == -1) {
Py_DECREF(it); Py_DECREF(it);
return NULL; return NULL;

View File

@ -2651,7 +2651,7 @@ PyBytes_FromObject(PyObject *x)
} }
/* For iterator version, create a string object and resize as needed */ /* For iterator version, create a string object and resize as needed */
size = _PyObject_LengthHint(x, 64); size = PyObject_LengthHint(x, 64);
if (size == -1 && PyErr_Occurred()) if (size == -1 && PyErr_Occurred())
return NULL; return NULL;
/* Allocate an extra byte to prevent PyBytes_FromStringAndSize() from /* Allocate an extra byte to prevent PyBytes_FromStringAndSize() from

View File

@ -76,9 +76,14 @@ iter_len(seqiterobject *it)
Py_ssize_t seqsize, len; Py_ssize_t seqsize, len;
if (it->it_seq) { if (it->it_seq) {
seqsize = PySequence_Size(it->it_seq); if (_PyObject_HasLen(it->it_seq)) {
if (seqsize == -1) seqsize = PySequence_Size(it->it_seq);
return NULL; if (seqsize == -1)
return NULL;
}
else {
return Py_NotImplemented;
}
len = seqsize - it->it_index; len = seqsize - it->it_index;
if (len >= 0) if (len >= 0)
return PyLong_FromSsize_t(len); return PyLong_FromSsize_t(len);

View File

@ -826,7 +826,7 @@ listextend(PyListObject *self, PyObject *b)
iternext = *it->ob_type->tp_iternext; iternext = *it->ob_type->tp_iternext;
/* Guess a result list size. */ /* Guess a result list size. */
n = _PyObject_LengthHint(b, 8); n = PyObject_LengthHint(b, 8);
if (n == -1) { if (n == -1) {
Py_DECREF(it); Py_DECREF(it);
return NULL; return NULL;