Wonky: bound methods, static methods, or independent functions in Python?

Hi all -
This is a very wonky question, but we’re writing a piece of Landlab component code that uses a number of fairly simple functions for various mathematical calculations. For example, one function calculates channel width using a simple empirical power-law formula. These functions are designed to called by code embedded in a Python class (i.e., the Landlab component). We’re wrestling with a silly question: is it better to have simple in-and-out functions like this be implemented as independent functions (outside of the class, but called from within the class, and not really used by anything else), as bound methods inside the class (even though they don’t need “self”), or as static methods inside the class (so, no “self”). Anyone have a recommendation about this kind of thing? (from Greg Tucker, Susannah Morey, and Yuval Shmilovitz)

Example:

def bar():
…pass

class Foo():
…def some_bound_method(self):
…bar()

OR

class Foo():

…def bar(self):
…(do stuff that doesn’t involve self)

…def some_bound_method(self):
…self.bar()

OR

class Foo():

@staticmethod
…def bar():
…(do stuff)

…def some_bound_method(self):
…Foo.bar()

(PS - not sure how to get code-literal formatting, hence the dots)

I think this would be the most pythonic way of implementing it. Additionally, if this function is only used inside the class in the same module, you can start the name with an underscore. This way you denote that the function is not part of the public API, and only used internally in your package.

"""My module containing the Foo class."""


def _bar(...): ...


class Foo():
    def some_bound_method(self):
        some_thing = _bar(...)

If you then do;

from mypackage import mymodule
mymodule.  # IDE suggestions will only be "Foo", as _bar starts with underscore

Instead of using staticmethod to group a bunch of utility functions in a class, it is probably better to put them into their own module. This still makes them easy to find and grouped together.

To quote Guido van Rossum:

Honestly, staticmethod was something of a mistake – I was trying to do something like Java class methods but once it was released I found what was really needed was classmethod. But it was too late to get rid of staticmethod.

classmethods are useful though, as they can be used to create functions that can instantiate your class in different ways;

class Foo()
    def __init__(self, ...):
        ...

    @classmethod    
    def load_from_file(cls, ...):
        ...
        return cls(...)

Code formatting is done the Markdown way, see the </> button on top of the text editor. If you start it with ```python your code block is formatted with Python syntax highlighting.

2 Likes

Thanks @BSchilperoort, I didn’t know about the BDFL (Benevolent Dictator For Life) regret about @staticmethod. Interesting. I like the idea of private functions in their own space.

1 Like

I really need to listen to this advice. I always end up writing staticmethods when they really should be in a separate utility module…

As a side point, I’m curious what power-law you use. Do you base it on flow? I tend to use width = 1.22 * flow^0.557 [1] but I’m concious this probably isn’t very realistic in many situations!

[1] From this paper, which I think specifically parameterised the equation for the US: https://doi.org/10.1111/j.1752-1688.1994.tb03321.x

Good question! It’s just the generic hydraulic geometry power-law W = a Q^b. In practice I tend to use b = 0.5 and the coefficient a as whatever value will generate a reasonable width for a given dataset. For landscape evolution models, we control the runoff rate that turns drainage area into Q, so it’s straightforward to find the coefficient a from a dataset of width and drainage area. One example is this paper: https://www.sciencedirect.com/science/article/pii/S0169555X02003495, which has data on width v drainage area and discharge v drainage area. It shows a width of very roughly 4 m at a drainage area of 1 km2.

1 Like