Como substituir as operações de cópia / cópia profunda para um objeto Python?

101

Eu entendo a diferença entre copye deepcopyno módulo de cópia. Usei copy.copye copy.deepcopyantes com sucesso, mas esta é a primeira vez que realmente estou sobrecarregando os métodos __copy__e __deepcopy__. Eu já pesquisei ao redor e olhou através do built-in módulos Python para procurar instâncias do __copy__e __deepcopy__funções (por exemplo sets.py, decimal.pye fractions.py), mas eu ainda não estou 100% de certeza que eu entendi direito.

Aqui está o meu cenário:

Eu tenho um objeto de configuração. Inicialmente, vou instanciar um objeto de configuração com um conjunto padrão de valores. Esta configuração será entregue a vários outros objetos (para garantir que todos os objetos comecem com a mesma configuração). No entanto, uma vez que a interação do usuário começa, cada objeto precisa ajustar suas configurações independentemente, sem afetar as configurações uns dos outros (o que me diz que vou precisar fazer cópias profundas de minha configuração inicial para entregar).

Aqui está um objeto de amostra:

class ChartConfig(object):

    def __init__(self):

        #Drawing properties (Booleans/strings)
        self.antialiased = None
        self.plot_style = None
        self.plot_title = None
        self.autoscale = None

        #X axis properties (strings/ints)
        self.xaxis_title = None
        self.xaxis_tick_rotation = None
        self.xaxis_tick_align = None

        #Y axis properties (strings/ints)
        self.yaxis_title = None
        self.yaxis_tick_rotation = None
        self.yaxis_tick_align = None

        #A list of non-primitive objects
        self.trace_configs = []

    def __copy__(self):
        pass

    def __deepcopy__(self, memo):
        pass 

Qual é a maneira correta de implementar os métodos copye deepcopyneste objeto para garantir copy.copye copy.deepcopyfornecer o comportamento adequado?

Brent escreve código
fonte
Funciona? Existem problemas?
Ned Batchelder
Achei que ainda estava tendo problemas com referências compartilhadas, mas é perfeitamente possível que eu tenha errado em outro lugar. Vou checar com base na postagem de @MortenSiebuhr quando tiver a chance e atualizar os resultados.
Brent escreve o código de
Do meu conhecimento atualmente limitado, eu esperaria que copy.deepcopy (ChartConfigInstance) retornasse uma nova instância que não teria nenhuma referência compartilhada com o original (sem reimplementar o deepcopy você mesmo). Isso está incorreto?
emschorsch

Respostas:

82

As recomendações para personalização estão no final da página de documentos :

As classes podem usar as mesmas interfaces para controlar a cópia que usam para controlar a decapagem. Consulte a descrição do módulo pickle para obter informações sobre esses métodos. O módulo de cópia não usa o módulo de registro copy_reg.

Para que uma classe defina sua própria implementação de cópia, ela pode definir métodos especiais __copy__()e __deepcopy__(). O primeiro é chamado para implementar a operação de cópia superficial; nenhum argumento adicional é passado. O último é chamado para implementar a operação de cópia profunda; ele recebe um argumento, o dicionário de memorandos. Se a __deepcopy__() implementação precisar fazer uma cópia detalhada de um componente, ela deve chamar a deepcopy()função com o componente como primeiro argumento e o dicionário de memorando como segundo argumento.

Uma vez que você parece não se importar com a personalização de decapagem, definir __copy__e __deepcopy__definitivamente parece ser o caminho certo para você.

Especificamente, __copy__(a cópia superficial) é muito fácil no seu caso ...:

def __copy__(self):
  newone = type(self)()
  newone.__dict__.update(self.__dict__)
  return newone

__deepcopy__seria semelhante (aceitando um memoargumento também), mas antes do retorno ele teria que chamar self.foo = deepcopy(self.foo, memo)qualquer atributo self.fooque precise de uma cópia profunda (essencialmente atributos que são contêineres - listas, dicts, objetos não primitivos que contêm outras coisas através de seus __dict__s).

