Dan, I should preface this by saying I don't substantially disagree with you, I just work differently and want to show how and why.
On 11Sep2020 21:24, Dan Sommers 2QdxY4RzWzUUiLuE@potatochowder.com wrote:
On 2020-09-12 at 09:57:10 +1000, Cameron Simpson email@example.com wrote:
@classmethod def func(cls, foo): print(foo)
A linter will warn you that "cls" is unused. With a static method:
@staticmethod def func(foo): print(foo)
a linter will be happy.
Think of @classmethod and @staticmethod as ways to write "clean" functions with no extraneous cognitive noise.
I concur with all of Cameron's technical details and explanations.
But no extraneous cognitive noise?
Specificly in the sense that the @classmethod example has an unused "cls" parameter. For a three line function this is trivial to ignore, but for a substantial function I'd be devoting annoying brainpower to understanding where it was used, and if it didn't appear, _should_ it be used? Thus noise. With the static method it is instantly clear that no instance or class context is used.
By definition, methods appear inside a class definition, and then I have to process the @staticmethod decorator. Effectively, the decorator "cancels" the class method status of the function.
For me, I have to process either the @staticmethod or @classmethod decorators equally - they just have different meanings. But at least they're up the front - I see them and their meaning for the function context is instantly available to me instead of needing to be deduced.
I can accomplish the same thing with clean module-level function, modulo the specific namespace in which the function is created.
So, in a module m:
class X: @staticmethod def f(x): print(x)
def f(x): print(x)
m.X.f and m.f are interchangeable.
Yes, technically. Let me say up front that I write plenty of module elvel plain functions.
However I do have plenty of use cases for @staticmethod in class definitions. They tend to be class utility functions, either for some common function used in the class methods, _or_ a common method meant to be available for outside use - particularly for a common pattern for outside use.
Here's an example of the former:
@staticmethod def pick_value(float_value, string_value, structured_value): ''' Chose amongst the values available. ''' if float_value is None: if string_value is None: return structured_value return string_value return float_value
@property def value(self): ''' Return the value for this `Tag`. ''' return self.pick_value( self.float_value, self.string_value, self.structured_value )
This is from a tag library of mine, where the tags live in a database and there are three fields: a float, a string and a JSON blob. Only one will be nonNULL. This supports a remarkable flexibility in tagging things. It is common in the class to pick amongst them, thus the static method. It is also not unreasonable to work with such a choice outside the class (so it might not be a private method/function).
On review, I've actually got relatively few examples of the latter category - methods for use outside the class - they are almost entirely @classmethods. The few which are static methods live in the class for purposes of conceptual hierarchy.
My commonest example is the transcription method from a binary structure module I have. Data structures each have a class which implements them, and all subclass a base class. This gets me a common API for parsing the structure and also for writing it back out.
When implementing a particular structure one always has a choice between implementing one of two "transcribe" methods. For a simple thing with a single "value" (eg an int) you'd implement "transcribe_value(value)", a static method. For something more complex you implement "transcribe()", a class or instance method depending. In the abstract base class each method calls the other - the subclass must provide a concrete implementation of one method.
Anyway, back to the static method: if is pretty common to want to write out a value using the transcription from a class, example:
value = 9 bs = BSUInt.transcribe_value(value)
without going to any hassle of instantiating an instance of BSUInt - we just want to convert from the Python type to bytes. There's a similar example for parsing.
The beauty here is that you have the same pattern of classname.transcribe_value(value) to use whatever binary format you want. And that's a static method because it doesn't need a class or instance for context - it just transcribes the data.
IMO, m.f has less cognitive load than m.X.f, at their definitions and at call sites. Full disclosure: I consider most of Object Oriented Programming to be extraneous cognitive noise.
Whereas I use OOP a lot. Hugely.
In other languages (*cough* Java *cough*), there are no functions outside of classes, and static methods fill that role.
Aye. I agree that is an example where static methods exist for language definition reasons instead of functionality. OTOH, the language design is deliberately like that to force encapsulation as a universal approach, which has its own benefits in terms of side effect limitation and code grouping, so there's an ulterior purpose there.
Cheers, Cameron Simpson firstname.lastname@example.org