biqqles/dataclassy

RFC: using `__call__` instead of `__new__`

Closed this issue · 1 comments

So it's that time of year when I finally have a bit more time to experiment with my projects. I've been thinking about the uninspiring performance when (and only when) a custom __init__ is used, as @TylerYep pointed out in #6 (comment). This arises because the current __call__ implementation used is generic and dynamically modifies the parameters to either __new__ or __init__ - what would be really good is to generate a static __call__ that does this with no overhead. To keep performance good and code simple, this method would both initialise the instance with its parameters and redirect any additional ones to __init__ if it's defined. However, because an object's __call__ method has to be defined on its type (i.e. in the case of a class its metaclass), this means we have to dynamically create a subclass of the metaclass at the time of decorator use.

I've implemented this, and though it works surprisingly well in my other tests, it breaking multiple inheritance (highlighted below) means it's nowhere near ready for rolling out in releases yet. Despite this, I'm curious to see how well it works in the code of others, if you would like to test it. The code is in the branch static-call.

Advantages

  • As-fast-as-possible initialiser performance
  • Even simpler code in dataclassy since it only has to work with one method (__call__), not __call__, __new__ and __signature__
  • The current use of __new__ could be considered hacky as it's not "supposed" to be used to initialise the class instance

Disadvantages

  • You (the user) can no longer call __new__ on a data class to instantiate it without __init__ being executed. I'm not sure if this is actually useful (or a good idea to do), but it is a feature nonetheless
  • Say class B is a subclass of A. With this method, type(A) is not type(B) which is unusual and surprising. Only issubclass(type(B), type(A)) is true. However, Python supports this "dynamic metaclass" paradigm fine. Besides looking odd (and how often do you compare the types of classes, really!) I've found no side effects other than...
  • Multiple inheritance now becomes ugly. Whereas before it worked perfectly (e.g. class C(A, B), now you have to do something like
class CMeta(type(A), type(B)):
    pass

@dataclass(meta=CMeta)
class C:
    ...

Superseded by #12.