Alex Martelli
fonte
1
@kaizer, eles podem personalizar a decapagem / retirada, bem como a cópia, mas se você não se preocupa com a decapagem, é mais simples e direto de usar __copy__/ __deepcopy__.
Alex Martelli,
4
Isso não parece ser uma tradução direta de cópia / cópia profunda. Nem copy nem deepcopy chamam o construtor do objeto que está sendo copiado. Considere este exemplo. class Test1 (object): def init __ (self): print "% s.% s"% (self .__ class .__ name__, " init ") class Test2 (Test1): def __copy __ (self): new = type (self) () return new t1 = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Rob Young
12
Acho que em vez de type (self) (), você deve usar cls = self .__ class__; cls .__ new __ (cls) para ser insensível à interface dos construtores (especialmente para subclasses). Entretanto, não é realmente importante aqui.
Juh_
11
Por que self.foo = deepcopy(self.foo, memo)...? Você realmente não quer dizer newone.foo = ...?
Alois Mahdal
4
O comentário de @Juh_ está correto. Você não quer ligar __init__. Não é isso que a cópia faz. Além disso, muitas vezes há um caso de uso em que a decapagem e a cópia precisam ser diferentes. Na verdade, nem sei por que o copy tenta usar o protocolo de decapagem por padrão. Copiar é para manipulação na memória, decapagem é para persistência entre épocas; são coisas completamente diferentes que têm pouca relação entre si.
Nimrod de
96

Juntando a resposta de Alex Martelli e o comentário de Rob Young, você obtém o seguinte código:

from copy import copy, deepcopy

class A(object):
    def __init__(self):
        print 'init'
        self.v = 10
        self.z = [2,3,4]

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

a = A()
a.v = 11
b1, b2 = copy(a), deepcopy(a)
a.v = 12
a.z.append(5)
print b1.v, b1.z
print b2.v, b2.z

estampas

init
11 [2, 3, 4, 5]
11 [2, 3, 4]

aqui __deepcopy__preenche o memodicionário para evitar cópias em excesso, caso o próprio objeto seja referenciado a partir de seu membro.

Antony Hatchkins
fonte
2
@bytestorm what is Transporter?
Antony Hatchkins
@AntonyHatchkins Transporteré o nome da minha classe que estou escrevendo. Para essa classe, desejo substituir o comportamento de deepcopy.
bytestorm
1
@bytestorm qual é o conteúdo Transporter?
Antony Hatchkins de
1
Eu acho que __deepcopy__deveria incluir um teste para evitar recursão infinita: <! - language: lang-python -> d = id (self) result = memo.get (d, None) se o resultado não for Nenhum: retornar resultado
Antonín Hoskovec
@AntonyHatchkins Não fica imediatamente claro em sua postagem onde memo[id(self)] realmente se acostuma para evitar recursão infinita. Reuni um pequeno exemplo que sugere que copy.deepcopy()internamente aborta a chamada para um objeto se id()for uma chave de memo, correto? Também é importante notar que deepcopy()parece fazer isso sozinho por padrão , o que torna difícil imaginar um caso em que definir __deepcopy__manualmente é realmente necessário ...
Jonathan H
14

Seguindo a excelente resposta de Peter , para implementar uma deepcopy customizada, com alterações mínimas na implementação padrão (por exemplo, apenas modificando um campo como eu precisava):

class Foo(object):
    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        cp.__deepcopy__ = deepcopy_method

        # custom treatments
        # for instance: cp.id = None

        return cp
Eino Gourdin
fonte
1
é preferível usar delattr(self, '__deepcopy__')então setattr(self, '__deepcopy__', deepcopy_method)?
joel
De acordo com essa resposta , ambos são equivalentes; mas setattr é mais útil ao definir um atributo cujo nome é dinâmico / desconhecido no momento da codificação.
Eino Gourdin
8

Não está claro para o seu problema por que você precisa sobrescrever esses métodos, uma vez que você não deseja fazer nenhuma customização nos métodos de cópia.

De qualquer forma, se você quiser personalizar a cópia profunda (por exemplo, compartilhando alguns atributos e copiando outros), aqui está uma solução:

from copy import deepcopy


def deepcopy_with_sharing(obj, shared_attribute_names, memo=None):
    '''
    Deepcopy an object, except for a given list of attributes, which should
    be shared between the original object and its copy.

    obj is some object
    shared_attribute_names: A list of strings identifying the attributes that
        should be shared between the original and its copy.
    memo is the dictionary passed into __deepcopy__.  Ignore this argument if
        not calling from within __deepcopy__.
    '''
    assert isinstance(shared_attribute_names, (list, tuple))
    shared_attributes = {k: getattr(obj, k) for k in shared_attribute_names}

    if hasattr(obj, '__deepcopy__'):
        # Do hack to prevent infinite recursion in call to deepcopy
        deepcopy_method = obj.__deepcopy__
        obj.__deepcopy__ = None

    for attr in shared_attribute_names:
        del obj.__dict__[attr]

    clone = deepcopy(obj)

    for attr, val in shared_attributes.iteritems():
        setattr(obj, attr, val)
        setattr(clone, attr, val)

    if hasattr(obj, '__deepcopy__'):
        # Undo hack
        obj.__deepcopy__ = deepcopy_method
        del clone.__deepcopy__

    return clone



