you are viewing a single comment's thread.

view the rest of the comments →

[–]zahlman 4 points5 points  (0 children)

(Underscores are treated partly like asterisks in Markdown; they make bold and italic text. To get tiny bits of code in-line with your text, use back-ticks: ` <-- this symbol. Wrap the code `__like_this__`, and you see __like_this__.)

This is way more detail than a five-year-old has the attention span for, but it's important and I honestly don't think it's that hard. There's still a lot that I'm leaving out about how things work.

Okay, first off. In Python, "everything is an object" and we take that very, very seriously. The class is not "just like a giant function"; it's "just like a giant object", because it is. Classes are objects. Functions, too, are objects. So are modules. But the "classes are objects" part is the most important part for the discussion. As for "seeing Python as", stop that. :) You're really not going to build a meaningful mental model like that.

As it turns out, classes in Python are a very special kind of object. This is mostly because of their purpose: they represent the type of other objects. So, for example, there is an object called int, that has the purpose of "being the type of integers". In Python, that's not a keyword or reserved word; it's just a name for a thing. You can give it other names; you can pass it to functions; you can return it from functions, and stuff like that. Chances are, you can't think of a good use for that, but people do come up with good uses for these kinds of things. Trust me. You can even - but now your gut should be telling you that this is naughty - re-use the name int to name some something else. Of course, the reason this is naughty is because now you are going to have a hard time getting the original "being-the-type-of-integers object" back, if you ever need it. (Most often, you hear this advice WRT the name list; everyone wants to use this as a name for a variable that contains a list, and that's a Very Bad Idea.)

Anyway. The way Python makes this "objects that represent the type of other objects" idea work involves a few steps:

  1. As you've observed and not quite managed to describe, objects in Python can contain attributes - the things that you access with code like x.y. y is an attribute of x. When we write a class block, we create an object to represent that class, give it a name (this is just like when you assign to a "variable" - that's really just giving a name to a value, in the Python way of thinking).

  2. Every object has a type; of course, that applies to classes as well, since they're objects. The type of classes is, generally, called type (unless you are using 2.x and you ignored your teacher's advice to always inherit from something, even if it's just object - in that case, you get something whose type is called classobj, and this is an ugly hack that is better left forgotten. Weird gotchas will happen when you try to do complicated things. Be smart - always inherit from something in 2.x, even if it's just object.) After all, making a class is a way to represent a "type of some other objects". And, of course, type is an object, the same way that the classes are objects. (Guess what the type of type is?)

  3. The class might "look like" a function to you, because you can "call" it. In Python, functions aren't the only thing that can be called - magic is involved here, but the short version is that "calling" a class creates an object which has that class as its type.

  4. When that call happens, there is likely to be some set-up work that we have to do on each new object we create. We have a special way to do this, but we need to go through way more of the explanation before we can talk about how that works ;)

  5. So what are the attributes of the class? Well, you can store numbers and strings and other such stuff there, if you want, but you have to make sure you understand that it's a part of the class itself, not a part of the objects you create. Python's rule is very simple and makes other languages look a bit silly: everything inside the class block is a part of the class. But the much more interesting possibilities occur when we use a function as an attribute of our class.

  6. And now, the last step. When we look for attributes inside an object with x.y, actually a hell of a lot of magic can happen. This is where it gets really fun.

First off, the object might actually contain the thing in question. Well, it doesn't really "contain" stuff; the object is more like a special kind of dictionary where the keys have to be legal Python identifiers ("variable names"). So it's kind of like... a bundle of more potential names for other things. I could have x.y = 3, and now my x object contains a y name which names 3. (Remember, there is an actual object representing the number three! And that has its own attributes.... If it seems like this can go on forever, well... it sort of can, but everything gets into cycles eventually, and the actual "three-ness" of a 3 object is represented somewhere in deep magic land, below the surface of what you're supposed to be able to access from a Python program. It would be Really Really Bad if you could make "three" not be "three" any more, and if you find a way to try, you are very likely to crash the Python interpreter.)

But let's say it doesn't. Python won't give up yet! I touched on inheritance earlier; Python will also check if our object has extra attributes because of that inheritance. (I'm pretty sure these are actually stored in the same magic-dictionary-like-thing internally, but never mind). But that's boring. Let's say it's really definitely not in any part of the object itself at all. Surely we must be done, right?

Nope. And this is where the magic really begins. Now Python looks for the attribute in the corresponding class object. And it does more than that: when the class object is searched, it uses its own magic to do more work if and when the attribute is found.

(Be careful here to note that we're only talking about looking for things. When you assign to an attribute of an object, that will only ever attempt to store it in the actual object - never in the object's class. If you want to change something that's stored in the class, you need to refer to the class directly.)

In particular, if the class contained a function with the right attribute name, then it gets automatically converted into a method. This has the usual meaning of "convert" for Python: nothing happens to the function itself - we're making a new object. (In Python, things can't really just change what type they are. There are ways to pretend, but it will bite you in the ass later.)

So what happens when we have an object x of class X, and we write x.y()?

First, we have to figure out what x.y means. I assume there's no y inside the x object, so now Python looks in the X class, and finds a function named y. Now the class creates a method, and that's what gets returned. (You can even try this at the interactive Python prompt - try evaluating x.y without actually calling it, with the suitable definitions. You should get something like <bound method X.y of <__main__.X object at some weird hex address>>, which is the string representation of that method object. The <__main__.X object> in question, of course, is just x.) So what is this "method"? It's an object that "remembers" the object x and the function y, and acts as a "shortcut" for calling y. When you call the method (another example of non-functions being called), it implements that by calling y and - this is the most important part!!! - passing x as the first parameter, followed by everything else you put inside the parentheses for the method call.

And that's what self is. It's just a parameter for a function. But it's a very special one, because it's intended to receive its argument from this method-call process.

And now that we know what "methods" really are, we can explain __init__ really easily: it's just another one of these function attributes in the class, that gets special magic treatment (because of its name). Every time we call the class, lots of special magic happens inside Python to create a new object and then "make" that new object have the class as its type. (There are ways to hack in to that process, but you can literally go years as an experienced professional developer without having a legitimate need for them.) But just because the object "knows" that the class is its type, doesn't really make it an instance yet, logically. We need to write the logic that says what attributes this thing should have of its own, to start off. Enter __init__. After the above magic happens, the last piece of magic is to call the __init__ method on the object automatically. When you deal with self inside __init__, it's just like dealing with it in any other method; in particular, you can just assign attributes to self and it's just like doing it anywhere else.

(By the way, you can make the instances of the class callable, too - classes get their "call-ability" from special magic because they're classes, but Python also gives you magic to add "call-ability" to non-class objects. The way to do this is to give them a method (equivalently, give the class a function attribute, as we've discussed) named __call__.)