Crimes with Python's pattern matching
hillelwayne.com
hillelwayne.com
class OneWayMeta(type):
seen_classes = set()
@classmethod
def __instancecheck__(cls, instance):
C = instance.__class__
print(f"trying {C}")
if C in cls.seen_classes:
return False
cls.seen_classes |= {C}
return True
class OneWay(metaclass=OneWayMeta):
pass
def f(x):
match x:
case OneWay():
print(f"{x} is a new class")
case _:
print(f"we've seen {x}'s class before")
if __name__ == "__main__":
f("abc")
f([1, 2, 3])
f("efg")
When running: trying <class 'str'>
abc is a new class
trying <class 'list'>
[1, 2, 3] is a new class
trying <class 'str'>
we've seen efg's class before
Am I missing a particular point the article is making, or did the author overlook this?[0]: https://github.com/python/cpython/blob/main/Lib/_py_abc.py
The point of the article is that to override what "isinstance(obj, ClassA)" means one doesn't need to touch ClassA, or any descendants of it, at all.
[1] It's almost ridiculous to call these features as they're just a consequence of the underlying language model. E.g. if you have expression blocks you have lambdas with multiple expressions with no extra work.
It can take a bit to fully understand/appreciate Julia's multiple dispatch, but once you do you pretty much understand the entirety of the machinery.
It's (a) extremely easy to learn (takes about 2hrs for a Java/C# dev to be productive) and (b) has a very deep ecosystem and wrappers for pretty much any native library you want. Then (c) it works great under Windows/OSX/Linux as long as you're on an x86/x64 platform. The clincher is (d) it's the de-facto beginners language so all the newbies can at least read it and hack away.
The competition:
* PHP is similarly easy but very limited.
* Ruby is in my experience a slow and buggy mess with a community who are welcoming but suffer from a reality-distortion field (might be different now, my experiences were 15+ years ago).
* Java has accidental complexity getting started.
* C# is competitive but for the low-skilled / newbies too hard and still has an irritating NIH syndrome (e.g. pushing people to MS's half-baked crypto APIs instead of first-class ports/wrappers of libsodium / BouncyCastle).
* Javascript/Typescript are probably the closest, they have better package management for the "hack it together" use-cases but the language itself poorly designed what with all of the unintuitive "surprises".
My kids are just about old enough to learn coding and I'm going to start them with Python before moving on to C, ASM then if they want to develop anything serious; C# / Java / Rust / TypeScript.
- The syntax required to build libraries feels like messing with the internals of the language. Defining various methods with reserved names that have 4 underscores each doesn't really feel like something you are supposed to do. The code becomes harder to read and messy IMO.
- Runtime type checking is great for iterating quickly, but bad for stable software.
- Encapsulation is only enforced through external tools, so if you aren't using those religiously you end up with problems with tightly coupled modules.
- Dependency management is not a good experience. Understanding the different rules about where python pulls modules from is hard. Venv makes things a bit better, but even then it's still a bit opaque. It means that I often spend more time on getting external dependencies aligned properly than writing any python when working on a python codebase locally.
If I'm working with less experienced developers or people for whom software engineering is a side issue (researchers/academics, security experts, data-scientists) then it's Python all the way.
That's not to say these are impossible in Julia, but there was enough friction to make me not really wanna use it, when python can do all that and more.
That last one is the surprising one: we're about to see a generation of programmers who learned Lua via Roblox when they were 8-13 years old. Roblox is singlehandedly in the process of making Lua the #1 beginners language, and if not the most popular language by number of developers, then at least the most undercounted.
I use it in embedded systems (think 128MiB of memory -- not tiny, but not enormous either) and it's fantastic. I can make changes to logic on the device without cross compiling things and I can make changes quickly to test things out.
I'm in my 40s, and definitely not part of the Roblox generation. I just really like the simplicity of the language and how it's small enough to pick up in an afternoon. More complicated topics like coroutines and upvalues might take a little longer to fully grasp, along with ffi in LuaJIT if you're going that route.
Maybe there are ways to shrink libtcl or cut pieces out, but that's quite a difference.
I've found that for most of my tasks the order of things is not terribly important. I suppose that if I really needed this I could add my own ordered list data type to Lua.
For that I still quite often use Perl.
I'm interested to know to where you find the simplicity in Python. My guess:
- the ecosystem
- portions of Python that date back over a decade + perhaps some of the modern string handling and maybe data classes
My overall point is that the Python community relentlessly beats the drum on simplicity, but modern Python is not a simple language for any reasonable definition. I believe they have increased the complexity of the language while claiming that these complexity-increasing changes are in service of simplicity. I further believe that mountains of this complexity could be avoided with better language design and a better implementation.
I'd consider the complexity of Python comparable to that of C# and Swift - it's a similar "kitchen sink" language.
C++, of course, is in a league of its own.
Doing an ssl connection in java is a daunting task
That being said, I'm not convinced Java is actually simpler.
I think Go is possibly simpler than Python. The syntax is smaller, no method overriding, and does not really have internals to the same depth as a VM language.
> But surely Python clamps down on this chicanery, right?
>
> $ py10 abc.py
> 10 is not iterable
> string is iterable
> [1, 2, 3] is iterable
>
> Oh.
>
> Oh my.
I'm sure I'm being dense and missing the obvious but ... what is the author responding to here? What's wrong or bad?I am not sure if it wouldn't have been better to make the conversion explicit here.
Python isn't even unique in this regard! You can iterate over a string whether you're working in JavaScript, C++, or Go. (And that's not even getting into cases like Haskell where String is merely syntactic sugar for [Char].)
Which he explained in another article that it allows the author of the abstract class to hijack calls to isinstance for any instances created from subclasses.
Control over destructuring isn't entirely new territory for PLs, Scala has Extractor Objects[0], as an example.
I think that it's a bit easy to say "it should just match the type!" when the reality is that even basic classes like list get overwritten in Python. Ultimately many language features have configurable features through dunder methods, and the fact that those get used by other language features is a feature, not a bug IMO.
As usual, don't use libraries that do weird stuff... and every once in a while you'll have the nice DSL that does something useful in this space and it will work well.
The thought experiment about a more restrictive version of this: how does Python tell that an object is a list? If it's through isinstance, then you're hooking into a bunch of tooling that have hooks that can be overwritten. If it's _not_ through isinstance, suddenly you have multiple ways to test if something is a list (which is a problem).
[0]: https://docs.scala-lang.org/tour/extractor-objects.html
Abstract Base Classes were an attempt to formalize python's duck typing. Matching things that don't inherit from them is their whole purpose.
> Abstract base classes complement duck-typing by providing a way to define interfaces when other techniques like hasattr() would be clumsy or subtly wrong (for example with magic methods). ABCs introduce virtual subclasses, which are classes that don’t inherit from a class but are still recognized by isinstance() and issubclass().
You simply don't "subclass ABCs" ever (except when defining an ABC); if you do it's no longer a virtual subclass and you're no longer implementing the ABC. As a concrete example, when did you last "subclass" collections.abc.Iterable? You did not, you implemented __iter__.
All in all, I really don't get the dramatic tone in this article. It turns out that in python (as in most languages that give you access to the internals) if you mess with the internals the results are well messy. But literally nothing in this article suprised me at all.
Perhaps it would have been a bit clearer, and less easy to dismiss as a fuss over nothing, if the author had left the 'not' out of the definition of NotIterable.__subclasshook__(), or defined an IsIterable class with the 'not' in place?
> ABCs are not intrinsically incompatible with Interfaces, but there is considerable overlap. For now, I’ll leave it to proponents of Interfaces to explain why Interfaces are better.
Let me try: interfaces are better because the protocol of an object isn't tied to its implementation, but in a properly encapsulated world an interface represents the information available about a class as a type [2]. A subclass may just be reusing an implementation without adhering to the same protocol, or two interchangeable classes might have no inheritance relationship.
Python is in a lot of ways a nice language, and I've certainly enjoyed programming in it, but many points of its design seem intentionally unobservant of prior work and research in programming languages, though perhaps it's equally an indictment of that research that the most popular languages ignore it so much. Typescript handles this much better, though neither it or Java eschew using classes as types entirely.
[0] https://dl.acm.org/doi/10.1145/96709.96721
Depends on which OOP language we are talking about, Smalltalk definitly doesn't have interfaces unless we are talking about later dialects like Pharo, which introduced traits into the language.
The paper you linked to, makes its point exactly by moving beyond Simula and Smalltalk into their own view of OOP.
So like anything else on the OOP ecosystem, it is only yet another view about what OOP should be like.
Generally, in Python one always has to understand the implementation and mentally execute the code, because everything is informally specified and nothing is declarative.
Same thing can be observed for his blog opinions on parsing. Sadly, this gives rise to a whole generation of programmers who believe (on account of perceiving him as an authority) that Peg are actually good.
There is a strong aversion in the Python space for unambiguous formalisms. A parser that resolves ambiguities by earliest match first seems to satisfy the dynamic mindset.
That might be true if there was one single definition of OOP that everybody agreed on. Like with most other religions, this isn't the case.
[0] -- https://www.reddit.com/r/Python/comments/w4xoxb/anybody_else...
That is actually pathetic and I'm not blaming the package authors here - Python needs to stop making big breaking changes that rototill the codebase continuously.
Breaking changes are pretty serious business in the Java world and there is an incredible amount of thought and research put into even something like modules let alone the JDK17 changes where reflection is being fundamentally changed. Python seems to have an absolutely carefree attitude to language breakage, and I guess why wouldn't they? It's always worked for them.
I don't think that's illustrating breakage, just the lack of an explicit declaration that the package supports a newer version of python (which may be newer than the latest release of a given package).
I agree that post Python 3 it is less common for a language update to break third-party libraries, but I also don’t think sweeping the 2 to 3 years under the rug is fair.
Honestly, I think people are too hard on languages (and especially Python) for having new features that challenge the status quo. And then there's also too much drama when it turns out that a scripting language is, in fact, a scripting language! So you can do weird things with stateful ABCs and such. I mean yeah, it's strange. But it probably also has some perfect use case in a very specific circumstance. At the end of the day, if you understand how a feature really works, you can do creative things with it. I'm glad we have it!
They are great examples. Great examples of what not to do.
The only use case I can think of is meta programming which most people don't need.
I've been thinking about programming language development, and some weird things from the typing front.
This could be easily used to achieve unions.
But another thing that I was thinking about was the return from unsafe land.
You have some class or object that you want to do some quasi-illegal fucky bullshit to - send it off and do what you will.
But what comes back might not have the guarantee that it's still the 'shape' of the thing that you sent away.
This could be used to validate that what comes back is sane.
Still like pattern matching in python though. Seems super useful.
How is that a crime?
Next, you will be surprised that you can use first class functions and late variable binding?
edit: I guess I found my answer [1]. That's kind of expected, but it's still ugly.
https://stackoverflow.com/questions/71811960/how-to-use-subc...
Personally, I think this is a very good situation: extremely dynamic languages are a worthwhile tool, and the only issue with the pattern matching "exploits" in the article is that boolean operators and non-cached evaluation for subclass checks are not built-in.
What's the hijack there?
I'm really not sure what's the author's point is. He writes the code and get's expected results back.
Cython at least aims to be a superset of Python, so it will support it sooner or later. However I don't doubt that using it will make your cython code stop being "fast".
Personally, I'd prefer that isinstance() and issubclass() have predictable behavior.
In general metaprogramming is the kind of thing you probably don't and shouldn't reach for first, in fact it's usually more for libraries and tools vs. your production business logic. It can get difficult to reason about and pass the maintenance of code that heavily uses metaprogramming to other people unfamiliar with it.
the other day, this allowed me to refactor a 300L in 50 which are actually readable
I think an example of `Iterable` (sort of like the one in the article) is a very ham-fisted way of getting this sort of check into Python code.
In the end, doesn't the difference just boil down to
class Iterable(ABC):
@classmethod
def __subclasshook__(cls, C):
return hasattr(C, "__iter__")
vs. def is_iterable(t: Type):
return hasattr(t, "__iter__")
Where the first makes it harder to use `Iterable` incorrectly (i.e. supplying a non-type as parameter).I can imagine that a Java or C# programmer would call the first version more "coherent" because it gives the interface `Iterable` a name explicitly.
Sort of like there's not reaaaaally a reason to use Extension Methods in C# (of course there are, but in a lot of simple scenarios there aren't) as opposed to static methods taking a Type as single parameter.
“but it will be so cool!” worked for me :)
def foo(bar: Iterable):
pass case Not(DistanceMetric)():
a syntax error?Consider things like “case DistanceMetric(distance=d):” earlier in the article: this checks “is the value an instance of DistanceMetric, and if so, take its distance attribute and bind it to the name d”.
So in this case, what would it mean? If the value is an instance of Not, take and bind it to the DistanceMetric name (as is typical for a single positional subpattern), and… uh oh, more parentheses, what to do? There’s no obvious sensible meaning for it, so it’s a syntax error.
I guess the word "hijack" is used loosely for rhetorical effect here because this seems to be working as intended, and it's not even remotely the most dangerous footgun in Python. The problem (if any) is with `isinstance`, and not the pattern matching. `isinstance` should probably explicitly work with ABCs (via flag or something) because I do agree it's a bit weird that it takes the ABC's `__subclasshook__` as gospel by default.
Yes, you are misusing the API. No, there ARE valid use-cases for it: for example a lot of testing/mocking facilities in Python are so easy to implement because of these features. No, the fact that you can't do it in statically typed languages does not imply Python must go the same way.
you are expect to not send a obj but something to have the properties matched.
There's more than one way to do it *vs* there should be one, and preferably only one, obvious way to do it
Loops, list/dict comprehensions, if statements, and ternary expressions would all like a word
The "never have to know or touch" argument applies only to the lone hacker working on a completely new project with no inherited legacy code.
I’m baffled as to how it retains its reputation of being a simple language.
In [5]: def cap(x):
...: xs = []
...: for i in range(3):
...: x+=1
...: xs.append(x)
...: return xs
...:
...:
In [6]: cap(numpy.array(0))
Out[6]: [array(3), array(3), array(3)]
In [7]: cap(0)
Out[7]: [1, 2, 3] >>> a = b = (1, 2) >>> a = b = (1, 2)
>>> a += (3,) >>> a = a + (3,)
>>> print(a, b) >>> print(a, b)
(1, 2, 3) (1, 2) (1, 2, 3) (1, 2)
Sometimes it does something different: >>> a = b = [1, 2] >>> a = b = [1, 2]
>>> a += [3] >>> a = a + [3]
>>> print(a, b) >>> print(a, b)
[1, 2, 3] [1, 2, 3] [1, 2, 3] [1, 2]
IMO if that behaviour had been "carefully thought out" by the language designers, it should have been obvious that it's a bad idea.Failing that, the implementation of that behaviour is convoluted - so the designers should have paid attention to the Zen of Python: "If the implementation is hard to explain, it's a bad idea".
Failing that, if the behaviour had been "explored to great lengths", the designers would have understood how it interacts with other language features - in particular, nested mutable and immutable objects.
Python's designers failed to do any of these things, so we've ended up with an operator with unpredictable behaviour and a long FAQ entry [0] about how its possible for an operator to both succeed and fail at the same time:
>>> a = ([1, 2], 4)
>>> a[0] += [3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> a
([1, 2, 3], 4)
[0] https://docs.python.org/3/faq/programming.html#why-does-a-tu...Either you get Scheme, or languages with features.
Even C isnt' as "simple" as people take it to be.