Join 3,417 readers in helping fund MetaFilter (Hide)


Python for Putzes, Part 2
April 6, 2012 7:16 AM   Subscribe

I'm looking for brilliant metaphors to explain the ins and outs of object-oriented programming. Know any?

So I've been stumbling through Learn Python the Hard Way, reinforced a bit by Snake Wrangling for Kids (which is great for me, even though I'm not a kid), StackOverflow questions, the embedded questions and answers at the Learn Python site, and various other tutorials and articles across the Web.

And I cannot -- cannot -- wrap my head around the whole "passing arguments between classes and functions" business. It's been weeks that I've been trying to just kind of wrap my head around code like this (Codepad link, all comments mine).

I just can’t seem to “get” how a variable can be passed from one class to another. Even when I do understand a particular piece of code in terms of how it works line by line, when I try to write my own, I can't grasp how the theory works in order to write new stuff, I can only ape existing models.

So I'm a bit past the super-beginner stuff (print, for-loops, etc.), and totally hitting the wall on the more OOP kinda stuff. What are some great tutorials, short courses, or illustrated examples of how classes, functions and variable-passing works? How would you explain this to a small child (which I am not) or an imbecile (which I am beginning to suspect I might be)?
posted by Shepherd to Computers & Internet (15 answers total) 33 users marked this as a favorite
 
You mean like this?

You're not an imbecile; these concepts are not intuitive. I just had to grapple with them for a long time before understanding them.
posted by massysett at 7:33 AM on April 6, 2012 [1 favorite]


And I cannot -- cannot -- wrap my head around the whole "passing arguments between classes and functions" business. It's been weeks that I've been trying to just kind of wrap my head around code like this (Codepad link, all comments mine).

It's an easy enough concept. Think of it as a transaction. In order to get something, I need to ask for it and maybe provide some materials.

Consider a grocery_checkout : I pass (things_I_want_to_buy, money) they return (new_stuff_I_just_bought)

But classes can have multiple functions.

Class My_dog has the following definitions : Sit, stay, fetch.

And I can have multiple instances of My_dog : My_dog.Finlay, My_dog.Monty but because they are both My_dog, they know the same tricks.
posted by Pogo_Fuzzybutt at 7:58 AM on April 6, 2012


First of all, a variable can not be passed "to a class", it's always passed to a function, that may belong to a class (in which case it's called a method - there are some exceptions but never mind that for now). Even if it looks like a var is passed to a class, it is actually passed to its __init__ function, and only when instance is created from a class, e.g.:

class A:
def __init__(self, x):
print x

a = A(5)

So the only part you need to understand is how vars are passed to functions. The way you might think about it is that it's similar to sending a work order to a plant or a factory, let's say you want a plant in china to make you 10 widgets with size 5 and colour red. You've already done business with them so they have template work order forms for you to fill out. It might look like:

widget xyz order form
amount: __
size: __
colour: ______

You fill it in, send it and bam! a few weeks later you get your widgets.

A function might look like:
def make_widgets_xyz(amount, size, colour):
[...]
return list_of_widgets

The function is equivalent to the order form. To use the function, you can make a call like:

widgets = make_widgets_xyz(10, 5, "red")

Calling a function is equivalent to filling out and sending the order form. Since function usually run pretty fast, you'll get your widgets a few milliseconds rather than weeks later.

You may already have your values stored in variables:

amount, size, colour = 10, 5, "red"
make_widgets_xyz(amount, size, colour)

This is equivalent to maybe having stamps with often-used values and stamping them into an order form instead of writing them in every time.

It's important to understand that the function does not care what the passed variable names are, it can't find them out and it should not care.. only their values are going to show up in the function:

make_widgets_xyz(amount, size, colour)
make_widgets_xyz(10, 5, "red")
a = 10; b = 5; c = "red"
make_widgets_xyz(a,b,c)

All of these calls are equivalent but the function only gets the values, it will never know if variable names were called 'a, b, c' or anything else.

Passing wrong number or type of variables would be similar to omitting some required values on the order form, or to writing in non-sensical values, e.g.:

amount: "red"

Then the chinese plant calls you back and complains that the form is all wrong and your order will be delayed; in a python program you will most likely get an exception raised when you run the program, or get wrong output.

Hope this helps!
posted by rainy at 7:58 AM on April 6, 2012 [1 favorite]


