Using static methods in python - best practice

后端 未结 4 1652
隐瞒了意图╮
隐瞒了意图╮ 2020-12-15 20:24

When and how are static methods suppose to be used in python? We have already established using a class method as factory method to create an instance of an object should be

相关标签:
4条回答
  • 2020-12-15 20:50

    When and how are static methods suppose to be used in python?

    The glib answer is: Not very often.

    The even glibber but not quite as useless answer is: When they make your code more readable.


    First, let's take a detour to the docs:

    Static methods in Python are similar to those found in Java or C++. Also see classmethod() for a variant that is useful for creating alternate class constructors.

    So, when you need a static method in C++, you need a static method in Python, right?

    Well, no.

    In Java, there are no functions, just methods, so you end up creating pseudo-classes that are just bundles of static methods. The way to do the same thing in Python is to just use free functions.

    That's pretty obvious. However, it's good Java style to look as hard as possible for an appropriate class to wedge a function into, so you can avoid writing those pseudo-classes, while doing the same thing is bad Python style—again, use free functions—and this is much less obvious.

    C++ doesn't have the same limitation as Java, but many C++ styles are pretty similar anyway. (On the other hand, if you're a "Modern C++" programmer who's internalized the "free functions are part of a class's interface" idiom, your instincts for "where are static methods useful" are probably pretty decent for Python.)


    But if you're coming at this from first principles, rather than from another language, there's a simpler way to look at things:

    A @staticmethod is basically just a global function. If you have a function foo_module.bar() that would be more readable for some reason if it were spelled as foo_module.BazClass.bar(), make it a @staticmethod. If not, don't. That's really all there is to it. The only problem is building up your instincts for what's more readable to an idiomatic Python programmer.

    And of course use a @classmethod when you need access to the class, but not the instance—alternate constructors are the paradigm case for that, as the docs imply. Although you often can simulate a @classmethod with a @staticmethod just by explicitly referencing the class (especially when you don't have much subclassing), you shouldn't.


    Finally, getting to your specific question:

    If the only reason clients ever need to look up data by ID is to construct an Entity, that sounds like an implementation detail you shouldn't be exposing, and it also makes client code more complex. Just use a constructor. If you don't want to modify your __init__ (and you're right that there are good reasons you might not want to), use a @classmethod as an alternate constructor: Entity.from_id(id_number, db_connection).

    On the other hand, if that lookup is something that's inherently useful to clients in other cases that have nothing to do with Entity construction, it seems like this has nothing to do with the Entity class (or at least no more than anything else in the same module). So, just make it a free function.

    0 讨论(0)
  • 2020-12-15 20:56

    Here is a decent use case for @staticmethod.

    I have been working on a game as a side project. Part of that game includes rolling dice based on stats, and the possibility of picking up items and effects that impact your character's stats (for better or worse).

    When I roll the dice in my game, I need to basically say... take the base character stats and then add any inventory and effect stats into this grand netted figure.

    You can't take these abstract objects and add them without instructing the program how. I'm not doing anything at the class level or instance level either. I didn't want to define the function in some global module. The last best option was to go with a static method for adding up stats together. It just makes the most sense this way.

    class Stats:
    
        attribs = ['strength', 'speed', 'intellect', 'tenacity']
    
        def __init__(self,
                     strength=0,
                     speed=0,
                     intellect=0,
                     tenacity=0
                     ):
            self.strength = int(strength)
            self.speed = int(speed)
            self.intellect = int(intellect)
            self.tenacity = int(tenacity)
    
        # combine adds stats objects together and returns a single stats object
        @staticmethod
        def combine(*args: 'Stats'):
            assert all(isinstance(arg, Stats) for arg in args)
            return_stats = Stats()
            for stat in Stats.attribs:
                for _ in args:
                    setattr(return_stats, stat,
                            getattr(return_stats, stat) + getattr(_, stat))
            return (return_stats)
    

    Which would make the stat combination calls work like this

    a = Stats(strength=3, intellect=3)
    b = Stats(strength=1, intellect=-1)
    c = Stats(tenacity=5)
    print(Stats.combine(a, b, c).__dict__)
    

    {'strength': 4, 'speed': 0, 'intellect': 2, 'tenacity': 5}

    0 讨论(0)
  • 2020-12-15 21:05

    Your first example makes the most sense to me: Entity.from_id is pretty succinct and clear.

    It avoids the use of data in the next two examples, which does not describe what's being returned; the data is used to construct an Entity. If you wanted to be specific about the data being used to construct the Entity, then you could name your method something like Entity.with_data_for_id or the equivalent function entity_with_data_for_id.

    Using a verb such as find can also be pretty confusing, as it doesn't give any indication of the return value — what is the function supposed to do when it's found the data? (Yes, I realize str has a find method; wouldn't it be better named index_of? But then there's also index...) It reminds me of the classic:

    find x

    I always try to think what a name would indicate to someone with (a) no knowledge of the system, and (b) knowledge of other parts of the system — not to say I'm always successful!

    0 讨论(0)
  • 2020-12-15 21:07

    The answer to the linked question specifically says this:

    A @classmethod is the idiomatic way to do an "alternate constructor"—there are examples all over the stdlib—itertools.chain.from_iterable, datetime.datetime.fromordinal, etc.

    So I don't know how you got the idea that using a classmethod is inherently bad. I actually like the idea of using a classmethod in your specific situation, as it makes following the code and using the api easy.

    The alternative would be to use default constructor arguments like so:

    class Entity(object):
        def __init__(self, id, db_connection, data=None):
            self.id = id
            self.db_connection = db_connection
            if data is None:
                self.data = self.from_id(id, db_connection)
            else:
                self.data = data
    
        def from_id(cls, id_number, db_connection):
            filters = [['id', 'is', id_number]]
            return db_connection.find(filters)
    

    I prefer the classmethod version that you wrote originally however. Especially since data is fairly ambiguous.

    0 讨论(0)
提交回复
热议问题