How does Python class system actually compare to Lisp CLOS?
I've seen arguments for Python class system is the blocker for important code optimizations, AOT or JIT. Are there elaborate explanations on why we get near-machine code speed for compiled SBCL Lisp but we cannot even save the image for PyPy runs? Seems like a problem worse than GIL.
Who says this? Python does expose a bit of the plumbing of its class/object system - but I can't recall seeing anyone calling it especially powerful or flexible?
I don't think I've seen anyone argue for any object system being strictly more flexible/powerful than CLOS.
Some, like smalltalk, ruby, Dylan or Javascript original prototype based object system might be useful, simpler subsets of CLOS - and might so arguably be "better".
Afaik much of the problem with python has to do with the language being quite dynamic, and also somewhat complex scope handling.
One CLOS feature that is particularly interesting is multi-dispatch. When you call a method, most languages implement polymorphism by looking at the class of the object and call the corresponding method in the class. In CLOS, it looks at all the parameters of the method. This is similar to pattern matching in functional programming languages like Elixir.
But not for CLOS code. CLOS is a subset of Common Lisp and it addresses flexibility, expressiveness, runtime dynamics AND decent speed. But fully dynamic CLOS code (with added meta-level) will not be 'machine level' speed.
See Julia for a more recent approach to multimethods and performance. There are also CLOS optimizations in the past and some newer approaches -> but these usually lead to less runtime dynamics.
Nah. Python's metaprotocol is inflexible compared to other similarly-dynamic languages.
Here, let's try to monkeypatch a method onto a hierarchy of existing classes, calling the superclass method from time to time.
First, here's our existing hierarchy:
class Base: pass
class Derived(Base): pass
This is the decorator that will do the monkeypatching. Any other way of doing the monkeypatching will fall foul of the error we're about to see. There's no way around it. def extend(cls):
def extender(f):
setattr(cls, f.__name__, f)
return f
return extender
OK, let's add `foo` to Base: @extend(Base)
def foo(self):
print('I am a base!')
and to Derived, calling the superclass: @extend(Derived)
def foo(self):
print('I am a derived!')
super().foo()
print('I am still a derived!')
Finally, let's try it: Derived().foo()
Oh! What's this?? ~$ python t.py
I am a derived!
Traceback (most recent call last):
File "/home/tonyg/t.py", line 20, in
Derived().foo()
File "/home/tonyg/t.py", line 17, in foo
super().foo()
RuntimeError: super(): __class__ cell not found
Huh!---
Turns out in situations like this you have to hold the runtime's hand by supplying `super(Derived, self)` instead of `super()` in the `foo` in Derived. It's to do with how the compiler statically (!) assigns information about the superclass hierarchy using magic closure variables at compile-time (!).
There may be some insane magic tricks one could do to make this work, maybe. Things like reaching into a closure, rebuilding it, adding new slots, preserving existing bindings, avoiding accidental capture, making sure everything lines up just right. It... didn't seem like a good idea to put insane magic in production, so I did the traditional Python thing: swallowed my discomfort and pressed on with the stupid workaround for the bad design in order to accomplish something like what I was trying to achieve.
I for one have never heard that said about Python; if this was on Wikipedia I'd definitely be mumbling Weasel Word[1] now...
I can write OO code as well, this is just personal preference.
However, as far as optimization, the point is that Python's class system is implicated in almost every line of code. Most operators are actually invocations of corresponding "dunder" methods on their operands, which can be potentially changed, and whose invocation is actually surprisingly complicated and difficult to optimize.
Common Lisp, of course, does not have operators in the same sense, just functions. However, the analogous functions, like +, *, aref, etc. are not generic functions in the sense of CLOS. They only take built-in data types as arguments and cannot be overloaded. This lack of extensibility makes it easier for the compiler to know what actual code is being invoked and to optimize their use. Arguably, this makes Common Lisp seem like a less flexible language, and in some ways its design does pay more attention to optimization than its reputation would have you believe. On the other hand, the lisp syntax means that there's no such thing as a finite set of operators that you'd want to overload. If you want a different type of multiplication, you can just use a different function.
Expert may pitch in but it feels like a regression (even though I understand the social dynamics at play)