Classes and functions are two very very different beasts.

First you need to understand how it works that you can call from one function to another and pass parameters. You could try reading this or what Wikipedia has to say.

Then you need to understand that a class is just a way for someone to group together functions along with the data the functions operate on, so the user of the code can have some clue what to fiddle with and what not.

That's just like when you buy a stereo: Someone has put all the electronics in a little box with some knobs on the front. They don't just sell you a circuit board with a bunch of stuff hanging off. They decided what you would need to do with the stereo, and they built buttons on the front for the things you should fiddle with. Then they shut all the things you should not fiddle with into the box. Someone who writes a class says, if you make an object with data of THIS KIND, and use exactly THESE FUNCTIONS on it, everything will be tickety boo and nothing will explode.

Of course it seems fine to you in a tiny project that you just have a big old pile of data and you fiddle arbitrarily with whichever bits you like. But once the project gets bigger, or you work on it with someone else, or you work on it for long enough to forget things, then you need to organise it a bit. And you do that by putting each bit of data in a object that you can ONLY FIDDLE WITH USING THESE FUNCTIONS HERE.

So a counter class might say: To make a counter, you should have an integer, and only ever use that integer with this "increment" function.

A counter object would be a specific counter, with its own integer (which can only ever be incremented). You could have several counters for different things. Each one would have its own integer.

A function in the counter class would say "If you have a counter, and you want to increment it, you should do it like this".


When I write code I normally do it in two alternating steps. First I write a small pile of stuff. Then I look how I could organise it into classes. Usually it's easy to see that THESE functions operate on THIS data and THESE OTHER functions operate on THAT data. Then I go back to step 1 and write some more stuff. And so on.
posted by emilyw at 7:59 AM on April 6, 2012 [1 favorite]


Beginners tend to over think OOP and experienced OOP'ers tend to under think it. You are not an imbecile.

Objects on a whole are to hide (encapsulate) data and to provide methods to manipulate that data. The Objects are easy to pass around and the data stays nice and tidy. That is the simple way to think about it.

Think about two simple objects in real life -- two pieces of fruit. Don't think about WHO can do stuff to the apples, that is where beginners over think. Think about what the objects will need to do their tasks (like juice, slice, squeeze, etc).

class Orange {
public int squeeze() {
// image squeezing an orange and what is the result?
// likely x-amount juice. So imagine returning the integer value of juice returned.
int millilitersOfJuice = 100;
return millilitersOfJuice;
}
}

class Apple {
private int numberOfSlices = 0;

public int slice() {
// image slicing an apple and what is the result?
// likely x slices of apple.
numberOfSlices = 6
return numberOfSlices;
}
}

Now who will use these object? A human? Ok.. And this person has the two pieces of fruit. When you create a Person, he will squeeze the orange and slice the apple.

class Person {
Apple apple = new Apple();
Orange orange = new Orange();

Person(){
}

doSomethingToFruit() {
print( "I got " + apple.slice() + " slices of Apple!");
print( "I got " + orange.squeeze() + " mls of Orange juice!");
}

}

In your main program, you would create a Person. The Person would have the Orange and Apple. You tell the Person to do something to the fruit... and the Person will spit out what he actually did the the fruit.

main() {
Person person = new Person();
person.doSomethingToFruit();
}
posted by LeanGreen at 8:07 AM on April 6, 2012 [1 favorite]


Don't learn it the hard way. Learn it the easy way. Head First Python is really _exactly_ what you want. If you don't have a subscription to Safari Books Online, sign up for a free trial and check the book out. The head first book was the only thing that got my head wrapped around OOP, and I'm a guy that had been faking it for years and successfully writing stuff 1000+ lines.
posted by bfranklin at 8:07 AM on April 6, 2012


Maybe a little history would help you out. In the dim dark ages, we would define how memory was laid out for things. Sometimes we would call them records and they might be something like this:
|First   |Last    |xxxXXxxxx|
|00000000|00111111|111122222|
|01234567|89012345|678901234|
So we'd know that the first name was 8 bytes and went from offset 0 to offset to offset 7 inclusive and the last name was 8 bytes and went from offset 8 to offset 15 inclusive and the social security number was 9 bytes and went from offset 16 to offset 24. And you should know that even with years of experience doing this in assembly language, I still made two mistakes.

