3

I saw a code snippet in Python 3.6.5 that can be replicated with this simplified example below and I do not understand if this is something concerning or not. I am surprised it works honestly...

class Foo:

    def bar(numb):
        return numb

    A1 = bar(1)


print(Foo)
print(Foo.A1)
print(Foo.bar(17))

In all python guides that I have seen, self appears as the first argument for all the purposes we know and love. When it is not, the methods are decorated with a static decorator and all is well. This case works as it is, however. If I were to use the static decorator on bar, I get a TypeError when setting A1:

Traceback (most recent call last):
  File "/home/user/dir/understanding_classes.py", line 1, in <module>
    class Foo:
  File "/home/user/dir/understanding_classes.py", line 7, in Foo
    A1 = bar(1)
TypeError: 'staticmethod' object is not callable

Is this something that is OK keeping in the code or is this a potential problem? I hope the question is not too broad, but how and why does this work?

OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
J.K.
  • 1,574
  • 1
  • 13
  • 21
  • I think you can read about the differences between programming with oop and functions. Regards. – Luis Alejandro Vargas Ramos Sep 01 '22 at 17:32
  • it would also work if you named the param as `self` instead, looks like. – rv.kvetch Sep 01 '22 at 17:32
  • 3
    What are you annotating with `@staticmethod` here? – Ryan Haining Sep 01 '22 at 17:33
  • however, I am unable to reproduce this on my machine. If i add static method code runs fine on my end at least. – rv.kvetch Sep 01 '22 at 17:34
  • @RyanHaining, sorry for not being specific. Decorating `bar`. Will updated the comment. – J.K. Sep 01 '22 at 17:37
  • correct, I am able to reproduce this error on version Python `3.7.13`. I think I was trying on Python 3.10 earlier. – rv.kvetch Sep 01 '22 at 17:39
  • 1
    @rv.kvetch, I used Python 3.6.5 – J.K. Sep 01 '22 at 17:40
  • When I try to reproduce, I get the same results with and without `@staticmethod` on Python 3.10.3, at least for the tests you've posted. I do *not* get the TypeError unless I try to access the method from an _instance_ rather than from the _class_ as you have shown. See also https://stackoverflow.com/questions/43587044/do-we-really-need-staticmethod-decorator-in-python-to-declare-static-method – Sarah Messer Sep 01 '22 at 17:47
  • 3
    You never call the function as a *bound instance method*, so it never receives an implicit first instance argument. If you instantiated the class and called the method there, you would… – deceze Sep 01 '22 at 17:48
  • 1
    No, you should not do this (even though you can) – juanpa.arrivillaga Sep 01 '22 at 17:49
  • 1
    @SarahMesser this issue is only reproducible in Python 3.6 and 3.7, if I understand it. I used `pyenv` for switching py versions to test it out. – rv.kvetch Sep 01 '22 at 17:49
  • The reason for defining instance methods this way has to do with the [descriptor protocol](https://docs.python.org/3/howto/descriptor.html). Understand that, and you'll know why you need an additional parameter (called whatever you like; "self" is conventional but not required) for it to work properly. – chepner Sep 01 '22 at 17:52
  • `staticmethod` is just a type that provides a *different* definition of `__get__` than the function type. – chepner Sep 01 '22 at 17:52

1 Answers1

2

The first parameter of the method will be set to the receiver. We call it self by convention, but self isn't a keyword; any valid parameter name would work just as well.

There's two different ways to invoke a method that are relevant here. Let's say we have a simple Person class with a name and a say_hi method

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self):
        print(f'Hi my name is {self.name}')


p = Person('J.K.')

If we call the method on p, we'll get a call to say_hi with self=p

p.say_hi() # self=p, prints 'Hi my name is J.K.'

What you're doing in your example is calling the method via the class, and passing that first argument explicitly. The equivalent call here would be

Person.say_hi(p) # explicit self=p, also prints 'Hi my name is J.K.'

In your example you're using a non-static method then calling it through the class, then explicitly passing the first parameter. It happens to work but it doesn't make a lot of sense because you should be able to invoke a non-static method by saying

f = Foo()
f.bar() # numb = f, works, but numb isn't a number it's a Foo

If you want to put a function inside of a class that doesn't have a receiver, that's when you want to use @staticmethod (or, @classmethod more often)

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self):
        print(f'Hi my name is {self.name}')

    @staticmethod
    def say_hello():
        print('hello')


p = Person('J.K.')

Person.say_hello()
p.say_hello()
Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
  • Hope not to bother, but could you elaborate about what is happening when I set `A1`? Is this something that is a sensible thing to do or should `bar` simply be a separate method? – J.K. Sep 01 '22 at 18:24
  • 1
    `A1` is defined at the class-level, so regardless of how many `Foo`s you create, you always just have a single `A1`. There are legitimate reasons to do this, it's hard to guess without more information, but if your intention is that each `Foo` instance has it's own `A1`, then you should change your code to set `self.A1` in `__init__` like I did with the `Person`'s `self.name` – Ryan Haining Sep 01 '22 at 18:49
  • Sorry, I see I was not specific with my question. I am more interested if it is OK that `A1` is created by calling `bar`, which is in turn a method of questionable utility, i.e. `A1 = bar(1)`. Thank you kindly for your patience and your time. – J.K. Sep 01 '22 at 18:53
  • 1
    @J.K. it would make sense if `bar` were a `@staticmethod`, which you should be able to add without a problem. I don't know what you're doing that's giving you that error. I was able to add `@staticmethod` without a problem (like others are saying). – Ryan Haining Sep 01 '22 at 18:55