Python: Pozor na deepcopy

cs in code

V práci jsme řešili takový nemilý problém, kdy v určitém stavu aplikace dokázala sežrat tolik paměti, až ji museli admini otočit. Ač to není nic dobrého, měli jsme štěstí. Zjistili jsme totiž, při jaké akci se to děje. Tudíž jsme si mohli z poloviny oddychnout…

Jednalo se o operaci, kde se prováděla obrovská kopie pomocí deepcopy. Instance nebyly malé, ale ani zase tak obrovské, aby se využití paměti po operaci navýšilo o pár řádů. Nedávno jsme přidávali zpětné reference a tak jsem se na ně zaměřil. I přesto, že jsme se jim se spojením deepcopy věnovali.

Překvapivě netrvalo dlouho nalézt problém. Měli jsme takovouto strukturu:

Container
 - Item (s referencí na container)
 - Item (opět s referencí na container)
…

Při kopírování celého containeru si deepcopy se zpětnou referencí poradí hravě. Pokud jste někdy přetěžovali magickou metodu __deepcopy__, jistě víte, že tam je parametr memo. Zde se předává slovník s objekty, které už byly zkopírované. Klíč je ID staré instance a hodnota je zkopírovaná instance. Tudíž když se má kopírovat znovu stejná instance, vezme se odsud. Programátor však tohle řešit nemusí, Python se postará sám.

Problém nastane, pokud se rozhodneme kopírovat itemy postupně.

for item in container:
    new_container.append(copy.deepcopy(item))

Vidíte to také? S každou položkou se zkopíruje i container. A nejen ten – container má referenci na položky, takže se zkopírují s každou položkou všechny položky! Nejenom, že se reference úplně rozjedou, ale navíc to při velkých strukturách sežere vše, co najde.

Možná řešení jsou dvě. Buď předávat memo, jenže to by musel řešit container a při kopírování pouze položek odjinud by vznikal problém znovu; nebo před kopírováním položky odmazat zpětnou referenci a po operaci ji vrátit. My zvolili druhou možnost.

Tím se problém téměř vyřešil. Bohužel se stále při kopírování mírně navyšovala podezřele paměť. Hledal jsem tedy dál a po čase jsem našel příčinu – flyweight. To je pattern, kdy je v celém programu jen jedna instance jednoho objektu. Využíváme to s úspěchem u číselníků. Představte si to jako modely v Djangu a kdykoliv si vyžádáte nějaký záznam, nedostanete celý objekt, ale jen referenci na již existující. U naší velké aplikace flyweight hodně pomohl s optimalizací.

Každopádně jsem až po letech využívání zjistil, že deepcopy nečetl vůbec nic o patternech, natož nic o flyweight, a v klidu nám tyto instance kopíroval. Stačilo do flyweight třídy implementovat metodu __deepcopy__, aby vracela self, a nyní je vše v pořádku.

Deepcopy může hodně potrápit. Před použitím si zkuste v hlavě představit, co se stane. Detailně projděte, co kopírujete. Sice vás uživatelé nepoplácají po zádech, že myslíte na vše možné, ale lepší než ve stresu řešit provozní problémy. :-)





You may also like