Worse, all the code would assume that the social security number was exactly 16 bytes from the start and there was hell to pay if you had to make more room for longer last names. So as we went on, we found it was better to name these things like this:
FNAMEOFFSET   EQU 0
LNAMEOFFSET   EQU 8
SSNOFFSET     EQU 16

And we'd happily use these symbolic names instead. Then we found that people mostly sucked at writing assembly language and would still screw up calculating the offsets and fixing them all when one changed, so we figured robots could do this better, so we made data structures (struct in C) and could do this:
#define FNAMELEN 8
#define LNAMELEN 8
#define SSNLEN 9
typedef struct {
    char[FNAMELEN] first;
    char[LNAMELEN] last;
    char[SSNLEN] ssn;
} t_person;
This took away a lot of that assembly language crap. Hooray! I could just refer to somePerson.first for their first name! Life was good again! Except that we discovered that we were giving everyone the ability to change first and last at any time. This is bad. It's like if you went to the bathroom and came back and someone had moved your chair and you didn't know about it until you tried to sit down and crashed on the floor.

So in C we came up with clever hacks to hide data - make it unseeable or opaque to the world and force people to call a function to get the data. So we'd make something like
extern char *getPersonFirstName(t_opaquePerson person);
and hand it out to the clients of person. This was the only way they could get the first name now. No snooping into our opaque data structures.

The problem is that doing all this wrapping and unwrapping is a pain in the ass and we figured robots could do it better, so we started taking all the data and all the functions that operate of the data and grouping them together and being able to label them as public (the world can see them) or private (only the bundled functions can see them).

So now I could do something like this:
public class Person {
private:
    char *first, *last;
public:
    char *getFirstName() { return first; }
    void setFirstName(char *newName) { free(first); first = strdup(newName); }
    char *getLastName() { return last; }
    void setLastName(char *newName) { free(first); first = strdup(newName); }
}
(note - don't write this code)

And what happens under the hood is that the compiler makes that struct that we had before. And it makes a new function for us, getFirstName, which takes 1 argument. Wait! getFirstName takes NO arguments, right? Wrong! The compiler magically creates a new argument that is the struct so that the function can get to it. It looks more or less like this:
char *QXZYSUIPerson_getFirstName(t_Person *this)
{
   return this->first;
}
I swear, this is what happens. It's that easy. Now, that QXZYSUIPerson is some garbage that the compiler introduces to prevent you from accidentally creating a global (not object) function or variable that would conflict with getFirstName, but that's not important, so ignore it. I'm putting it in here because some pedant would call me out on it if I didn't (and other things too, so you pedants just relax, OK?).

So what is OOP? It's the ability to take pieces of that represent your model of your problem and selectively hide them from the rest of the world (this is a good thing). It is a way of presenting a set of operations on these models and hide the implementation (also a good thing). These operations are methods. It is also a way of being able to express relationships between models and share code (this is dubiously a good thing - it's called inheritance and there has been a fair amount of backlash against it in recent years).

In general, it's like making sure like having a group of people in front of you and being able to treat them all in roughly the same way without caring (or needing to care) about how they got dressed that day, and in fact you can't really know except for what they show to you. In the dim dark ages, all the people walked around with their underwear on the outside and it was totally transparent, so the whole world could see (and play with) their junk and get messed up by the skid marks. It wasn't pretty. So we figured out how to dress properly and we found that it was too much work, so we made robots that dressed us and enforced that no one could touch our junk (except our friends, who have access to our private parts - HA! That C++ joke never gets old).
posted by plinth at 8:30 AM on April 6, 2012 [8 favorites]


Aside from the problem at hand, I want to reassure you that you aren't stupid. I've taught an intro to programming class to university students and OOP is the hardest concept for them. Ive seen it both as the teacher, and as a TA. So don't feel too bad. Despite what people may say, OOP is rather unintuitive.

