New submission from Dan Snider <mr.assume.a...@gmail.com>:
Right now, you really gotta jump through hoops in some cases if you only want to use __new__ and don't care about __init__ (is there ever a time where you'd use both?). The problem originates in type.__call__. I'm attaching a full Python implementation of type_call/object_init at the end, but here's a tl;dr representation of the issue i type_call (where the whole thing can be basically summarized as this): def type_call(type, *args, **kws): r = type.__new__(type, *args, **kws) if not issubclass(r, type): type(r).__init__(r, *args, **kws) return r So if type(r).__init__ is object.__init__ or some other method with an incompatible signature to the metaclass's, errors are raised, which leads to having to implement really annoying workarounds. The annoyingness is compounded further by the fact that all object_init does is make sure it didn't receive any arguments. All of that can be avoided by setting __init__ in the class to None, which would have the same effect as setting tp_init to NULL on an extension type. Perhaps with a caveat that the only current reachable __init__ is object.__init__? I don't really find myself in situations involving the need for __new__ too often, but wouldn't this have a profoundly positive impact on pickle as well? One thing I can think of is there can be a universal pickle implementation for extension types with Py_TPFLAGS_BASETYPE set and a nonzero dict offset since if it detected a NULL init all it has to do is memcpy the base struct and pickle the instance dict. Anyway, the C code is pretty easy to understand but here's a Python implementation almost exactly equivalent that shows a case where a metaclass can be used as a callable that accepts an arbitrary number of arguments instead of a forced three and returns a class: PyType_Type = type Py_TYPE = type PyType_IsSubtype = issubclass PyTuple_Check = tuple.__instancecheck__ PyDict_Check = dict.__instancecheck__ PyDict_GET_SIZE = dict.__len__ PyTUple_GET_SIZE = tuple.__len__ NULL = 0 ALLOW_NULL_INIT = False def excess_args(args, kws): return args or kws class Object: def __init__(self, *args, **kws): # object_init literally does nothing but check for zero len args/kws # (bit of a tangent but why is excess_args not a macro...?) if ALLOW_NULL_INIT: return tp = Py_TYPE(self) print('Object.__init__, type is:', tp.__name__) if excess_args(args, kws): if tp.__init__ is object.__init__: raise TypeError("object.__init__() takes no arguments") raise TypeError(f'{tp.__name__}.__init__() takes no arguments') def fake_init(*args, **kws): pass class Type(type): def __getattribute__(self, attr): value = type.__getattribute__(self, attr) if attr == '__init__': if ALLOW_NULL_INIT and value is None: return fake_init return super(type(self).__mro__[1], self).__init__ return value def __call__(tp, *args, **kws): if getattr(tp, '__new__', 0) == NULL: raise TypeError(f"cannot create {tp.__name__} instances") obj = tp.__new__(tp, *args, **kws) if (tp is type and PyTuple_Check(args) and PyTuple_GET_SIZE(args)== 1 and (kws == NULL or (PyDict_Check(kws) and PyDict_GET_SIZE(kws)==0))): return obj if not PyType_IsSubtype(Py_TYPE(obj), tp): return obj tp = Py_TYPE(obj) # Here's where the problem is. What could possibly be negatively # affected by this? if getattr(tp, '__init__', 0): res = tp.__init__(obj, *args, **kws) return obj def autoname(arg): return '_'.join(arg.split()) def autobase(opt): return () if not opt else (opt,) class MetaBase(Object, type): pass class Mixin1: pass class Meta(MetaBase, metaclass=Type): __init__ = None def __new__(metacls, name, opt=None): return super().__new__(metacls, autoname(name), autobase(opt), {}) if __name__ == '__main__': try: foobar = Meta('foo bar', Mixin1) except TypeError as er: print(f'could not make foobar because: {er}') print('setting ALLOW_NULL_INIT to True;') ALLOW_NULL_INIT = True foobar = Meta('foo bar', Mixin1) print('foobar.__name__ is:', foobar.__name__) print('foobar__mro__ is:', foobar.__mro__) ---------- components: Interpreter Core messages: 322907 nosy: bup priority: normal severity: normal status: open title: Like __hash__, allow setting MyClass.__init__ to None to prevent it being called by type.__call__ type: enhancement versions: Python 3.8 _______________________________________ Python tracker <rep...@bugs.python.org> <https://bugs.python.org/issue34314> _______________________________________ _______________________________________________ Python-bugs-list mailing list Unsubscribe: https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com