1

I like to create helper classes that can be used by other classes and where all methods are static (staticmethod). I need to wrap each method with a decorator @staticmethod, but this solution seems to me not very aesthetic. I decided to create a metaclass for such classes - tools, here is an abstract implementation example:

import types
import math


class StaticClass(type):
    def __new__(mcs, name, bases, attr):
        for name, value in attr.items():
            if type(value) is types.MethodType:
                attr[name] = staticmethod(value)
        return super().__new__(mcs, name, bases, attr)


class Tool(metaclass=StaticClass):

    def get_radius_from_area(area):
        return math.sqrt(area/Tool.get_pi())

    def get_pi():
        return math.pi


class CalcRadius:
    def __init__(self, area):
        self.__area = area

    def __call__(self):
        return Tool.get_radius_from_area(self.__area)


if __name__ == '__main__':
    get_it = CalcRadius(100)
    print(get_it())  # 5.641895835477563

Everything works and gives the correct result, but there are understandable and predictable problems with code inspection in the IDE (I use Pycharm 2019.2).

  • for def get_radius_from_area(area): Usually first parameter of a method in named 'self'. 'area' highlighted in yellow in the return of the method.

  • for get_pi(): Method must have a first parameter, usually called 'self' Void in brackets with out arguments is underlined in red.

If I add the line "# noinspection PyMethodParameters" above the class this partially solves the problem, but it looks even worse than dozens of @staticmethods.

I understand why this is happening and why the developers from JetBrains specially adapt parts of the code in their IDE for Django.

But can I somehow beautifully create a purely static class, in which all methods are static? Maybe metaclasses are not the best option and is there some kind of alternative solution?

2
  • 2
    Related: stackoverflow.com/questions/30556857/…. I think you're trying to force Python into doing something it doesn't really want to do. I'd suggest either just dealing with the @staticmethod being on all your class methods, or move it all into a module. For what it's worth, I think most Python devs would disagree with your aesthetic complaints about @staticmethod. Having it on all your methods more clearly communicates your intention than any roundabout method you could come up with to make this work, and clarity is good. Commented Sep 14, 2019 at 0:15
  • @skrrgwasme, I agree with you about the readability of the code, well, aesthetics are a matter of taste :) Commented Sep 14, 2019 at 0:20

3 Answers 3

3

Your metaclass isn't actually doing anything, because types.MethodType matches only bound method objects. You aren't getting any of those when you browse through the class namespace in __new__, so you never wrap anything in staticmethod.

You can fix it by changing the check to types.FunctionType (this will probably satisfy automated tools who will correctly see the method types):

class StaticClass(type):
    def __new__(mcs, name, bases, attr):
        for name, value in attr.items():
            if type(value) is types.FunctionType:
                attr[name] = staticmethod(value)
        return super().__new__(mcs, name, bases, attr)

But I'd suggest just doing away with the classes and using functions directly. Functions are first class objects in Python, you can pass them around between objects as much as you want. If you want a handy grouping of them, you can put them in lists or dictionaries, or use modules to collect their code in various groupings (and use packages to group modules). I'd also advise you to avoid using leading double-underscore __names to try to get privacy for your attributes by invoking name mangling. It doesn't actually protect your data from anything (outside code can still get at it), and it makes it a whole lot harder to debug. It's included in Python to help you avoid accidental name collisions, not to protect member variables as a matter of course.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the explanation about types.FunctionType. As for double underlining, I know that getting access to it is easy - get_it._CalcRadius__area. This is probably a habit that remained after C++ with its private.
2

Why not encapsulate your code at the module level, instead of class, and offer functions to your users?

Your code could be as simple as that:

import math


def calc_radius(area):
    return math.sqrt(area/math.pi)


print(calc_radius(100))

5 Comments

This is just an abstract example that I invented from my head on the go. If you rephrase your question: “why I can’t use functions instead of static methods?”, then I would answer that - "I don’t like mixing the functional level and OOP", it seems to me that it is unacceptable to use both in the same script, although of course this is not is declorized.
@YeapyBen. Under the circumstances, using an extra level of namespace seems counter productive. A static class creates a namespace and a module creates a namespace. There is no practical difference between them for you.
@MadPhysicist, It’s not how it is implemented at a low level, but how it looks.
@YeapyBen. That's what I'm referring to. The way you access the methods from the module is identical to the class, just without the extra pointless level of indirection. If you never plan to instantiate the class, it looks like crap to have it, in my opinion.
You are talking of aesthetics, but outside job security, there are zero reasons that justify a metaclass! A simple namespace as @MadPhysicist elegantly phrased it (here a module with functions) does what you need, I stand by my answer!
2

You can also create a class decorator, which I personally prefer over a metaclass in aesthetics:

import types

def staticclass(cls):
    for name, value in vars(cls).items():
        if isinstance(value, types.FunctionType):
            setattr(cls, name, staticmethod(value))
    return cls

so that:

@staticclass
class Tool:

    def get_radius_from_area(area):
        return math.sqrt(area/Tool.get_pi())

    def get_pi():
        return math.pi


class CalcRadius:
    def __init__(self, area):
        self.__area = area

    def __call__(self):
        return Tool.get_radius_from_area(self.__area)


if __name__ == '__main__':
    get_it = CalcRadius(100)
    print(get_it())

outputs: 5.641895835477563

EDIT: @Blckknght correctly points out that a function is not a bound method until it is actually bound to an instance, which the class object is not. Switching to isinstance(value, types.FunctionType) would allow proper wrapping.

2 Comments

Oh, this is a great solution, it didn't occur to me. Thank you very much!
UPD: Unfortunately, problems remain... IDE does not understand decorators in the same way as metaclasses. By and large, there is no benefit here, Python along with PyChart, insists that I have the wrong syntax. However, they are probably right in something :D

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.