It may help you to think of variables as boxes. In python, variables (boxes) can hold any object (like numbers, strings, and even objects you've designed and created). You refer to boxes by name, the name of the variable. A function in python defines a number of input parameters (boxes again) and code that uses whatever gets put in those boxes by referring to the box by name.

A class in python can be thought of as a blueprint or a rubber stamp. When you create an individual object from a class (known as an instance), you create the object according to the blueprints. Or, in the rubber stamp metaphor, you press the stamp into some ink and then stamp it onto some paper; the ink stamped onto the paper is one of many instances that you could create, all identical. The purpose of the metaphor is to show you that the class you define is how all objects begin their existence, but that after creation, data stored in those objects may change, just like how you might choose to tear out the godawful shag carpet that came with your manufactured home (according to the blueprints) and replace it with wood flooring.

So, moving away from the metaphor, when you write a class in python, you are defining a number of named boxes that will be kept together as one object. The contents of these boxes can be different from object to object. You are also defining a number of functions associated with objects created from your class. The self box is filled in by python to contain the object whose function was called, since all objects created from your class share this function. The rest of the boxes function the same as with a non-object oriented function.
an_object.some_method("hello")
vvvvvvvvvvvvvvvvv
class SomeObject(object):
    def some_method(self, greeting):
        print greeting
To reiterate, you are not stupid for not getting OOP. It's a hard thing to grasp. It may just require enough time for your brain to finally see what's going on. This happens to all of us when faced with a sufficiently abstract idea.
posted by Axle at 8:37 AM on April 6, 2012


I am sure you have seen a number of tutorials already that start like this, so bear with me. But I remember quite clearly when it all started to make sense, and this is the way I was thinking then.

Think of objects as a model of a real world thing. (We can ignore the more abstract stuff for now.) So in your example, it looks to me like there are four possible objects that could be modeled in code: Map, Player, Location, and Engine. Engine could also be procedural, because it is less of an object and more controlling logic for how the objects interact.

We might define a map in pseudocode as follows, (note that I am not tying to replicate your example.)

Map{

// Properties:
var locations = [];
var currentLocation;

// Constructor
construct(locations) {
this.locations = locations;
}

// Methods:
getCurrentLocation = function();
nextLocation = function();

}

Here we have a simple map object, which contains two properties: an array of locations and a pointer to the current location. There is a constructor, which is code that gets executed whenever a map object is created. This simply takes an array of locations and saves it to the map's "locations" property. In addition, there are two methods (for now the actual implementation of these methods is not important): one to get the current location, and one to move to the next location in the array.

Other properties we might add to map are things like randomLocation() and setLocation(), or if you want the map to be dynamic, addLocation() and removeLocation(). Note that all the properties and methods of map are centered around keeping track of locations, which is what a map is for. But also note that if we were to run this code, it would not do anything on its own. It just defines how a map will behave in our program, if we use it.

Now lets define a location for the map to keep track of.

Location
{

// Properties
name;

// Constructor
construct(name,action){
this.name = name;
this.action = action;
}

// Methods
action = function()

}

This is a very simple class with one property and one method. The constructor takes two arguments: the name and the action. This means that the second parameter is actually meant to be a function. Not all languages support passing functions, but I am doing it this way here because I think it helps understand how things can get passed around. Don't worry if this seems confusing, because next we are going to talk about how to create the two objects we have defined, and how they interact, and things should become a lot more clear.

As I said before, nothing we have written yet will actually do anything. It's just descriptions. In order to make them do something, we need to create some instances of our objects.

myLocation = new Location("Paris");

This line creates two things. It creates a variable "myLocation", and it also creates a new Location object. Then it sets the variable to reference the object. The variable can be passed into functions or other objects, and they will be able to use it to access the object. The object, though, is not the variable. We could create another variable and make that reference the object too:

locationReference = myLocation;

Not both of those names reference the same location object. (In some languages it works differently, but the important thing to keep in mind is that the variable and the object are two separate but linked things.)

But you have also probably noticed that we did not pass the second parameter when we created that location. In the class, it was defined to take two parameters, both a name and an action. So let's create our location again, and this time pass everything it needs:

myLocation = new Location("Paris",function(){print("You are in Paris!");});

Now we are passing in a string ("Paris") and a function. Functions, like classes, are usually created along with a name that can be used to reference them, but in this case we are creating the function without a name, because the constructor is going to assign the function to the name "action" inside the object.

To break it down, we really have three types of things floating around in a program: names, data, and executable code.

Names can refer to data, such as:

myString = "Hello!";

Names can also refer to executable code:

function myFunction(name) {
print "Hello " + name;
}

Or even collections of the two, which is what a class is:

myClass = new Class();

So when we pass things around in a program, usually that is happening is that we are creating things, assigning names to them, and then passing around the names. Now lets create a map with a few locations in it.

// Create an array
locationArray = [];

// Create three locations and add them to the array.
locationArray.push(new Location("London",function(){print("Good day!")}));
locationArray.push(new Location("Paris",function(){print("Bonjour!")}));
locationArray.push(new Location("Reykjavik",function(){print("Goðan daginn")}));

// Create a map, and pass it the array of locations
myMap = new Map(locationArray);

// Execute the nextLocation method of the map we just created.
myMap.nextLocation();

// Get the current location, and execute the "action" method.
myMap.getCurentLocation().action();

This example should demonstrate fairly well how variables and references get passed around. We have a short piece of procedural code (not part of a class) which sets up some objects and has them interact. The objects behave as we defined them. Lets follow the function that prints out "Good Day":

First, this function is created without a name, and passed to the constructor of the Location object. A new Location is created with the "name" property of "London" and our function is assigned the name of "action". This location has no name of its own, but is part of an array, so you might say it has the name of locationArray[0] and the function we are watching has the name locationArray[0].action().

Next, the array which references our new location object is passed to the constructor of the Map object. The Map object creates a new name for it ("locations") so that all the methods of the Map function will know where to find it. So now the function we are watching has the name myMap.locations[0].action(). It also still has the name locationArray[0].action().

Third, the "nextLocation" method of the Map object is called. We never defined this, but for the sake of the example, let's say that if no location is selected, it selects the first location in the array and names it "currentLocation". Remember that things can have more than one name, so even though it is named myMap.currentLocation, it will also still be named myMap.locations[0]. The function we are watching can now be found at myMap.currentLocation.action() and myMap.locations[0].action(). It also still has the name locationArray[0].action().

Finally, we call myMap.getCurrentLocation() which returns the value of myMap.currentLocation(). This method returns the value of myMap.currentLocation, which as you will remember from the previous step, is the Location object we have been watching. Because this function returns that value, we can use it just like the name, and recerence the function we are following like so: myMap.getCurentLocation().action(). And that is in fact the final line of the example, which would print out "Good day!".
posted by Nothing at 8:54 AM on April 6, 2012


I am a lazy worker (or I sometimes substitute a manager), and I pass my work on to you. You do it, and pass the results back to me.

Oh, and by the way, I don't care HOW you did the work, just do it and get back to me when you're done.
posted by SuperSquirrel at 9:48 AM on April 6, 2012


If nothing else works, then try this explanation. It's long and complicated and it involves recapitulating the history of programming, but it's worth a shot!

So the thing about objects in Python is that they're really just weird dictionaries. For example, here's a class that doesn't have any methods:
>>> class Person(object):
...   pass
... 
>>> guy = Person()
>>> guy.name = "bob"
>>> guy.favorite_color = "red"
>>> print guy.name
bob
They're weird dictionaries in the sense that you use . to access values in them (called "attributes") instead of square brackets. Also, to make a new object, you have to call the name of the class as though it were a function (Person()) instead of using curly brackets. Here's that same code, but written with a dictionary instead of an object:
>>> dict_person = dict()
>>> dict_person["name"] = "bob"
>>> dict_person["favorite_color"] = "red"
>>> print dict_person["name"]
bob
You can imagine making a function that would take a dictionary as a parameter, and then do something with that dictionary. Here's a function that prints out the value for a name key in a dictionary:
>>> def get_dict_name(d):
...   print d["name"]
... 
>>> get_dict_name(dict_thing)
bob
Likewise, you could make a function that takes an object as a parameter and prints out the value of its name attribute:
>>> def get_name(obj):
...   print obj.name
... 
>>> get_name(thing)
bob
We could make a function, let's call it init, that initializes an empty object, like so:
>>> def init(obj):
...   obj.name = "John Q. Default"
...   obj.favorite_color = "chartreuse"
... 
>>> new_guy = Person()
>>> init(new_person)
>>> new_person.name
'John Q. Default'
>>> new_thing.favorite_color
'chartreuse'
So that's pretty cool. We could define another function that does something more interesting with our object, like display a declaration of hue-love:
>>> def exclaim(obj):
...   print "my name is " + obj.name + " and I love " + obj.favorite_color
... 
>>> exclaim(new_thing)
my name is John Q. Default and I love chartreuse
We're doing object-oriented programming! Sort of! We've at least got objects, and we're passing them into functions, and those functions operate on the data in the object. A problem arises when we want to have another kind of thing—another class. Maybe in addition to Persons, we also want to have Dogs. Easy enough to declare the class and start making objects:
>>> class Dog(object):
...   pass
... 
>>> fido = Dog()
>>> fido.name = "fido"
>>> fido.breed = "french bulbradoodle"
Hey, sweet. Our Dogs should have a way to exclaim stuff as well, right? Let's do that:
>>> def exclaim(obj):
...   print "my name is " + obj.name + " and I'm proud to be a " + obj.breed
... 
>>> exclaim(fido)
my name is fido and I'm proud to be a french bulbradoodle
Nice, but now what happens if we use the same function on our Person object? Disaster!
>>> exclaim(new_guy)
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 2, in exclaim
AttributeError: 'Person' object has no attribute 'breed'
When we redefined exclaim, we overwrote our old version. The new version only works with the attributes we're putting on Dog objects (like breed), not with the attributes on Person. We could solve this problem by making separate functions for both kinds of objects:
>>> def dog_exclaim(obj):
...   print "my name is " + obj.name + " and I'm proud to be a " + obj.breed
... 
>>> def person_exclaim(obj):
...   print "my name is " + obj.name + " and I love " + obj.favorite_color
... 
>>> dog_exclaim(fido)
my name is fido and I'm proud to be a french bulbradoodle
>>> person_exclaim(new_guy)
my name is John Q. Default and I love chartreuse
This is basically how programming got done before the object-oriented paradigm gained prominence. Separate functions to do things on different kinds of data. The problem with this solution is that it's verbose, clunky, and prone to error.

Fortunately, Python gives you a better solution to the problem! It lets you define functions inside of a class. Functions defined inside of a class (called methods) exist only for objects of that class. If we were to redefine our Person class like so:
>>> class Person(object):
...   def exclaim(self):
...     print "my name is " + self.name + " and I love " + self.favorite_color
... 
>>> dude = Person()
>>> dude.name = "Horatio"
>>> dude.favorite_color = "blue-green"
>>> dude.exclaim()
my name is Horatio and I love blue-green
When we write object_name.function_name(), Python translates this behind the scenes into something like "call the function function_name defined inside the class that object_name belongs to. Pass object_name to that function as the first parameter, so that the function knows what object it's operating on."

It seems elaborate, but the advantages of this approach are manifold: Two other things to know:

(1) Methods can take multiple parameters. As I mentioned above, the first parameter to a method is always going to be the object that the method was called on, but the rest of the parameters work as they do in any other function. We could rewrite our Person class to incorporate a function to print out what that person's favorite color of X might be:
>>> class Person(object):
...   def exclaim(self):
...     print "my name is " + self.name + " and I love " + self.favorite_color
...   def favorite_color_thing(self, thing):
...     print "the best " + thing + " is a " + self.favorite_color + " " + thing
... 
>>> dude = Person()
>>> dude.name = "Horatio"
>>> dude.favorite_color = "blue-green"
>>> dude.favorite_color_thing("apple")
the best apple is a blue-green apple
(2) Python lets you define a special, weird method called __init__ in your class. Python calls this method whenever you create an object using the ClassName() syntax. Any parameters that you include inside the parentheses in the ClassName() call will be passed to this function—with, again, the object that's being operated on as the first parameter. Let's make an __init__ method for Person that lets us initialize the object when we create it:
>>> class Person(object):
...   def __init__(self, name, favorite_color):
...     self.name = name
...     self.favorite_color = favorite_color
...   def exclaim(self):
...     print "my name is " + self.name + " and I love " + self.favorite_color
...   def favorite_color_thing(self, thing):
...     print "the best " + thing + " is a " + self.favorite_color + " " + thing
... 
>>> dude = Person('Dr. Tim Obtuse', 'orangish')
>>> dude.exclaim()
my name is Dr. Tim Obtuse and I love orangish
>>> dude.favorite_color_thing("hovercraft")
the best hovercraft is a orangish hovercraft
Hope that helps a little bit...?
posted by aparrish at 1:33 PM on April 6, 2012 [2 favorites]


So, you don't *have* to use classes and functions. I rewrote your program without them. So why bother? In this program, you really don't need them. But as programs get more complicated, they really help.


You actually understand functions already, and are already using them. Part of your code is:

spaced = raw_input("h or d? ")
spaced = spaced.lower()

So what does the second line do? It calls a function that converts the user input to lower case. You don't care how it does it, it isn't relevant to what you're doing, you just want it to work. So calling that function is easier than processing it yourself, it saves you time, and it makes your code more readable. I think you're just confusing yourself because you are the one writing the functions for this one.

Let's say you wanted to make a bunch of different card games. It would really make sense to write a class with some functions that handled all the stuff like shuffling, identifying cards, drawing, discarding, etc, so you could actually have the game mechanics ready to go for each game. Then you could just import that class at the beginning of your new blackjack program, and get to work.

Otherwise, you'd have to redo all the card handling stuff every single time you wanted to work with a deck of cards, which is silly. Why work more than you have to?
posted by zug at 4:09 PM on April 6, 2012


As Larry Wall has said, one of the virtues of a good programmer is laziness. For this reason, I refuse to learn OOP. As you are currently discovering, it adds too much baggage to the program (not to mention the programmers brain) to be useful. To all you OOP programmers out there, my hat is off to you. You have succeeded in learning to do what I haven't, and won't. :-)

