In Python 2.x Tkinter code, you see a lot of stuff like this:
    class MyFrame(Frame):
        def __init__(self, parent, n):
            Frame.__init__(self, parent)
            self.n = n
Why?

Inheritance and overriding

Some people start on Tkinter before getting far enough into learning Python. You should definitely read the Classes chapter in the Python tutorial, but I'll summarize the basics.

First, you're subclassing (inheriting from) the Tkinter class Frame. This means instances of your class are also instances of Frame, and can be used like Frames, and can use the internal behavior of Frame.

When someone constructs a MyFrame object by typing MyFrame(root, 3), that creates a new MyFrame instance and calls new_instance.__init__(root, 3).

You've overridden the parent's __init__ method with your own, so that's what gets called.

But if you want to act like a Frame, you need to make sure all the stuff that gets done in constructing a Frame object also gets done in constructing your object. In particular, whatever Tkinter does under the covers to create a new widget in the window manager, connect it up to its parent, etc. all has to get done. So, you need Frame.__init__ to get called for your object to work.

Unlike some other languages, Python doesn't automatically call base class constructors for you; you have to do it explicitly. The advantage of this is that you get to choose which arguments to pass to the parent--which is handy in cases (like this example) where you want to take extra arguments.

Not so super

The normal way to do this is to use the super function, like this:
    class MyFrame(Frame):
        def __init__(self, parent, n):
            super(MyFrame, self).__init__(parent)
            self.n = n
Unfortunately, that doesn't work with Tkinter, because Tkinter uses classic classes instead of new-style classes. So, you have to do this clunky thing instead where you call the method on the class instead of calling it like a normal method.

You don't really want to learn all about classic classes, because they're an obsolete technology. (In Python 3, they no longer exist.) You just need to know two things:
  • Never use classic classes when you can help it. If you don't have anything to put as your base class, use object.
  • When you can't help it and are forced to use classic classes (as with Tkinter), you can't use super, so you have to call methods directly on the class.
But how does the clunky thing work?

Unbound methods

If you want all the gory details, see How Methods Work. But I'll give a short version here.

Normally, in Python—like most other object-oriented languages—you call methods on objects. The way this works is a bit surprising: foo.bar(spam) actually constructs a "bound method" object foo.bar, then calls it like a function, with foo and spam as the arguments. That foo then becomes the self parameter that you have to put in every method definition.

Since classes themselves are just another kind of objects, you can call methods on them too, like FooType.bar(spam). But here, Python doesn't have any bound object to get passed as your self parameter—it constructs an "unbound method" FooType.bar, then calls it with just spam as an argument, so there's nothing to match up with your self parameter. (Python could have been designed to pass the FooType class itself as the self parameter, but that would be confusing more often than it would be useful. When you want that behavior—for example, to create "alternate constructors" like datetime.datetime.now, you have to ask for it explicitly, with the @classmethod decorator.) So, you have to pass it yourself.

In other words, in this code, the two method calls at the end are identical:
    class FooType(object):
        def bar(self, spam):
            print self, spam
    foo = FooType()
    foo.bar(2)
    FooType.bar(foo, 2)
So, why would you ever use the clumsy and verbose second form? Basically, only when you have to. Maybe you want to pass the unbound method around to call on an object that will be created later. Maybe you had to look up the method dynamically. Or maybe you've got a classic class, and you're trying to call a method on your base class.
1

View comments

It's been more than a decade since Typical Programmer Greg Jorgensen taught the word about Abject-Oriented Programming.

Much of what he said still applies, but other things have changed. Languages in the Abject-Oriented space have been borrowing ideas from another paradigm entirely—and then everyone realized that languages like Python, Ruby, and JavaScript had been doing it for years and just hadn't noticed (because these languages do not require you to declare what you're doing, or even to know what you're doing). Meanwhile, new hybrid languages borrow freely from both paradigms.

This other paradigm—which is actually older, but was largely constrained to university basements until recent years—is called Functional Addiction.

A Functional Addict is someone who regularly gets higher-order—sometimes they may even exhibit dependent types—but still manages to retain a job.

Retaining a job is of course the goal of all programming. This is why some of these new hybrid languages, like Rust, check all borrowing, from both paradigms, so extensively that you can make regular progress for months without ever successfully compiling your code, and your managers will appreciate that progress. After all, once it does compile, it will definitely work.

Closures

It's long been known that Closures are dual to Encapsulation.

As Abject-Oriented Programming explained, Encapsulation involves making all of your variables public, and ideally global, to let the rest of the code decide what should and shouldn't be private.

Closures, by contrast, are a way of referring to variables from outer scopes. And there is no scope more outer than global.

Immutability

One of the reasons Functional Addiction has become popular in recent years is that to truly take advantage of multi-core systems, you need immutable data, sometimes also called persistent data.

Instead of mutating a function to fix a bug, you should always make a new copy of that function. For example:

function getCustName(custID)
{
    custRec = readFromDB("customer", custID);
    fullname = custRec[1] + ' ' + custRec[2];
    return fullname;
}

