11

i am implementing a python class that provides some nested data structure. i want to add support for copying through copy.copy() and deep copying through copy.deepcopy(), which, as the docs for the copy module describe, involves writing __copy__() and __deepcopy__ special methods.

i know how to teach my class to make a copy of itself, but i want to avoid going through __init__() on the new instance, since __init__() does some things that my copying logic does not want (or need) to do.

what i ended up with is this method, which works as intended:

def __copy__(self):
    cls = type(self)
    obj = cls.__new__(cls)
    # custom copying logic that populates obj goes here
    return obj

my question is: is calling cls.__new__(cls) the right approach for a __copy__() implementation that wants to skip __init__() for the copy? or is there a more "pythonic" approach that i overlooked?

wouter bolsterlee
  • 3,879
  • 22
  • 30
  • The only alternative I can think of is to remove the stuff you don't want running more than once from `__init__` and putting it in some other method instead. That way your calling code can choose whether or not to run that part after creating an instance. – Blckknght May 28 '17 at 20:00
  • You might add a copy flag parameter to the `init` so as to differentiate the different initializations. Right now, if you make changes to your class, you would always have to check two methods.. – user2390182 May 28 '17 at 20:43
  • the __init__() signature is fixed and cannot grow any more (keyword) arguments since the constructor eats all *args and **kwargs to populate the data structure (same behaviour as the dict() constructor). – wouter bolsterlee May 29 '17 at 09:41
  • It should be possible to pop special kwargs from the dict and handle them separately...right? – Andras Deak -- Слава Україні May 29 '17 at 10:12
  • nope, since the constructor takes *args and **kwargs like dict() so __init__() is completely "taken". – wouter bolsterlee May 29 '17 at 10:42
  • I mean rewriting the constructor... so that is doesn't "take" every kwarg, but first pops off a few things. I believe I mean [something like this](https://stackoverflow.com/a/2033554/5067311). (Also: if you don't @notify other users, they won't see a notification of your comment) – Andras Deak -- Слава Україні May 29 '17 at 11:25
  • @AndrasDeak that would mean that that special argument cannot be used for other purposes, which is wrong in this case, since my constructor behaves like the dict() constructor and must not treat any of its arguments in a special way. – wouter bolsterlee May 29 '17 at 13:49
  • How close to the behaviour of the dict constructor? You could use a non-hashbable special key that is not allowed to be a key in a dict. But obviously this hinges on your class behaving similarly to dicts. – Andras Deak -- Слава Україні May 29 '17 at 14:44
  • @AndrasDeak exactly the same as a dict since it is supposed to be a dict with fancy extra features for nested lookups and type checking. (open source, not yet publicized, but available on my github.) – wouter bolsterlee May 29 '17 at 16:51
  • @wouterbolsterlee I see that you [rolled back my approved edit](https://stackoverflow.com/posts/44231316/revisions) to add the [tag:copy] and [tag:deep-copy] tags along with some general cleanup. Is there a reason why you did this? – Knowledge Cube Jun 07 '17 at 13:12
  • sorry, removing tags was unintentional. what i meant to roll back is random capitalisation "fixes" to stuff i wrote. that is not how i write (for various reasons not relevant here) yet my sig is below it, so if that is literally the only thing that "improves" i consider it unwanted interference with my writing style. it annoys me if random strangers change things with my name on them with no valuable contribution but pedantry. it also wastes my time with useless notifications, so i actively discourage that practice by reverting those changes. nothing personal, but i alone decide how i write :) – wouter bolsterlee Jun 07 '17 at 17:03

1 Answers1

1

I don't know if this is more pythonic, but you could use a flag.

from collections import Mapping
from copy import copy, deepcopy


class CustomDict(dict, Mapping):
    _run_setup = True

    def __init__(self, *args, **kwargs):
        self._dict = dict(*args, **kwargs)
        if args and isinstance(args[0], CustomDict):
            self._run_setup = args[0]._run_setup
        if self._run_setup:
            print("Doing some setup stuff")
        else:
            try:
                print("Avoiding some setup stuff")
            finally:
                self._run_setup = True

    def __getitem__(self, key):
        return self._dict[key]

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)

    def __copy__(self):
        self._run_setup = False
        try:
            copied_custom_dict = CustomDict(self)
        finally:
            self._run_setup = True
        return copied_custom_dict

In the __init__ above, the conditional setup stuff is only done if _run_setup = True. The only way to avoid this is by calling CustomDict with the first parameter being an instance of itself with _run_setup = False. This way, it's easy to flip the setup switch in different methods.

The try...finally blocks look clunky to me, but it's a way to make sure every method starts and ends with _run_setup = True.

Nathan Werth
  • 5,093
  • 18
  • 25
  • to me it seems that CustomDict(some_other_custom_dict_instance) is perfectly normal so that should exhibit "standard" behaviour. that means "if args and isinstance(args[0], CustomDict):" is not a test that can be used, i think. – wouter bolsterlee Jun 06 '17 at 20:52
  • hmm discard my previous comment since the _original_ is temporarily modified to set the flag. that is indeed a way to propagate the flag. but mutating state for a read-only operation _feels_ wrong, and will break with multi-threading. – wouter bolsterlee Jun 08 '17 at 14:14