So, Shepherd, in case your struggles with OOP lead you to give up, you have not only my permission, but also my congratulations.
posted by exphysicist345 at 5:04 PM on April 6, 2012


I think everybody runs up against this particular wall the first time they encounter OOP. I'm not a professional programmer so I'm not an expert by any stretch of the imagination. If anyone comes along and says I don't know what I'm talking about, you should probably listen to them and not me. But a couple things that helped it click for me:

1. In procedural programming you have data and functions to manipulate that data. The data and the functions are completely separate entities, though they work together to accomplish some task.

2. In OOP the data and the functions are combined into a single entity: a class. So,

3. Instead of thinking of a objects(classes) as a model of a real world object, like a dog or a room or whatever, I think of objects as "data that can do stuff." The "doing stuff" is really secondary to the data.

So when writing something, think about the data first. Concentrate on the kinds of things you want to represent and how you want to represent them. Doing something with rooms? And each room has a name and a description? Okay:
class Room:
    def __init__(self, name, description):
        self.name = name
        self.description = description
That represents your room and that's all you need to write for now. Do the same for any other data types you might need. Again, concentrate on the data and don't worry, yet, about "doing stuff."

Once you've got all your data types start writing your program. When you find yourself writing a function which manipulates one of your data types, turn that function into a class method. For instance, if you find you've written this function:
def print_name(a_room):
    print a_room.name

