11 ms·
Ask HN: How is Python's OOP is superior vs. Lisp CLOS?
Python classes may be clunky but it is said that Python class system is more flexible than other languages' OOP, even Lisp CLOS.
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.
- jackdaniel 4y agoI've never heard that Python OOP is any good, not to even mention superior to other contemporary systems (like Java). Could you share the source where it is said that "Python class system is more flexible than other languages"?
- tyingq 4y agoI would guess it's talking about things like __str__, __repr__, __add__, __getitem__, __new__, _setattr__, @staticmethod, @property, @dataclass, and so on. I agree that doesn't necessarily mean "good", but it is flexible out of the box.
- smitty1e 4y agoThat is, python seems a relatively good set of enginering tradeoffs. Things more or less work as one would expect. Where it becomes challenging is a library using reflection extensively, like SQLAlchemy.
- p_l 4y agoThose are majorly bad bandaids over bad design in my experience, with a dash of legacy of magic names in a dict as implementation strategy... Nowhere close even to standard CLOS without using any of the MOP extensions.
- lispm 4y ago> Are there elaborate explanations on why we get near-machine code speed for compiled SBCL Lisp 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.
- nemoniac 4y agoI usually start a programming task CLOS by taking advantage of its flexibility,expressiveness and runtime dynamics. Then when I've got a better grasp of the structure of the task, I start to restrict that flexibility by using inlined generics and the like to get more speed. Much in the same way as I program Lisp in general, first getting the shape of the task and then honing it by adding type declarations gradually.
- lispm 4y agofinalize the classes in some implementations -> fixed class layout -> faster slot access Training CLOS caches. There are attempts to improve Generic Function performance, especially for the SBCL implementation. ...
- e12e 4y ago> (...) it is said that Python class system is more flexible than other languages' OOP, even Lisp CLOS. 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.
- mikelevins 4y ago"I don't think I've seen anyone argue for any object system being strictly more flexible/powerful than CLOS." I guess I'd argue that SK8's object system was more flexible and powerful than CLOS. It was an extension of CLOS in Macintosh Common Lisp, implemented with support from low-level modifications in MCL's CLOS implementation. It added, among other things, persistent objects, constraints, truth-maintenance, and several extensions to CLOS' method combinations and before, after, and around methods. SK8 supported multiple simultaneous inheritance architectures. I don't mean multiple inheritance, though it did support that. I mean that SK8 supported more than one inheritance architecture at the same time in the same set of objects. It offered support for user-defined inheritance architectures, and support for defining inheritance relationships along multiple different inheritance graphs in the same object at the same time. In other words, a single given object could inherit structure from one set of parents according to one set of inheritance rules, and behavior from another set of parents according to another set of rules, and truth-maintenance constraints from another set of parents according to another set of rules, and containment-related properties according to another set of parents according to another set of rules, and so forth. It provided APIs for constructing and supporting additional user-defined inheritance architectures. SK8 supported both class-based and prototype-based inheritance at the same time, with more than one inheritance model active at the same time for different purposes. SK8 was originally designed as a knowledge-representation system--a frame language in the sense of https://en.wikipedia.org/wiki/Frame_(artificial_intelligence) https://en.wikipedia.org/wiki/Frame_(artificial_intelligence.... Other frame languages and systems (e.g. KEE and KL-ONE and CycL) share similar features. Indeed, if I remember right, SK8's creator, Ruben Kleiman, worked on Cyc before he created MacFrames, which later became SK8. I guess, strictly speaking, these object-system features were features of the frame language, MacFrames, and SK8 was the authoring application that was built on MacFrames.
- agumonkey 4y agoEvery time I watch a dabeaz talk about metaprogramming python I think about CLOS. Expert may pitch in but it feels like a regression (even though I understand the social dynamics at play)
- tonyg 4y ago> Python classes may be clunky but it is said that Python class system is more flexible than other languages' OOP, even Lisp CLOS. 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 <module> 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.
- p_l 4y agoHaving recently done few projects in Python after long time of not touching the language, I found out I had to do the two-argument super all the fucking time. And I wasn't even trying metaprogramming. Just expectations for class system as if it was Java or .NET, I wouldn't dream of dealing with it like I do with CLOS.
- bsdz 4y ago
- jasfi 4y agoI prefer Nim's approach where you can have objects, but they're just variables (properties). The procedures aren't tied to objects, but you can pass/return objects. To me this is more flexible. I can write OO code as well, this is just personal preference.
- pjmlp 4y agoPython OOP more flexible than CLOS? Not really.
- bjourne 4y agoNo, it is absolutely the opposite. Lisp's object system(s) are much more powerful and much more expressive than what Python got. However, that expressiveness comes with a cost; everyone and their grandmother builds their own object system because they are unhappy about some detail in the default object system. Personally, I prefer the Python approach where the object system is coded into the language. Strictly less powerful, but more practical and to me it doesn't "feel" very limiting.
- jackdaniel 4y agoIn my experience people tend to use standard classes in Common Lisp, without further specialization (which is of course possible with MOP). They are part of the standard, so I'd say that they are "coded into the language" alright.
- bjourne 4y agoHow many Lisp implementations follow the HyperSpec? I'm no Lisp expert but from what I've seen the object system can vary quite a bit depending on the exact implementation.
- jackdaniel 4y agoSince we talk about CLOS (and HyperSpec, which is based on the last draft before the standard), I assume that we talk about Common Lisp. All conforming Common Lisp implementations have CLOS. To name a few which are used: free: ABCL, CLISP, CCL, CMUCL, ECL, SBCL; commercial: AllegroCL, LispWorks. There are more. I've made a diagram a few years back that shows the family with ancestors and sub-branches including these that are not complaint: https://ecl.common-lisp.dev/static/quarterly/img/vol4/all-hierarchy.png https://ecl.common-lisp.dev/static/quarterly/img/vol4/all-hi... Common Lisp strength is its standard; many programs developed on one implementation will work on another one. And standard includes CLOS. As far as I know, scheme had a bit of problems with that the standard was too small, and there were numerous implementations of the same functionality that were different; the issue with fragmentation was also problematic in pre-common lisp times - that was one of reasons why there was a grant to create the standard - to unify numerous implementations.
- jake_morrison 4y agoPython's OOP is not superior to CLOS. CLOS is very full-featured. Python's OO does support features like multiple inheritance, which makes it more sophisticated than e.g. Java. 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. https://lispcookbook.github.io/cl-cookbook/clos.html https://lispcookbook.github.io/cl-cookbook/clos.html
- crabbygrabby 4y agoYea I think the starting premiss here is flawed from the get go. MD is so powerful, kind of dangerous, but very powerful.
- gmadsen 4y agois that any different than function overloading?
- bananaoomarang 4y agoYes I believe because it happens at runtime. This is quite a good series of articles on multi dispatch that gets to CLOS: https://eli.thegreenplace.net/2016/a-polyglots-guide-to-multiple-dispatch/ https://eli.thegreenplace.net/2016/a-polyglots-guide-to-mult...
- gpderetta 4y agoMD is basically late binding for overloading. For example in C# can do MD with overloaded functions when the parameters are additionally marked as dynamic.
- dhosek 4y ago>Python's OO does support features like multiple inheritance, which makes it more sophisticated than e.g. Java. I’m not sure that “more sophisticated” is the right term for multiple inheritance. Multiple inheritance create all manner of challenges in understanding code. The simplest form of multiple inheritance as it’s present in languages like Perl or C++ can create hard to debug problems (I’m not entirely certain how it works in Python). That said, Java has (and has had) a restricted form of multiple inheritance for years through default methods in interfaces which allows the most useful aspect of MI—mixed in methods.
- declnz 4y ago> It is said that <x> I for one have never heard that said about Python; if this was on Wikipedia I'd definitely be mumbling Weasel Word[1] now... [1] https://en.wikipedia.org/wiki/Weasel_word https://en.wikipedia.org/wiki/Weasel_word
- nahuel0x 4y agoYou need to compare Self against CLOS or Smalltalk. Python object system is a poor thought hybrid between prototypes and classes.
- gregors 4y agoThis is a very leading question. Are you trying to invoke Cunningham's Law on purpose? Or just in a very pro-lisp environment that HN is known for? A better question might be to "compare & contrast Python OOP vs Lisp CLOS" I certainly don't think Python's OOP is superior.
- akjj 4y agoI think others have addressed the differences between Python's class system and CLOS, both of which are very flexible, as I understand. 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.