Better Python APIs
ozkatz.github.com
ozkatz.github.com
The first example in the post (using __repr__) is probably the least controversial. There are places in the official docs that explicitly recommend defining __repr__ for every object you create. This is mostly something that I do do (though there are exceptions of course), but one thing you do want to be careful with is to make sure that what appears in your repr is relevant, terse, and helpful. That means that I will and do sacrifice a repr that is eval'able (which I find to be useless) for a repr that is short and sweet. So for the example in the article, it may or may not make sense to put `self.objects` in there. For a "Container" class, it probably does, but don't extend that to "let's put all our data in the repr for everything".
__iter__ is nice, and is one of the most oddly underused methods, but rather than using it there, I think the better thing is to not have the object encapsulate a search, but to have the object encapsulate a YoutubeAPIClient or whatever, and have `search` be a method that returns an iterable. This is also kinda touchy-feely though since I guess there might be examples of __iter__ where I'd feel more comfortable with something like this (a results object).
The next one with __sub__ I don't like too much, because I don't like encouraging slow operations with syntax sugar (besides the typechecking).
Anyways, I guess I'm nitpicking on examples, mostly. Double underscore methods are generally underused I've found, and if you have doubts on whether you're overreaching, you can always add a method with the same behavior you're using them for in case anyone doesn't like how it reads.
def youtube_search(term):
url = 'https://gdata.youtube.com/feeds/api/videos'
params = {'q': term, 'alt': 'json', 'orderby': 'relevance', 'v': '2'}
r = requests.get(url, params=params)
r.raise_for_status() # 4xx and 5xx replies raise exception
for video in r.json.get('feed').get('entry'):
yield {
'title': video.get('title').get('$t'),
'url': video.get('link')[0].get('href'),
}
The ExpressiveList's __sub__() method, as written in the article, requires O(n*k) time to remove k things from a list of length n. By using a set of things to remove, we can get that down to O(n+k): class ExpressiveList(list):
def __sub__(self, other):
if isinstance(other, list):
other_set = set(other)
return ExpressiveList(x for x in self if x not in other_set)
else:
return ExpressiveList(x for x in self if x != other) >>> ExpressiveList(item, [2], [3]) - item
will either be [[2], [3]] if item isn't a list, or something different if item is a list. This goes against the "Explicit is better than implicit" philosophy of python, and can cause all sorts of headaches later.However, I discourage promoting inheritance (except in the guise of mixin, and even then...) as a way to factorize common functionalities. Composition is usually much better suited.
http://www.youtube.com/watch?v=o9pEzgHorH0
That said, some nice factoids in the post about making your objects readable in the repl etc.
For simple things like list addition it might be intuitive what the result will be, but if I had a a Vector class and then you were reading some code and saw two vectors being multiplied like this
a * b
well is that a dot product or a cross product? Or maybe a is a scalar and it's scalar multiplication? If it's one of those what do the other operations get for symbols? isn't it easier to do a.cross(b)?If you keep it simple a little syntactic sugar is probably find. Nobody wants to be writing a.add(b.subtract(c.mult(d))) for a + b - c *d .
Anyone have other thoughts on this?
However, if you're a Haskeller (or a mathematician), this makes perfect sense. Any type with an associative binary + and an identity element is a monoid; any additive monad (MonadPlus in Haskell) is a monoid; List is a monad; therefore it makes perfect sense that operations like "sum" that are defined on monoids should work on lists.
This is just the age old abstraction vs. traceability debate that's been going on in programming languages for 40 years. Die-hard C programmers look at Java code that says a.dot(b) and say "How can I possibly know what this does? It depends upon the run-time type of a. Much safer just to do vector_add(a, b)." Then Java programmers look at Python code that says a.dot(b) and say "How can I possibly know what this does? I don't even know what interface a and b have to conform to, they could be anything." Then Python programmers look at Haskell code that says a * b and say "How can I possibly know what this does? You say it depends upon the type of a and b, and it could be either a dot or a cross product? And what's this -XOverlappingInstances flag I need to know about?"
Somewhere, there's a happy middle ground between impenetrable code vs. rewriting the same thing over and over again for different types. Personally, I think you won't find it with a blanket rule like "never use operator overloading", and it depends a lot on which abstractions come up in your problem domain.
The C++ approach has the problems you outlined. A single operator name (like +) gets overloaded with far too many different--and often completely unrelated meanings. Particularly, this violates reasonable expectations about how certain operators behave; for example, + should be commutative. Python's + for lists, very clearly, isn't.
The Java approach of not allowing operator overloading or custom operators is also patently untenable. Just look at the BigInteger class! There are plenty of other cases when using an infix symbol makes for much clearer code.
With a language like Haskell, you can just come up with new operators as you need them. In fact, they act exactly like normal function names: the only difference is in how they're parsed. This also means they're overloaded like normal functions. So while there is a + operator for a bunch of different types, it always represents some notion of addition. This feature can be abused--it's relatively easy to write confusing operator names--but I've found it to be a net benefit in practice. If used correctly, it makes the code much clearer; you just have to be a little careful.
For example, in Haskell you use ++ to concatenate lists. So you can't mistake list concatenation for addition, which is important because they behave in very different ways.
As an aside, if you're willing to use Unicode symbols--and I think you should be!--then you can define cross as × and write a.cross(b) as a × b. In a small expression this does not seem like much of an advantage, but if you are doing more complicated math it makes the code easier to follow. It's also roughly as easy to type: with the right input mode, it's just a \times b.
I found that in practice it is often abused and leads to code full of .+:, >>> and other operators which certainly make a lot of sense to the author of the library, but don't do much for intuitive understanding of a piece of code without reading the documentation for each imported library.
I think it's still better than what you get with C++ and Python. I would much rather have a relatively inscrutable <+> operator that does something vaguely like addition than having + do multiple completely different things on different types.
Also, for many of these operators, the clarity of having an infix version trumps the fact that you'd have to look it up. Take >>> as an example. Even if it was called something like next, you'd still have to read the Arrow documentation to understand exactly what it did (it's fairly abstract). And compare how the code would look:
actionA >>> some complicated action >>> actionB
next actionA (next (some complicated action) actionB)
Ultimately, it is impossible to design a language that does not allow any bad code. I think having custom operators is better than the main alternatives (C++ and Java styles).I can live with + as a concatenation operator for strings, but in general, if you're starting to implement custom operators for +, you should only do this for things which are numbers, and which can also support -, / and *.
Wrt to Arrow, you could still have your custom operator but with a human-readable name:
actionA >next> some complicated action >next> actionB
or alternatively: actionA `next` some complicated action `next` actionB
Sure, it's a bit more verbose, but it makes for more readable code (IMO). I think it's not so much the notion of custom operators I object to than the "let's get wild with non-AZERTY characters" philosophy.Of course, it doesn't mean that human-readable names are always better... I'm looking at you, "return".
(a quick google implies that Haskell supports this (http://www.haskell.org/haskellwiki/Unicode-symbols). Good!)
1: http://stackoverflow.com/questions/2793792/is-it-a-good-idea...
In this situation, everyone is going to have a computer set up to use the character set, so the ����� + ��� situation shouldn't arise.
In terms of confusing things at linking / runtime stage, I didn't think of that - maybe you're right! Go gets away with it by being completely compiled down to binary (and requiring the source of all the libraries locally, IIRC.)
For example, my biggest gripe with ruby's bcrypt library is how == is non commutative. They do provide a named method alternative, but I was very confused as an infrequent ruby user.