… because I have to replace this:
def category(request, slug, language):
translation.activate(language or 'ru')
category = get_object_or_404(models.Category, slug=slug)
return object_list(request,
template_name = 'marcus/category.html',
queryset = models.Article.public.language(language).filter(categories=category),
paginate_by = settings.MARCUS_PAGINATE_BY,
extra_context = {
'category': models.Translation(category, language),
'language': language,
},
)
with this:
class Category(generic.ListView):
template_name = 'marcus/category.html'
paginate_by = settings.MARCUS_PAGINATE_BY
def get_queryset(self):
self.category = get_object_or_404(models.Category, slug=self.args[0])
return models.Article.public.language(self.args[1]).filter(categories=self.category)
def get_context_data(self, **kwargs):
translation.activate(self.args[1] or 'ru')
context = super(Category, self).get_context_data(**kwargs)
context.update({
'category': models.Translation(self.category, self.args[1]),
'language': self.args[1],
})
return context
Now, these snippets might not look strikingly different from the first sight so let me break them down for you and explain my point:
-
When your code is a function it is this function that actually works and calls other functions. When your code lives in overridden methods, something else works elsewhere and calls your code. This is called "inversion of control" and it makes code harder to read because you no longer see why and in which order things happen. This is not necessarily a bad thing (the whole framework works by inverting control, after all) but in this particular case it did make me think harder. In the original code I call
translation.activate
before anything else to make sure that any code that might depend on current language will have it available. Here I don't really know in which orderget_queryset
andget_context_data
are called and the only reliable way to activate translation early is to override another method —dispatch
— that calls both of them. But it makes code even hairier. -
You're bound to have some boilerplate code when subclassing. Here I have two method signatures that have nothing to do with my specific domain, I just have to look them up in the docs and repeat to the letter. And I also have to call
super()
, not the prettiest thing in Python and it makes me to repeat the class name. -
Since my code is now in two places the only way to have a variable known to both of them is to put it in the class instance — the
self.category
in this case. It doesn't really belong there, it was just a local temporary variable but after it became a class attribute it looks just as important and global as any other "real" attribute. -
I lose descriptive names of arguments that I had in a function. Now they are available as opaque
self.args[..]
values. True, I could assign them to local variables but doing it every time is just silly… -
And of course, the code just got obviously longer.
By the way, this is not the worst refactoring that I had to deal with during my yesterday's crusade on deprecation warnings. But I decided to be merciful on you :-).
Philosophy
I often hear that class based generic views are better because they are more flexible and extensible. What people miss is that extensibility doesn't come at all from the fact that they are implemented as classes. Extensibility exists only in those places that framework explicitly defines as extensibility points. Everything that you can do by overriding a method or setting an attribute you can do with a function passing values and callables as arguments. These two paradigms are thus equivalent in terms of functionality.
Still, it probably makes perfectly good sense for Django itself to have generic views as classes because users demand insane level of extensibility for them. We'll never know if they would look prettier were they implemented as functions because nobody volunteered to do that. Probably readability of classes does indeed scale better along the axis of use-case complexity.
What I'm really trying to show is that in case of the majority of user code there's no point at all in having views defined as classes. Your code is never going to be as generic as Django's (unless you're building a framework) and functions just make better sense. I, for one, simply implemented a custom short version of object_list
and everything just worked.
Comments: 12
+1.
Ну, и,
"We'll never know if they would be prettier..."
"... functions just make more sense. I, for one, simply ... and everything just worked."
Oh, и в школе учили что начинать sentence с "Probably" или "And" неположено.
:D
Definitely agree. A long-time complaint of mine about trying to write domain specific languages is that now the developer has to learn a second language and unless it's a HUGE win to do so or the language is absolutely tiny, the extra abstraction is making things harder. Class-based views are a DSL, in effect: they are trying to remove the need to write a view by saying you "only" have to fit the bits in your code that are different to the framework pattern, so the language is a pattern language. But people's constant wish for Yet Another Lever and Shiny Button(tm) has led to something that isn't a huge difference over just writing a view in the first place. Developers already have a nicely configurable interface for writing views: it's called Python.
I totally agree. Class-based generic views is the ugliest thing in django.
Definitely agree... I'm relatively new to Django and generic views made no sense to me coming from other frameworks (Grails and Rails). I'm glad to see I'm not the only one...
+1! I wrote about this (but not as eloquently as you did) in my own blog a few months back. (http://seeknuance.com/2011/05/28/i-dont-like-django-class-based-generic-views/) I agree with the intent, but between the extra coding you need to make them work, and the documentation's paucity, they're not fun at all. I still don't use them.
From Reddit:
I'd have to disagree with this article. It misses a major point of class-based (generic) views. Your language and category filtering code must be duplicated across all your views. This argument may work for one view but as soon as you have a project or app with 10+ views you wouldn't want this boilerplate mess in every view.
This code needs to be separated (mixins are a good choice for this) so that it can be mixed and matched in multiple views at a whim.
https://gist.github.com/1415cf6300f07b9bc568 (untested but i'm using something very similar)
As you can see the final view boils down to 3 lines of code, the rest of your views will also end up a very low LOC. Your code may be shorter in one view but as your codebase scales to multiple views properly utilized class-based views will not only reduce your line count, it will reduce the chances of error.
Classes has one thing functions missing - inheritance. So you can reuse class Category() later (maybe with overiden methods/data).
Imho, as always.
I was expecting these :-)
andrew@amccloud.com:
Volodymyr Sergeyev:
I already mentioned in the post, in passing, that in terms of code reuse functional and class paradigms are equivalent. I'm not going to write some sort of a formal proof here but I'll try my best to explain it…
A function, at its essence, is the abstraction of logic that can be reused elsewhere — nothing else. A method of a class is simply a function that has an implicit first argument (not that implicit in Python). This makes class methods a specialized kind of functions meaning that functional abstraction is actually more flexible than class-based. An instance of a class is much the same as a function with variables bound in a closure.
Inheritance is a fancy mechanism of method lookup that makes it possible to override only some of your methods. It implements the idea that descendant objects share a common semantics that is expressed with common method names. In a functional world you just have differently named functions that you can call freely from each other. Again, inheritance establishes specialized rules of naming and calling functions that makes class hierarchies less flexible than functional call graphs.
Andrew's example shows this very well. Each part of the code that can be reused is represented by a mixin. And the final view just uses them all. All this can be easily expressed with functions:
Having all those words inside the view — "translate", "filter_category", "get_category" — is the equivalent of listing mixin names in bases. But without having to think about the MRO :-).
If necessary this functional code can be made more generic. For example,
translate
can be a decorator and would dig for language parameter by itself. I don't have it in my code becausetranslation.activate(language or 'ru')
is not really worth shortening to@translate
. Andobject_list
can be a function withPAGINATE_BY
as the default value for an argument. This is how I have it in my code right now because there are about five views using it. The outcome is the same as in a class code:PAGINATE_BY
is present in the code only once, I don't repeat it.My point is that class-based code adds boilerplate that can only be tolerated if you have many places to use it while functions are good for both ends of the simple—complex spectrum. The only real purpose of classes, as Malcolm wrote earlier, is to build DSLs. It's just so happened that this particular DSL of Django's generic views didn't work for my app (though it did work for one of my other apps).
Further (lazy) reading: SICP, Paul Graham on Object Orientation.
P.S. BTW, I blame universities for continuing to plant the misconception of classes being more powerful than functions. It's notoriously hard to overcome.
Иван Сагалаев:
Yes MRO can bite you in the ass sometimes. Yes you can chain multiple functions with a slew of arguments to override behavior. Yes "generic" class-based views add some sort of boilerplate. Could you provide an instance where the boilerplate gets in your way and causes you to write more code or gets in your way? I've never had this issue and i've done some pretty radical things with class-based views like rendering to an api that interacts with phones. https://github.com/amccloud/django-tropo-webapi/blob/master/example/opengate/views.py
Yeah, there's indeed some preference to OOP approach over the functional one in Python, not sure why. For instance in Scala functional approach is considered much more clean and robust way than writing imperative OO-code and those who prefer class abstractions over functional ones are usually considered as newbies in Scala community :) That's huge contrast comparing to Python which is really strange cuz Python features first-class functions and certain syntax sugar like decorators.
Looks like the only reason is Python's poor syntax for lambdas. People would probably use much more of FP in Python if they'd have some nicer way to define multiline anonymous functions inline. I mean the language is powerful enough but its syntax really encourages imperative style/OO-approach.
@coffeesnake
You seems to oppose FP to OOP, but I think this is wrong. FP is about being referentially transparent and OOP is about encapsulation. Please note I'm not saying FP langs don't have encapsulation, they are, but usually via other techniques, not OOP.
@Иван Сагалаев
You're definitely right, classes are essentially sets of related functions, nothing more. Anything you can do with classes you can also achieve by using functions in Python. But classes still exist in the language for the reason — they capture useful pattern named encapsulation.
Consider a function which is parametrized over some function which do filtering on database results:
Now something has changed and you want also to parametrize your function over some another function which do grouping of database results:
Now things get interesting — you need to change every func call to provide additional argument... and that's every time you add more parameters to your func. Another solution is to pass all functions as single argument — dictionary:
But that's exactly the same as define you func being method in class and have parametrized over other methods:
This also helps in usual case of func's params being closely related to each other — you define them together in single class definition and what's more important you can't mix params irrelevant to each other in client code.
I am learning generic views and it has been a painful experience so far. The official documentation about them is bad.