Skip to content

Commit 9600371

Browse files
author
Antoine Pitrou
committed
ENH: avoid creating reference cycle on indexing (#15746)
1 parent e1dabf3 commit 9600371

File tree

4 files changed

+38
-22
lines changed

4 files changed

+38
-22
lines changed

pandas/_libs/lib.pyx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,5 +1839,27 @@ cdef class BlockPlacement:
18391839
return self._as_slice
18401840

18411841

1842+
cdef class _NDFrameIndexerBase:
1843+
'''
1844+
A base class for _NDFrameIndexer for fast instantiation and attribute
1845+
access.
1846+
'''
1847+
cdef public object obj, name, _ndim
1848+
1849+
def __init__(self, name, obj):
1850+
self.obj = obj
1851+
self.name = name
1852+
self._ndim = None
1853+
1854+
@property
1855+
def ndim(self):
1856+
# Delay `ndim` instantiation until required as reading it
1857+
# from `obj` isn't entirely cheap.
1858+
ndim = self._ndim
1859+
if ndim is None:
1860+
ndim = self._ndim = self.obj.ndim
1861+
return ndim
1862+
1863+
18421864
include "reduce.pyx"
18431865
include "inference.pyx"

pandas/core/generic.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1841,23 +1841,12 @@ def to_latex(self, buf=None, columns=None, col_space=None, header=True,
18411841
@classmethod
18421842
def _create_indexer(cls, name, indexer):
18431843
"""Create an indexer like _name in the class."""
1844+
from functools import partial
18441845

18451846
if getattr(cls, name, None) is None:
1846-
iname = '_%s' % name
1847-
setattr(cls, iname, None)
1848-
1849-
def _indexer(self):
1850-
i = getattr(self, iname)
1851-
if i is None:
1852-
i = indexer(self, name)
1853-
setattr(self, iname, i)
1854-
return i
1855-
1847+
_indexer = partial(indexer, name)
18561848
setattr(cls, name, property(_indexer, doc=indexer.__doc__))
18571849

1858-
# add to our internal names set
1859-
cls._internal_names_set.add(iname)
1860-
18611850
def get(self, key, default=None):
18621851
"""
18631852
Get item from object for given key (DataFrame column, Panel slice,

pandas/core/indexing.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from pandas.core.common import (is_bool_indexer, _asarray_tuplesafe,
2424
is_null_slice, is_full_slice,
2525
_values_from_object)
26+
from pandas._libs.lib import _NDFrameIndexerBase
2627

2728

2829
# the supported indexers
@@ -85,19 +86,14 @@ class IndexingError(Exception):
8586
pass
8687

8788

88-
class _NDFrameIndexer(object):
89+
class _NDFrameIndexer(_NDFrameIndexerBase):
8990
_valid_types = None
9091
_exception = KeyError
9192
axis = None
9293

93-
def __init__(self, obj, name):
94-
self.obj = obj
95-
self.ndim = obj.ndim
96-
self.name = name
97-
9894
def __call__(self, axis=None):
9995
# we need to return a copy of ourselves
100-
new_self = self.__class__(self.obj, self.name)
96+
new_self = self.__class__(self.name, self.obj)
10197

10298
if axis is not None:
10399
axis = self.obj._get_axis_number(axis)
@@ -1321,7 +1317,7 @@ class _IXIndexer(_NDFrameIndexer):
13211317
13221318
"""
13231319

1324-
def __init__(self, obj, name):
1320+
def __init__(self, name, obj):
13251321

13261322
_ix_deprecation_warning = textwrap.dedent("""
13271323
.ix is deprecated. Please use
@@ -1333,7 +1329,7 @@ def __init__(self, obj, name):
13331329

13341330
warnings.warn(_ix_deprecation_warning,
13351331
DeprecationWarning, stacklevel=3)
1336-
super(_IXIndexer, self).__init__(obj, name)
1332+
super(_IXIndexer, self).__init__(name, obj)
13371333

13381334
def _has_valid_type(self, key, axis):
13391335
if isinstance(key, slice):

pandas/tests/indexing/test_indexing.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
import weakref
89
from warnings import catch_warnings
910
from datetime import datetime
1011

@@ -881,6 +882,14 @@ def test_partial_boolean_frame_indexing(self):
881882
columns=list('ABC'))
882883
tm.assert_frame_equal(result, expected)
883884

885+
def test_no_reference_cycle(self):
886+
df = pd.DataFrame({'a': [0, 1], 'b': [2, 3]})
887+
for name in ('loc', 'iloc', 'ix', 'at', 'iat'):
888+
getattr(df, name)
889+
wr = weakref.ref(df)
890+
del df
891+
assert wr() is None
892+
884893

885894
class TestSeriesNoneCoercion(object):
886895
EXPECTED_RESULTS = [

0 commit comments

Comments
 (0)