Python: Regular method and static method with same

2019-04-20 16:01发布

问题:

Introduction

I have a Python class, which contains a number of methods. I want one of those methods to have a static counterpart—that is, a static method with the same name—which can handle more arguments. After some searching, I have found that I can use the @staticmethod decorator to create a static method.

Problem

For convenience, I have created a reduced test case which reproduces the issue:

class myclass:

    @staticmethod
    def foo():
        return 'static method'

    def foo(self):
        return 'public method'

obj = myclass()
print(obj.foo())
print(myclass.foo())

I expect that the code above will print the following:

public method
static method

However, the code prints the following:

public method
Traceback (most recent call last):
  File "sandbox.py", line 14, in <module>
    print(myclass.foo())
TypeError: foo() missing 1 required positional argument: 'self'

From this, I can only assume that calling myclass.foo() tries to call its non-static counterpart with no arguments (which won't work because non-static methods always accept the argument self). This behavior baffles me, because I expect any call to the static method to actually call the static method.

I've tested the issue in both Python 2.7 and 3.3, only to receive the same error.

Questions

Why does this happen, and what can I do to fix my code so it prints:

public method
static method

as I would expect?

回答1:

A similar question is here: override methods with same name in python programming

functions are looked up by name, so you are just redefining foo with an instance method. There is no such thing as an overloaded function in Python. You either write a new function with a separate name, or you provide the arguments in such a way that it can handle the logic for both.

In other words, you can't have a static version and an instance version of the same name. If you look at its vars you'll see one foo.

In [1]: class Test:
   ...:     @staticmethod
   ...:     def foo():
   ...:         print 'static'
   ...:     def foo(self):
   ...:         print 'instance'
   ...:         

In [2]: t = Test()

In [3]: t.foo()
instance

In [6]: vars(Test)
Out[6]: {'__doc__': None, '__module__': '__main__', 'foo': <function __main__.foo>}


回答2:

Because attribute lookup in Python is something within the programmer's control, this sort of thing is technically possible. If you put any value into writing code in a "pythonic" way (using the preferred conventions and idioms of the python community), it is very likely the wrong way to frame a problem / design. But if you know how descriptors can allow you to control attribute lookup, and how functions become bound functions (hint: functions are descriptors), you can accomplish code that is roughly what you want.

For a given name, there is only one object that will be looked up on a class, regardless of whether you are looking the name up on an instance of the class, or the class itself. Thus, the thing that you're looking up has to deal with the two cases, and dispatch appropriately.

(Note: this isn't exactly true; if an instance has a name in its attribute namespace that collides with one in the namespace of its class, the value on the instance will win in some circumstances. But even in those circumstances, it won't become a "bound method" in the way that you probably would wish it to.)

I don't recommend designing your program using a technique such as this, but the following will do roughly what you asked. Understanding how this works requires a relatively deep understanding of python as a language.

class StaticOrInstanceDescriptor(object):

    def __get__(self, cls, inst):
        if cls is None:
            return self.instance.__get__(self)
        else:
            return self.static

    def __init__(self, static):
        self.static = static

    def instance(self, instance):
        self.instance = instance
        return self


class MyClass(object):

    @StaticOrInstanceDescriptor
    def foo():
        return 'static method'

    @foo.instance
    def foo(self):
        return 'public method'

obj = MyClass()
print(obj.foo())
print(MyClass.foo())

which does print out:

% python /tmp/sandbox.py
static method
public method


回答3:

While it's not strictly possible to do, as rightly pointed out, you could always "fake" it by redefining the method on instantiation, like this:

class YourClass(object):

    def __init__(self):
        self.foo = self._instance_foo

    @staticmethod
    def foo()
        print "Static!"

    def _instance_foo(self):
        print "Instance!"

which would produce the desired result:

>>> YourClass.foo()
Static!
>>> your_instance = YourClass()
>>> your_instance.foo()
Instance!


回答4:

Ended up here from google so thought I would post my solution to this "problem"...

class Test():
    def test_method(self=None):
        if self is None:
            print("static bit")
        else:
            print("instance bit")

This way you can use the method like a static method or like an instance method.



回答5:

I'm really ignorant in Python when it comes to these types of questions but I agree with a number of the sentiments in the answers so far that it's not native and you should not try.

So, now for something completely different. I just mocked up the following for a similar problem I have:

class Foo(object):
    def __init__(self, name):
        self.name = name

    @staticmethod
    def get_norm_name(name):
        # Normalize name
        return name.strip().lower()

    def norm_name(self):
        return Foo.get_norm_name(self.name)

>>> Foo.get_norm_name('Robert')
'robert'

>>> f = Foo('Robert')
>>> f.norm_name()
'robert'

The method names are different, which you were trying to avoid, but I think they're close enough. Again, not sure if this is Pythonic, but it seems easy to understand.