Skip to content

Commit 68d3761

Browse files
author
Aviram Hassan
committed
- PyList, PyTuple and PySequence's get_item now accepts only usize indices instead of isize.
- `PyList` and `PyTuple`'s `get_item` now always uses the safe API. See `get_item_unchecked` for retrieving index without checks.
1 parent 388c255 commit 68d3761

File tree

9 files changed

+223
-85
lines changed

9 files changed

+223
-85
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
88

99
## [Unreleased]
1010

11+
### Added
12+
- Add `PyList::get_item_unchecked()` and `PyTuple::get_item_unchecked()` to get items without bounds checks. [#1733](https://github.com/PyO3/pyo3/pull/1733)
13+
1114
### Changed
1215

1316
- Change `PyErr::fetch()` to return `Option<PyErr>`. [#1717](https://github.com/PyO3/pyo3/pull/1717)
17+
- `PyList`, `PyTuple` and `PySequence`'s `get_item` now accepts only `usize` indices instead of `isize`. [#1733](https://github.com/PyO3/pyo3/pull/1733)
1418

1519
### Fixed
1620

@@ -39,6 +43,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3943
- Fix compiler warning: deny trailing semicolons in expression macro. [#1762](https://github.com/PyO3/pyo3/pull/1762)
4044
- Fix incorrect FFI definition of `Py_DecodeLocale`. The 2nd argument is now `*mut Py_ssize_t` instead of `Py_ssize_t`. [#1766](https://github.com/PyO3/pyo3/pull/1766)
4145

46+
### Changed
47+
- `PyList` and `PyTuple`'s `get_item` now return `PyResult<&PyAny> instead of panicking. [#1733](https://github.com/PyO3/pyo3/pull/1733)
4248
## [0.14.1] - 2021-07-04
4349

4450
### Added

benches/bench_list.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,22 @@ fn list_get_item(b: &mut Bencher) {
3232
let mut sum = 0;
3333
b.iter(|| {
3434
for i in 0..LEN {
35-
sum += list.get_item(i as isize).extract::<usize>().unwrap();
35+
sum += list.get_item(i).unwrap().extract::<usize>().unwrap();
36+
}
37+
});
38+
}
39+
40+
fn list_get_item_unchecked(b: &mut Bencher) {
41+
let gil = Python::acquire_gil();
42+
let py = gil.python();
43+
const LEN: usize = 50_000;
44+
let list = PyList::new(py, 0..LEN);
45+
let mut sum = 0;
46+
b.iter(|| {
47+
for i in 0..LEN {
48+
unsafe {
49+
sum += list.get_item_unchecked(i).extract::<usize>().unwrap();
50+
}
3651
}
3752
});
3853
}
@@ -41,6 +56,7 @@ fn criterion_benchmark(c: &mut Criterion) {
4156
c.bench_function("iter_list", iter_list);
4257
c.bench_function("list_new", list_new);
4358
c.bench_function("list_get_item", list_get_item);
59+
c.bench_function("list_get_item_unchecked", list_get_item_unchecked);
4460
}
4561

4662
criterion_group!(benches, criterion_benchmark);

benches/bench_tuple.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,22 @@ fn tuple_get_item(b: &mut Bencher) {
3232
let mut sum = 0;
3333
b.iter(|| {
3434
for i in 0..LEN {
35-
sum += tuple.get_item(i).extract::<usize>().unwrap();
35+
sum += tuple.get_item(i).unwrap().extract::<usize>().unwrap();
36+
}
37+
});
38+
}
39+
40+
fn tuple_get_item_unchecked(b: &mut Bencher) {
41+
let gil = Python::acquire_gil();
42+
let py = gil.python();
43+
const LEN: usize = 50_000;
44+
let tuple = PyTuple::new(py, 0..LEN);
45+
let mut sum = 0;
46+
b.iter(|| {
47+
for i in 0..LEN {
48+
unsafe {
49+
sum += tuple.get_item_unchecked(i).extract::<usize>().unwrap();
50+
}
3651
}
3752
});
3853
}
@@ -41,6 +56,7 @@ fn criterion_benchmark(c: &mut Criterion) {
4156
c.bench_function("iter_tuple", iter_tuple);
4257
c.bench_function("tuple_new", tuple_new);
4358
c.bench_function("tuple_get_item", tuple_get_item);
59+
c.bench_function("tuple_get_item_unchecked", tuple_get_item_unchecked);
4460
}
4561

4662
criterion_group!(benches, criterion_benchmark);

pyo3-macros-backend/src/from_pyobject.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ impl<'a> Container<'a> {
243243
for i in 0..len {
244244
let error_msg = format!("failed to extract field {}.{}", quote!(#self_ty), i);
245245
fields.push(quote!(
246-
s.get_item(#i).extract().map_err(|inner| {
246+
s.get_item(#i).and_then(PyAny::extract).map_err(|inner| {
247247
let py = pyo3::PyNativeType::py(obj);
248248
let new_err = pyo3::exceptions::PyTypeError::new_err(#error_msg);
249249
new_err.set_cause(py, Some(inner));

src/conversions/array.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ mod min_const_generics {
6060
if seq_len != N {
6161
return Err(invalid_sequence_length(N, seq_len));
6262
}
63-
array_try_from_fn(|idx| seq.get_item(idx as isize).and_then(PyAny::extract))
63+
array_try_from_fn(|idx| seq.get_item(idx).and_then(PyAny::extract))
6464
}
6565

6666
// TODO use std::array::try_from_fn, if that stabilises:

src/types/dict.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,8 +639,8 @@ mod tests {
639639
let mut value_sum = 0;
640640
for el in dict.items().iter() {
641641
let tuple = el.cast_as::<PyTuple>().unwrap();
642-
key_sum += tuple.get_item(0).extract::<i32>().unwrap();
643-
value_sum += tuple.get_item(1).extract::<i32>().unwrap();
642+
key_sum += tuple.get_item(0).unwrap().extract::<i32>().unwrap();
643+
value_sum += tuple.get_item(1).unwrap().extract::<i32>().unwrap();
644644
}
645645
assert_eq!(7 + 8 + 9, key_sum);
646646
assert_eq!(32 + 42 + 123, value_sum);

src/types/list.rs

Lines changed: 109 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,38 @@ impl PyList {
6666
self.len() == 0
6767
}
6868

69-
/// Gets the item at the specified index.
70-
///
71-
/// Panics if the index is out of range.
72-
pub fn get_item(&self, index: isize) -> &PyAny {
73-
assert!(index >= 0 && index < self.len() as isize);
69+
/// Gets the list item at the specified index.
70+
/// # Example
71+
/// ```
72+
/// use pyo3::{prelude::*, types::PyList};
73+
/// Python::with_gil(|py| {
74+
/// let list = PyList::new(py, &[2, 3, 5, 7]);
75+
/// let obj = list.get_item(0);
76+
/// assert_eq!(obj.unwrap().extract::<i32>().unwrap(), 2);
77+
/// });
78+
/// ```
79+
pub fn get_item(&self, index: usize) -> PyResult<&PyAny> {
7480
unsafe {
75-
#[cfg(not(Py_LIMITED_API))]
76-
let ptr = ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t);
77-
#[cfg(Py_LIMITED_API)]
78-
let ptr = ffi::PyList_GetItem(self.as_ptr(), index as Py_ssize_t);
79-
81+
let item = ffi::PyList_GetItem(self.as_ptr(), index as Py_ssize_t);
8082
// PyList_GetItem return borrowed ptr; must make owned for safety (see #890).
81-
ffi::Py_INCREF(ptr);
82-
self.py().from_owned_ptr(ptr)
83+
ffi::Py_INCREF(item);
84+
self.py().from_owned_ptr_or_err(item)
8385
}
8486
}
8587

88+
/// Gets the list item at the specified index. Undefined behavior on bad index. Use with caution.
89+
///
90+
/// # Safety
91+
///
92+
/// Caller must verify that the index is within the bounds of the list.
93+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
94+
pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny {
95+
let item = ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t);
96+
// PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890).
97+
ffi::Py_INCREF(item);
98+
self.py().from_owned_ptr(item)
99+
}
100+
86101
/// Sets the item at the specified index.
87102
///
88103
/// Panics if the index is out of range.
@@ -142,16 +157,19 @@ impl PyList {
142157
/// Used by `PyList::iter()`.
143158
pub struct PyListIterator<'a> {
144159
list: &'a PyList,
145-
index: isize,
160+
index: usize,
146161
}
147162

148163
impl<'a> Iterator for PyListIterator<'a> {
149164
type Item = &'a PyAny;
150165

151166
#[inline]
152167
fn next(&mut self) -> Option<&'a PyAny> {
153-
if self.index < self.list.len() as isize {
154-
let item = self.list.get_item(self.index);
168+
if self.index < self.list.len() {
169+
#[cfg(any(Py_LIMITED_API, PyPy))]
170+
let item = self.list.get_item(self.index).expect("tuple.get failed");
171+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
172+
let item = unsafe { self.list.get_item_unchecked(self.index) };
155173
self.index += 1;
156174
Some(item)
157175
} else {
@@ -164,8 +182,8 @@ impl<'a> Iterator for PyListIterator<'a> {
164182
let len = self.list.len();
165183

166184
(
167-
len.saturating_sub(self.index as usize),
168-
Some(len.saturating_sub(self.index as usize)),
185+
len.saturating_sub(self.index),
186+
Some(len.saturating_sub(self.index)),
169187
)
170188
}
171189
}
@@ -216,10 +234,10 @@ mod tests {
216234
fn test_new() {
217235
Python::with_gil(|py| {
218236
let list = PyList::new(py, &[2, 3, 5, 7]);
219-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
220-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
221-
assert_eq!(5, list.get_item(2).extract::<i32>().unwrap());
222-
assert_eq!(7, list.get_item(3).extract::<i32>().unwrap());
237+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
238+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
239+
assert_eq!(5, list.get_item(2).unwrap().extract::<i32>().unwrap());
240+
assert_eq!(7, list.get_item(3).unwrap().extract::<i32>().unwrap());
223241
});
224242
}
225243

@@ -235,19 +253,10 @@ mod tests {
235253
fn test_get_item() {
236254
Python::with_gil(|py| {
237255
let list = PyList::new(py, &[2, 3, 5, 7]);
238-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
239-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
240-
assert_eq!(5, list.get_item(2).extract::<i32>().unwrap());
241-
assert_eq!(7, list.get_item(3).extract::<i32>().unwrap());
242-
});
243-
}
244-
245-
#[test]
246-
#[should_panic]
247-
fn test_get_item_invalid() {
248-
Python::with_gil(|py| {
249-
let list = PyList::new(py, &[2, 3, 5, 7]);
250-
list.get_item(-1);
256+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
257+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
258+
assert_eq!(5, list.get_item(2).unwrap().extract::<i32>().unwrap());
259+
assert_eq!(7, list.get_item(3).unwrap().extract::<i32>().unwrap());
251260
});
252261
}
253262

@@ -256,9 +265,9 @@ mod tests {
256265
Python::with_gil(|py| {
257266
let list = PyList::new(py, &[2, 3, 5, 7]);
258267
let val = 42i32.to_object(py);
259-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
268+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
260269
list.set_item(0, val).unwrap();
261-
assert_eq!(42, list.get_item(0).extract::<i32>().unwrap());
270+
assert_eq!(42, list.get_item(0).unwrap().extract::<i32>().unwrap());
262271
});
263272
}
264273

@@ -286,11 +295,11 @@ mod tests {
286295
let list = PyList::new(py, &[2, 3, 5, 7]);
287296
let val = 42i32.to_object(py);
288297
assert_eq!(4, list.len());
289-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
298+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
290299
list.insert(0, val).unwrap();
291300
assert_eq!(5, list.len());
292-
assert_eq!(42, list.get_item(0).extract::<i32>().unwrap());
293-
assert_eq!(2, list.get_item(1).extract::<i32>().unwrap());
301+
assert_eq!(42, list.get_item(0).unwrap().extract::<i32>().unwrap());
302+
assert_eq!(2, list.get_item(1).unwrap().extract::<i32>().unwrap());
294303
});
295304
}
296305

@@ -315,8 +324,8 @@ mod tests {
315324
Python::with_gil(|py| {
316325
let list = PyList::new(py, &[2]);
317326
list.append(3).unwrap();
318-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
319-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
327+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
328+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
320329
});
321330
}
322331

@@ -393,15 +402,15 @@ mod tests {
393402
Python::with_gil(|py| {
394403
let v = vec![7, 3, 2, 5];
395404
let list = PyList::new(py, &v);
396-
assert_eq!(7, list.get_item(0).extract::<i32>().unwrap());
397-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
398-
assert_eq!(2, list.get_item(2).extract::<i32>().unwrap());
399-
assert_eq!(5, list.get_item(3).extract::<i32>().unwrap());
405+
assert_eq!(7, list.get_item(0).unwrap().extract::<i32>().unwrap());
406+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
407+
assert_eq!(2, list.get_item(2).unwrap().extract::<i32>().unwrap());
408+
assert_eq!(5, list.get_item(3).unwrap().extract::<i32>().unwrap());
400409
list.sort().unwrap();
401-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
402-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
403-
assert_eq!(5, list.get_item(2).extract::<i32>().unwrap());
404-
assert_eq!(7, list.get_item(3).extract::<i32>().unwrap());
410+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
411+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
412+
assert_eq!(5, list.get_item(2).unwrap().extract::<i32>().unwrap());
413+
assert_eq!(7, list.get_item(3).unwrap().extract::<i32>().unwrap());
405414
});
406415
}
407416

@@ -410,15 +419,15 @@ mod tests {
410419
Python::with_gil(|py| {
411420
let v = vec![2, 3, 5, 7];
412421
let list = PyList::new(py, &v);
413-
assert_eq!(2, list.get_item(0).extract::<i32>().unwrap());
414-
assert_eq!(3, list.get_item(1).extract::<i32>().unwrap());
415-
assert_eq!(5, list.get_item(2).extract::<i32>().unwrap());
416-
assert_eq!(7, list.get_item(3).extract::<i32>().unwrap());
422+
assert_eq!(2, list.get_item(0).unwrap().extract::<i32>().unwrap());
423+
assert_eq!(3, list.get_item(1).unwrap().extract::<i32>().unwrap());
424+
assert_eq!(5, list.get_item(2).unwrap().extract::<i32>().unwrap());
425+
assert_eq!(7, list.get_item(3).unwrap().extract::<i32>().unwrap());
417426
list.reverse().unwrap();
418-
assert_eq!(7, list.get_item(0).extract::<i32>().unwrap());
419-
assert_eq!(5, list.get_item(1).extract::<i32>().unwrap());
420-
assert_eq!(3, list.get_item(2).extract::<i32>().unwrap());
421-
assert_eq!(2, list.get_item(3).extract::<i32>().unwrap());
427+
assert_eq!(7, list.get_item(0).unwrap().extract::<i32>().unwrap());
428+
assert_eq!(5, list.get_item(1).unwrap().extract::<i32>().unwrap());
429+
assert_eq!(3, list.get_item(2).unwrap().extract::<i32>().unwrap());
430+
assert_eq!(2, list.get_item(3).unwrap().extract::<i32>().unwrap());
422431
});
423432
}
424433

@@ -427,8 +436,49 @@ mod tests {
427436
Python::with_gil(|py| {
428437
let array: PyObject = [1, 2].into_py(py);
429438
let list = <PyList as PyTryFrom>::try_from(array.as_ref(py)).unwrap();
430-
assert_eq!(1, list.get_item(0).extract::<i32>().unwrap());
431-
assert_eq!(2, list.get_item(1).extract::<i32>().unwrap());
439+
assert_eq!(1, list.get_item(0).unwrap().extract::<i32>().unwrap());
440+
assert_eq!(2, list.get_item(1).unwrap().extract::<i32>().unwrap());
441+
});
442+
}
443+
444+
#[test]
445+
fn test_list_get_item_invalid_index() {
446+
Python::with_gil(|py| {
447+
let list = PyList::new(py, &[2, 3, 5, 7]);
448+
let obj = list.get_item(5);
449+
assert!(obj.is_err());
450+
assert_eq!(
451+
obj.unwrap_err().to_string(),
452+
"IndexError: list index out of range"
453+
);
454+
});
455+
}
456+
457+
#[test]
458+
fn test_list_get_item_sanity() {
459+
Python::with_gil(|py| {
460+
let list = PyList::new(py, &[2, 3, 5, 7]);
461+
let obj = list.get_item(0);
462+
assert_eq!(obj.unwrap().extract::<i32>().unwrap(), 2);
463+
});
464+
}
465+
466+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
467+
#[test]
468+
fn test_list_get_item_unchecked_sanity() {
469+
Python::with_gil(|py| {
470+
let list = PyList::new(py, &[2, 3, 5, 7]);
471+
let obj = unsafe { list.get_item_unchecked(0) };
472+
assert_eq!(obj.extract::<i32>().unwrap(), 2);
473+
});
474+
}
475+
476+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
477+
#[test]
478+
fn test_list_get_item_unchecked_invalid_index() {
479+
Python::with_gil(|py| {
480+
let list = PyList::new(py, &[2, 3, 5, 7]);
481+
unsafe { list.get_item_unchecked(5) };
432482
});
433483
}
434484
}

0 commit comments

Comments
 (0)