which you would use like this:
x = Room("a room", "it's a mess")
print_name(x)

You can instead make it a class method like so:
Class Room:
    def __init__(self, name, description):
        # initialization code

    def print_name(self):
        print self.name

which you use like this:
x = Room("a room", "it's a mess")
x.print_name()
Both versions of print_name are basically the same. "a_room" is changed to "self" and instead of explicitly passing "x" to the function, we call the function on x. (x.print_name() instead of print_name(x))

Anyway, this is getting kind of long. To reiterate:
Objects are data which can "do stuff" First, concentrate on what data you need and how you want to represent it. Write your data types (basic classes with only an __init__ method for instance) with an eye to what your classes are without worrying too much about what it is that they do.

Once you have all your basic data types in place, then start adding the functions which do stuff. It's a simplistic description of OOP, but it helped me start to figure it out.
posted by Mister_Sleight_of_Hand at 7:36 AM on April 7, 2012 [1 favorite]


In the case of OOP you have classes (which most closely correspond to types of things) and you have objects (individual examples of things) and you have functions (what those things or types of things do).

So let's say you have a class called Animal. For our purposes let's assume that *all* Animal objects eat, mate, die, and sleep. These are universal actions. But there's not such thing as a generic animal in the real world -- only specific kinds of animals. So an "Animal" does not exist in the wild. It's what's known as an abstract class.

