Почему атрибуты теряются после копирования Pandas DataFrame

Почему невозможно передать атрибуты экземпляра через копию? Я хочу передать атрибут name другому фрейму.

import copy
df = pd.DataFrame([1,2,3])
df.name = 'sheet1'
df2 = copy.deepcopy(df)

print(f'df.name: {df.name}')
>> df.name: sheet1

print(f'df2.name: {df2.name}')
>>    AttributeError    
        ...      
      'DataFrame' object has no attribute 'name'

Точно так же почему это также не работает, когда создается класс и наследуется от него?

class DfWithName(pd.DataFrame):

    def __init__(self, *args, **kwargs):
        self.__init__ = super().__init__(*args, **kwargs)
        print('lol')

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

и используя тот же код:

import copy
df = DfWithName([1,2,3])
df.name = 'sheet1'
df2 = copy.deepcopy(df) 
print(f'df.name: {df2.name}')
>>    AttributeError    
        ...      
      'DataFrame' object has no attribute 'name'

Ответ 1

Как отмечено в другом месте, класс DataFrame имеет собственный метод __deepcopy__ который не обязательно копирует произвольные атрибуты, назначенные экземпляру, как и для обычного объекта.

Интересно, что существует внутренний атрибут _metadata который, как представляется, предназначен для отображения дополнительных атрибутов NDFrame которые должны храниться при копировании/сериализации. Это обсуждается здесь: https://github.com/pandas-dev/pandas/issues/9317

К сожалению, это до сих пор считается недокументированной внутренней деталью, поэтому его, вероятно, не следует использовать. От взгляда на код вы можете в принципе сделать:

mydf = pd.DataFrame(...)
mydf.name = 'foo'
mydf._metadata += ['name']

и когда вы его копируете, он должен взять имя вместе с ним.

Вы можете подклассифицировать DataFrame чтобы сделать это по умолчанию:

import functools

class NamedDataFrame(pd.DataFrame):
    _metadata = pd.DataFrame._metadata + ['name']

    def __init__(self, name, *args, **kwargs):
        self.name = name
        super().__init__(*args, **kwargs)

    @property
    def _constructor(self):
        return functools.partial(self.__class__, self.name)

Вы также можете сделать это, не полагаясь на этот внутренний атрибут _metadata если вы предоставите свою собственную оболочку существующему методу copy и, возможно, также __getstate__ и __setstate__.

Обновление: Кажется, на самом деле использовать в _metadata атрибута для расширения классов панды теперь документировано. Таким образом, приведенный выше пример должен работать более или менее. Эти документы больше подходят для разработки самого Pandas, поэтому он может быть немного изменчивым. Но именно так NDFrame сама расширяет подклассы NDFrame.

Ответ 2

copy.deepcopy будет использовать собственный метод __deepcopy__ если он найден в MRO, который может вернуть все, что ему нравится (включая полностью фиктивные результаты). Действительно, __deepcopy__ метод __deepcopy__:

def __deepcopy__(self, memo=None):
    if memo is None:
        memo = {}
    return self.copy(deep=True)

Он делегирует self.copy, где вы найдете эту заметку в docstring:

Notes
-----
When ''deep=True'', data is copied but actual Python objects
will not be copied recursively, only the reference to the object.
This is in contrast to 'copy.deepcopy' in the Standard Library,
which recursively copies object data (see examples below).

И вы найдете в примечаниях к выпуску v0.13 (объединенных в PR 4039):

__deepcopy__ теперь возвращает неглубокую копию (в настоящее время: представление) данных, что позволяет изменять метаданные.

Связанная тема: 17406.

Ответ 3

Прикрепление пользовательских метаданных к DataFrames кажется неподдерживаемым для панд. См. Этот ответ (возможно дублировать?) И этот вопрос github.

Ответ 4

Этот код работает:

>>> class test():
...     @property
...     def name(self):
...         return self._name
...     @name.setter
...     def name(self, value):
...         self._name = value
...
>>>
>>> a = test()
>>> a.name = 'Test123'
>>> import copy
>>> a2 = copy.deepcopy(a)
>>> print(a2.name)
Test123

поэтому я думаю, что поведение определяется pd.DataFrame

Я обнаружил, что панды определяют функцию __deepcopy__, но я не могу полностью понять причину.

панды/ядро/индексы/base.py # l960