| tl;dr: I don't like metaprogramming. ---- Both of those things are very good, even great, but they don't speak to the issue: It's [bad] magic. In this case, it does at module-load-time what should have been done previously at build-time. (And yes, I know Python normally doesn't have build-time the way e.g. C does.) I'm working on a large production codebase right now where several of the cowboy-coders here have added all sorts of crazy metaprogramming and it's a PITA. Let me tell you a story. We had a class decorator that introspected the attributes of the class and added "constants" to the enclosing module providing named string values for each of the class's attributes. It lets us access dict versions of these objects (they're data models) like foo_as_dict[foo_module.SOME_ATTR_NAME_BUT_IN_CAPS] ... Good luck finding foo_module.SOME_ATTR_NAME_BUT_IN_CAPS in foo_module.py though, because it's not there. (and notice that if the model field name changes ALL of your uses of the "constant" have to be refactored to the new name too, so it's not really as helpful as it might seem in the first place.) And now every new developer has to ask, "Where are these coming from?" and we get to point out the magic extra-clever decorator. (Try to imagine the horrible wonderful "voodoo" that decorator encompasses... Imagine the innocent newbie encountering it.) We have a "service builder" thing that takes a bunch of arguments and build a web service object in memory at module-load time. No source for any of that machinery. A bug in the service gives you a traceback running through a bunch of weird generic meta-code. Did I mention the guy who originally wrote it bailed to a new job a month after he finished it? No one else knows how it works. We can read the code and reverse engineer it, of curse, but that's kind of B.S., no? A junior dev could debug the real service code if it existed, but the meta-code that generates the services is another challenge (that they shouldn't have and the company shouldn't have to pay them to overcome.) We had unittest that read a JSON file and built a test suite in memory to run a bunch of other tests. They finally let me re-write that to a build-time script that scans the exact same JSON files and emit plain-vanilla unittest modules, that then can be run as normal, and the dev/user can look at the actual code of the test, rather than the meta-code of the test-builder. The same problem applies to this attr package. If you're trying to trace into or debug code that uses attr you've got to know attr at least enough to be able to follow what it's doing. It's an in-joke. ;-) (P.S. I'm a big fan dude. Nice to interact with you. All respect.) |
I understand that spooky action-at-a-distance magic can really ruin a codebase's maintainability. But what you've developed here is not a judicious appreciation of its danger and its power, but a blanket aversion to both its risks and its benefits.
An apt metaphor would be, let's say: toast. If you try to make some toast, but accidentally burn your house down, it's understandable that you might want to have your bread un-toasted for a while. But it doesn't make sense to switch your diet to be entirely raw as a result, especially if you eat a lot of chicken.
In python, metaprogramming is like fire. People who haven't seen it before are fascinated before it, and try to touch it. This always goes badly. But that doesn't mean it's useless or we shouldn't have it. There are many things it's good for, if it's carefully and thoughtfully applied.
attrs goes out of its way to avoid any behind-the-scenes effects. It even generates Python code, rather than using setattr(), so that if any error happens in the constructor, you can step through it normally in the debugger, and see it in the traceback. Its semantics are clear and direct. It doesn't have these problems.