When you discover that you actually wanted fields 2 and 3 rather than 1 and 2, it might be tempting to mutate the state of this function. But doing so is dangerous. The right answer is to make a copy, and then try to remember to use the copy instead of the original:

function getCustName(custID)
{
    custRec = readFromDB("customer", custID);
    fullname = custRec[1] + ' ' + custRec[2];
    return fullname;
}

function getCustName2(custID)
{
    custRec = readFromDB("customer", custID);
    fullname = custRec[2] + ' ' + custRec[3];
    return fullname;
}

This means anyone still using the original function can continue to reference the old code, but as soon as it's no longer needed, it will be automatically garbage collected. (Automatic garbage collection isn't free, but it can be outsourced cheaply.)

Higher-Order Functions

In traditional Abject-Oriented Programming, you are required to give each function a name. But over time, the name of the function may drift away from what it actually does, making it as misleading as comments. Experience has shown that people will only keep once copy of their information up to date, and the CHANGES.TXT file is the right place for that.

Higher-Order Functions can solve this problem:

function []Functions = [
    lambda(custID) {
        custRec = readFromDB("customer", custID);
        fullname = custRec[1] + ' ' + custRec[2];
        return fullname;
    },
    lambda(custID) {
        custRec = readFromDB("customer", custID);
        fullname = custRec[2] + ' ' + custRec[3];
        return fullname;
    },
]

Now you can refer to this functions by order, so there's no need for names.

Parametric Polymorphism

Traditional languages offer Abject-Oriented Polymorphism and Ad-Hoc Polymorphism (also known as Overloading), but better languages also offer Parametric Polymorphism.

The key to Parametric Polymorphism is that the type of the output can be determined from the type of the inputs via Algebra. For example:

function getCustData(custId, x)
{
    if (x == int(x)) {
        custRec = readFromDB("customer", custId);
        fullname = custRec[1] + ' ' + custRec[2];
        return int(fullname);
    } else if (x.real == 0) {
        custRec = readFromDB("customer", custId);
        fullname = custRec[1] + ' ' + custRec[2];
        return double(fullname);
    } else {
        custRec = readFromDB("customer", custId);
        fullname = custRec[1] + ' ' + custRec[2];
        return complex(fullname);
    }
}

Notice that we've called the variable x. This is how you know you're using Algebraic Data Types. The names y, z, and sometimes w are also Algebraic.

Type Inference

Languages that enable Functional Addiction often feature Type Inference. This means that the compiler can infer your typing without you having to be explicit:


function getCustName(custID)
{
    // WARNING: Make sure the DB is locked here or
    custRec = readFromDB("customer", custID);
    fullname = custRec[1] + ' ' + custRec[2];
    return fullname;
}

We didn't specify what will happen if the DB is not locked. And that's fine, because the compiler will figure it out and insert code that corrupts the data, without us needing to tell it to!

By contrast, most Abject-Oriented languages are either nominally typed—meaning that you give names to all of your types instead of meanings—or dynamically typed—meaning that your variables are all unique individuals that can accomplish anything if they try.

Memoization

Memoization means caching the results of a function call:

function getCustName(custID)
{
    if (custID == 3) { return "John Smith"; }
    custRec = readFromDB("customer", custID);
    fullname = custRec[1] + ' ' + custRec[2];
    return fullname;
}

Non-Strictness

Non-Strictness is often confused with Laziness, but in fact Laziness is just one kind of Non-Strictness. Here's an example that compares two different forms of Non-Strictness:

/****************************************
*
* TO DO:
*
* get tax rate for the customer state
* eventually from some table
*
****************************************/
// function lazyTaxRate(custId) {}

function callByNameTextRate(custId)
{
    /****************************************
    *
    * TO DO:
    *
    * get tax rate for the customer state
    * eventually from some table
    *
    ****************************************/
}

Both are Non-Strict, but the second one forces the compiler to actually compile the function just so we can Call it By Name. This causes code bloat. The Lazy version will be smaller and faster. Plus, Lazy programming allows us to create infinite recursion without making the program hang:

/****************************************
*
* TO DO:
*
* get tax rate for the customer state
* eventually from some table
*
****************************************/
// function lazyTaxRateRecursive(custId) { lazyTaxRateRecursive(custId); }

Laziness is often combined with Memoization:

function getCustName(custID)
{
    // if (custID == 3) { return "John Smith"; }
    custRec = readFromDB("customer", custID);
    fullname = custRec[1] + ' ' + custRec[2];
    return fullname;
}

Outside the world of Functional Addicts, this same technique is often called Test-Driven Development. If enough tests can be embedded in the code to achieve 100% coverage, or at least a decent amount, your code is guaranteed to be safe. But because the tests are not compiled and executed in the normal run, or indeed ever, they don't affect performance or correctness.

Conclusion

Many people claim that the days of Abject-Oriented Programming are over. But this is pure hype. Functional Addiction and Abject Orientation are not actually at odds with each other, but instead complement each other.
5

View comments

Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.