A tiger is a kind of animal and tigers do exist in the world, so it's not an abstract class, just a normal class. In addition to the normal eat, mate, die, and sleep, tigers have their own function called "maul".

Let's look at an instance of the class Tiger.

Although a lot more can go into it, let's say that to create a tiger, all you need is a name. In python, it would look something like this:
class Tiger(Animal):
    def __init__(self, name):
        self.name = name
        super(Animal, self).__init__()

    ...
Tony the Tiger is an instance of Tiger, so he can maul. He can also do the other normal actions.

Here's how maul might be defined as a function:
    def maul(self, prey):
        prey.die() # we assume here that the tiger successfully kills his prey
        if self.is_hungry: # this is a universal property defined by the abstract Animal class
            self.eat(prey)
First, notice that since the prey is an animal, like all Animals it shares the die action. This is because we can send individual objects/instances to a function, and we can act on that object.

So now we see that after mauling, if the tiger is hungry, it eats his prey. It passes the variable -- in this case, the prey -- to its own eat function. When eat runs, it receives the prey as the parameter variable and does whatever it will to it.

I think the real problem with the code example you included is that I don't think it does a great job of modeling real object oriented functions. I'm not clear on whether this is code you wrote yourself or copied from elsewhere, but it's seriously not a good way to get an idea of how OOP works. Essentially, if you see a lot of print or exit statements, IMHO you're not looking at code that was written in an object-oriented way.

