Dear Python Developers,
we have a potential idea for enhancing Python. Below you will find what the PEP might look like. A reference implementation has been written and will be posted in a follow up message. We are look forward to hearing your feedback and ideas.
Pim Schellart & Nate Lust
This PEP proposes the introduction of new syntax to create a community standard, readable way to continue a definition for classes which are already defined.
Extending existing classes with new members currently requires the members to be defined outside of class scope and then assigned to the class, as in::
def foo(self): pass A.foo = foo del foo
This pattern frequently occurs when extending classes from extension modules written in other languages, but is also not uncommon in pure Python code.
This syntax is however cumbersome and error prone for the following reasons:
1. The name of the new member might clash with an existing name at module scope. 2. The developer might forget the assignment. 3. The developer might forget to delete the temporary, leaving it at module scope where it is meaningless (or worse non functional as a stand alone function).
Alternatives are to use inheritance, lambda or a decorator.
Inheritance is not a good option for use with Python extension modules (written in for instance C / C++). The reason is that if in the other language the inheritance relation is A<-B, and this is exposed to Python, we can't add functionality to A in Python by subclassing A<-A' without also introducing B' that inherits from B and A'. Thus one change propagates all the way down the inheritance chain. Inheritance with pure Python classes exhibit the same issue.
It might be tempting to avoid (1) and (2) by subclassing, reusing the name of the base class. However this approach suffers from two issues. The first issue, like above, has to do with the inheritance relation. If A subclasses A and B subclasses A (A<-A<-B), then calls to super in B may have unexpected behavior (anything using the method resolution order may exhibit the same unexpectedness). The second issue arises when class B inherits from class A before it is extended (A<-B) and then class A is extended though inheritance (A<-A). Class A will now have new methods and data members which are not present in B, as it is a subclass only of the original A. This would be confusing to anyone examining B as it would be apparently missing members of it's parent.
Adding attributes to a class using lambda is not equivalent because it only allows for expressions and not statements.
A class decorator (say "@continue_class(A)" that takes methods and attributes from the class and attaches them to the wrapped type) works, but is cumbersome and requires a new class name to be introduced for each extended type. Thus suffering from the same problems numbered (1) and (2) above.
A function decorator could extend a class, but also requires defining a new name and suffers from problems (1) and (2).
This proposal adds the keyword combination "continue class" which instructs the interpreter to add members to an existing class. This syntax requires no new keywords and allows all existing Python code to continue to function with no modification.
By combining existing keywords it avoids name clashes. Precedents for this exists in "is not" and "yield from".
Another big advantage of the "continue class" syntax is discoverability. For humans, the meaning of the syntax is evident, the class will be continued with the following block. Automated tools will also benefit from this syntax as parsing a file to discover the complete definition of a class will be easier than trying to detect 'monkey patching'.
The following two snippets are semantically identical::
continue class A: x = 5 def foo(self): pass def bar(self): pass
def foo(self): pass def bar(self): pass A.x = 5 A.foo = foo A.bar = bar del foo del bar
An alternative could be to allow a function definition to include a class specifier as in::
def A.foo(self): pass
Adapting Python's grammar to support the continue class keyword combination requires modifying the grammar file to parse for the new keyword combination. Additional changes to Python.asdl, ast.c, compile.c, symtable.c will be required to process the parsed syntax to byte code.
A reference implementation (already written and posted separately) parses the continue class line for the name of the class to be modified and loads it. Next the body of the block is compiled into a code object scoped by the name of the class. The class and the code are used as arguments to a new built-in module, called __continue_class__. This function evaluates the code block, passing the results to setattr on the supplied class.