class A(object):

    def __init__(self):
        self.copy_me = []
        self.share_me = []

    def __deepcopy__(self, memo):
        return deepcopy_with_sharing(self, shared_attribute_names = ['share_me'], memo=memo)

a = A()
b = deepcopy(a)
assert a.copy_me is not b.copy_me
assert a.share_me is b.share_me

c = deepcopy(b)
assert c.copy_me is not b.copy_me
assert c.share_me is b.share_me
Peter
fonte
O clone também não precisa __deepcopy__redefinir o método, pois terá __deepcopy__= Nenhum?
flutefreak7
2
Não. Se o __deepcopy__método não for encontrado (ou obj.__deepcopy__retornar Nenhum), então deepcopyrecorre à função de cópia profunda padrão. Isso pode ser visto aqui
Pedro,
1
Mas então b não terá a capacidade de copiar profundamente com compartilhamento? c = deepcopy (a) seria diferente de d = deepcopy (b) porque d seria um deepcopy padrão onde c teria alguns atributos compartilhados com a.
flutefreak7
1
Ah, agora eu entendo o que você está dizendo. Bom ponto. Eu consertei, eu acho, excluindo o __deepcopy__=Noneatributo falso do clone. Veja o novo código.
Pedro,
1
talvez claro para os especialistas em python: se você usar este código em python 3, altere "for attr, val in shared_attributes.iteritems ():" com "for attr, val in shared_attributes.items ():"
complexM
6

Eu posso estar um pouco fora dos detalhes, mas aqui vai;

Dos copydocumentos ;

  • Uma cópia superficial constrói um novo objeto composto e, em seguida (na medida do possível), insere nele referências aos objetos encontrados no original.
  • Uma cópia profunda constrói um novo objeto composto e então, recursivamente, insere nele cópias dos objetos encontrados no original.

Em outras palavras: copy()irá copiar apenas o elemento superior e deixar o resto como ponteiros na estrutura original. deepcopy()irá copiar recursivamente sobre tudo.

Ou seja, deepcopy()é o que você precisa.

Se você precisa fazer algo realmente específico, pode substituir __copy__()ou __deepcopy__(), conforme descrito no manual. Pessoalmente, eu provavelmente implementaria uma função simples (por exemplo, config.copy_config()ou algo semelhante) para deixar claro que esse não é o comportamento padrão do Python.

Morten Siebuhr
fonte
3
Para que uma classe defina sua própria implementação de cópia, ela pode definir métodos especiais __copy__() e __deepcopy__(). docs.python.org/library/copy.html
SilentGhost
Vou verificar meu código novamente, obrigado. Vou me sentir idiota se isso for um bug simples em outro lugar :-P
Brent Writes Code
@MortenSiebuhr Você está correto. Eu não estava totalmente certo de que copy / deepcopy faria qualquer coisa por padrão, sem que eu substituísse essas funções. Eu estava procurando um código real que eu possa ajustar mais tarde (por exemplo, se eu não quiser copiar todos os atributos), então eu dei a você um voto favorável, mas vou continuar com a resposta de @AlexMartinelli. Obrigado!
Brent escreve o código de
2

O copymódulo usa eventualmente o protocolo__getstate__() / pickling , portanto, esses também são alvos válidos para serem substituídos.__setstate__()

A implementação padrão apenas retorna e define o __dict__da classe, então você não precisa chamar super()e se preocupar com o truque inteligente de Eino Gourdin, acima .

Ankostis
fonte
1

Com base na resposta clara de Antony Hatchkins, aqui está minha versão em que a classe em questão deriva de outra classe personalizada (st precisamos chamar super):

class Foo(FooBase):
    def __init__(self, param1, param2):
        self._base_params = [param1, param2]
        super(Foo, result).__init__(*self._base_params)

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        super(Foo, result).__init__(*self._base_params)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, copy.deepcopy(v, memo))
        super(Foo, result).__init__(*self._base_params)
        return result
BoltzmannBrain
fonte