The problem with the code you linked to is that what it's sending back and
forth are names of functions. The running code then uses getattr to
essentially grab that named function from map object, and then call it. The
called function, in turn, returns the name of another function to call (or exits). I think this is confusing and not a good way of implementing this. Each time you want to add a location to this game, you're going to have to write a new function and add it to the map function. If you decide to change the order of when things happen, or add a scene between two different scenes, it's going to get confusing.

If you were to rewrite this in a more object oriented way, this is the organization I'd use:
class Choice(object):
    def __init__(self, label, command, response=None, next=None):
        self.label = label
        self.command = command
        self.response = response
        self.next = next

    def choose(self):
        print self.response
        return self.next

class Scene(object):
    def __init__(self, description, choices=None):
        self.description = description
        self.choices = choices

    def list_options(self):
        return "Choices: %s" % ", ".join(
            ["%s (%s)" % (c.label, c.command) for c in current.choices
        )

    def decide(self, command):
        choice = None
        for c in current.choices:
            if c.command.lower() == action.lower():
                choice = c
                break
        if not choice:
            raise ValueError("%s, bozo." % " or ".join([c.command for c in self.choices]))
        return choice

class Engine(object):
    def __init__(self, start):
        self.scene = start
    
    def play(self):
        current = self.scene
        print current.description
        if current.choices:
            print current.list_options()
            choice = None
            while not choice:
                action = raw_input("> ")
                try:
                    choice = current.decide(action)
                except ValueError, error:
                    print error
            self.scene = choice.choose()
            if self.scene:
                self.play()


Now all that remains is to create the scenes. Notice how this is much more object oriented -- each scene is its own object, and it's a lot easier to extend than adding and moving around different functions.

It *is* a bit more complicated (okay, a *lot* more complicated) than the code you linked to, but it matches OOP ideas more cleanly.

Here are some example scenes. Note that we have to create them in backwards fashion because scenes point to others. It's definitely possible to write this in a cleverer fashion that handles this automatically for you.
death = Scene("You died")
home = Choice("home","h","Well, now you've gone home. Which is great, if you like home.")
drift = Choice("drift","d","You drift forever, a happy vagabond.")
outer_space = Scene("now you are in outer space! Do you want to go home, or drift?", choices=[home,drift])
taunt = Choice("taunt", "t", "improbably, that works!", outer_space)
shoot = Choice("shoot", "s", "You shoot! You miss! You die!", death)
corridor = Scene(""Gothons have killed your crew and one's about to blast you!"", choices=[shoot, taunt])
Gameplay starts with:
game = Engine(corridor)
game.play()
Note that I haven't tested this code, so there are probably some typos and errors that you'd have to clear up, but I feel like it presents some good OOP ideas. Now, instead of a Map object with each location being a function, now each location is an Location instance. Locations have a list of choices (Choice instances). All the engine does is output the description in the location, and then if the location has choices it outputs the display of those locations and grabs input and sends it to the decide function of the location object, which accepts that input and returns the matching choice. If not, the location throws a ValueError (because it's not a valid value input) that is caught by the engine, which loops again to grab the input. As soon as something valid comes up, the engine receives a choice. It then calls the choose function on that choice, which returns a new location, and the engine plays again.
posted by Deathalicious at 8:53 AM on April 12, 2012


« Older I'd like to legally take my sp...   |  Looking for molded/custom blue... Newer »
This thread is closed to new comments.