[Tutor] class functions/staticmethod?

Cameron Simpson cs at cskk.id.au
Tue Aug 13 19:58:35 EDT 2019


On 11Aug2019 22:58, James Hartley <jjhartley at gmail.com> wrote:
>I am lacking in understanding of the @staticmethod property.
>Explanation(s)/links might be helpful.  I have not found the descriptions
>found in the Internet wild to be particularly instructive.

You have received some answers; to me they seem detailed enough to be 
confusing.

I think of things this way: what context does a method require?  Not 
everything needs the calling instance.

Here endeth the lesson.

============

All this stuff below is examples based on that criterion:

Here's a trite example class:

  class Rectangle:
    def __init__(self, width, height):
      self.width=width
      self.height = height

Most methods do things with self, and are thus "instance methods", the 
default. They automatically receive the instance used to call them as 
the first "self" argument.

  def area(self):
    return self.width * self.height

They need "self" as their context to do their work.

Some methods might not need an instance as context: perhaps they return 
information that is just based on the class, or they are factory methods 
intended to return a new instance of the class. Then you might use a 
@classmethod decorator to have the calling instance's class as the 
context.

  @classmethod
  def from_str(cls, s):
    width, height = parse_an_XxY_string(s)
    return cls(width, height)

And some methods do not need the class or the instance to do something 
useful:

  @staticmethod
  def compute_area(width, height):
    return width * height

and so we don't give them the instance or the class as context.

Now, _why_?

Instance methods are obvious enough - they exist to return values 
without the caller needing to know about the object internals.

Class methods are less obvious.

Consider that Python is a duck typed language: we try to arrange that 
provided an object has the right methods we can use various different 
types of objects with the same functions. For example:

  def total_area(flat_things):
    return sum(flat_thing.area() for flat_thing in flat_things)

That will work for Rectangles and also other things with .area() 
methods. Area, though, is an instance method.

Class methods tend to come into their own with subclassing: I 
particularly use them for factory methods.

Supposing we have Rectangles and Ellipses, both subclasses of a 
FlatThing:

  class FlatThing:
    def __init__(self, width, height):
      self.width=width
      self.height = height
  @classmethod
  def from_str(cls, s):
    width, height = parse_an_XxY_string(s)
    return cls(width, height)

  class Rectangle(FlatThing):
    def area(self):
      return self.width * self.height

  class Ellipse(FlatThing):
    def area(self):
      return self.width * self.height * math.PI / 4

See that from_str? It is common to all the classes because they can all 
be characterised by their width and height. But I require the class for 
context in order to make a new object of the _correct_ class. Examples:

  rect = Rectangle.from_str("5x9")
  ellipse = Ellipse.from_str("5x9")
  ellispe2 = ellipse.from_str("6x7")

Here we make a Rectangle, and "cls" is Rectangle when you call it this 
way. Then we make an Ellipse, and "cls" is Ellipse when called this way.  
And then we make another Ellipse from the first ellipse, so "cls" is 
again "Ellipse" (because "ellipse" is an Ellipse).

You can see that regardless of how we call the factory function, the 
only context passed is the relevant class.

And in the last example (an Ellipse from an existing Ellipse), the class 
comes from the instance used to make the call. So we can write some 
function which DOES NOT KNOW whether it gets Ellipses or Rectangles:

  def bigger_things(flat_things):
    return [ flat_thing.from_str(
               "%sx%s" % (flat_thing.width*2, flat_thing.height*2))
             for flat_thing in flat_things
           ]

Here we could pass in a mix if Rectangles or Ellipses (or anything else 
with a suitable from_str method) and get out a new list with a matching 
mix of bigger things.

Finally, the static method.

As Peter remarked, because a static method does not have the instance or 
class for context, it _could_ be written as an ordinary top level 
function.

Usually we use a static method in order to group top level functions 
_related_ to a specific class together. It also helps with imports 
elsewhere.

So consider the earlier:

  @staticmethod
  def compute_area(width, height):
    return width * height

in the Rectangle class. We _could_ just write this as a top level 
function outside the class:

  def compute_rectangular_area(width, height):
    return width * height

Now think about using that elsewhere:

  from flat_things_module import Rectangle, Ellipse, compute_rectangular_area

  area1 = compute_rectangular_area(5, 9)
  area2 = Rectangle.compute_area(5, 9)
  area3 = Ellipse.compute_area(5, 9)

I would rather use the forms of "area2" and "area3" because it is clear 
that I'm getting an area function from a nicely named class. (Also, I 
wouldn't have to import the area function explicitly - it comes along 
with the class nicely.)

So the static method is used to associate it with the class it supports, 
for use when the caller doesn't have an instance to hand.

Cheers,
Cameron Simpson <cs at cskk.id.au>


More information about the Tutor mailing list