Repository: pandas-dev/pandas

Issue:
Subclassed attributes are not serializable
In a subclass of pandas objects, pickling an object doesn't serialize properties of an instance of that subclass, even if the attribute has been added to `_metadata`. It would be hard to override this behavior entirely in the subclass, because it will require updates to `__getstate__` and `__setstate__`, and probably also an addition or subclass of `BlockManager`. It would be nice if the `_metadata` serialization was handled in the base pandas class.

Example:

```
class SubDataFrame(DataFrame):

    _metadata = ['my_data']

    @property
    def _constructor(self):
        return SubDataFrame

sdf = SubDataFrame()
sdf.my_data = 'foo'
sdf.to_pickle('tmp.pkl')
new_sdf = read_pickle('tmp.pkl')
new_sdf.my_data
```

raises `AttributeError: 'SubDataFrame' object has no attribute 'my_data'`

(edited original example for correctness)



Discussion:
I think if this were replaces `core/generic.py` (and no need to have the `Series.__getstate__` either)
then this would work (and be back-compat). The reconstruction accepts a dict of these things

```
 def __getstate__(self):
        meta = dict([ (k, getattr(self, k, None)) for k in self._metadata ])
        return dict(_data=self._data, **meta)
```

Want to do a pull-request (already lots of tests on prior version pickle recovery in `pandas/io/tests/test_pickle.py` so that will test the back-compat)


Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -80,8 +80,8 @@ class NDFrame(PandasObject):
     """
     _internal_names = ['_data', '_cacher', '_item_cache', '_cache',
                        'is_copy', '_subtyp', '_index',
-                       '_default_kind', '_default_fill_value',
-                       '__array_struct__','__array_interface__']
+                       '_default_kind', '_default_fill_value', '_metadata',
+                       '__array_struct__', '__array_interface__']
     _internal_names_set = set(_internal_names)
     _accessors = frozenset([])
     _metadata = []
@@ -760,7 +760,9 @@ def to_dense(self):
     # Picklability
 
     def __getstate__(self):
-        return self._data
+        meta = dict((k, getattr(self, k, None)) for k in self._metadata)
+        return dict(_data=self._data, _typ=self._typ,
+                    _metadata=self._metadata, **meta)
 
     def __setstate__(self, state):
 
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -438,10 +438,6 @@ def imag(self, v):
     __long__ = _coerce_method(int)
     __int__ = _coerce_method(int)
 
-    # we are preserving name here
-    def __getstate__(self):
-        return dict(_data=self._data, name=self.name)
-
     def _unpickle_series_compat(self, state):
         if isinstance(state, dict):
             self._data = state['_data']


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -45,7 +45,8 @@
                                  assertRaisesRegexp,
                                  assertRaises,
                                  makeCustomDataframe as mkdf,
-                                 ensure_clean)
+                                 ensure_clean,
+                                 SubclassedDataFrame)
 from pandas.core.indexing import IndexingError
 from pandas.core.common import PandasError
 
@@ -14501,16 +14502,8 @@ def test_assign_bad(self):
 
     def test_dataframe_metadata(self):
 
-        class TestDataFrame(DataFrame):
-            _metadata = ['testattr']
-
-            @property
-            def _constructor(self):
-                return TestDataFrame
-
-
-        df = TestDataFrame({'X': [1, 2, 3], 'Y': [1, 2, 3]},
-                           index=['a', 'b', 'c'])
+        df = SubclassedDataFrame({'X': [1, 2, 3], 'Y': [1, 2, 3]},
+                                 index=['a', 'b', 'c'])
         df.testattr = 'XXX'
 
         self.assertEqual(df.testattr, 'XXX')
@@ -14519,6 +14512,11 @@ def _constructor(self):
         self.assertEqual(df.iloc[[0, 1], :].testattr, 'XXX')
         # GH9776
         self.assertEqual(df.iloc[0:1, :].testattr, 'XXX')
+        # GH10553
+        unpickled = self.round_trip_pickle(df)
+        assert_frame_equal(df, unpickled)
+        self.assertEqual(df._metadata, unpickled._metadata)
+        self.assertEqual(df.testattr, unpickled.testattr)
 
     def test_to_panel_expanddim(self):
         # GH 9762
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -1856,3 +1856,11 @@ def inner(*args, **kwargs):
                 thread.join()
         return inner
     return wrapper
+
+
+class SubclassedDataFrame(DataFrame):
+    _metadata = ['testattr']
+
+    @property
+    def _constructor(self):
+        return SubclassedDataFrame