Apparently inconsistent errors when calling functions defined in other modules from within a class

I understand this is the correct way to include a function in a class, and works as desired:

from statsmodels.tsa.seasonal import seasonal_decompose

class Foo3:
    def my_seasonal_decompose(self, *args, **kwargs):
        # from statsmodels.tsa.seasonal import seasonal_decompose
        return seasonal_decompose(*args, **kwargs)

a3 = Foo3()
print(a3.my_seasonal_decompose([1, 2], period=1))

However, this works for sin, and not for seasonal_decompose. Why?

class Foo4:
    from numpy import sin
    from statsmodels.tsa.seasonal import seasonal_decompose


a4 = Foo4()
print(a4.sin(2))  # works

print(type(a4.seasonal_decompose))  # returns <class 'method'>
help(a4.seasonal_decompose)  # works as expected
print(a4.seasonal_decompose([1, 2], period=1))
# TypeError: float() argument must be a string or a number, not 'Foo4'

Answer

Because seasonal_decompose is a function instance and numpy.sin is a ufunc instance. Both are callable and are used pretty much as functions, but only seasonal_decompose is treated as a bona fide Python function. And Python functions are treated special.

The <class 'method'> you’re printing out should not be confused with class methods but that its class is method, more specifically a bound method. Bound methods are how Python wraps function instances to instance or class methods. Maybe a simple example will help:

def outerf(): pass # a true Python function

class Foo:
    f = outerf
    def g(self): pass


print(f)     # <function f at ...>
print(Foo.f) # <function outerf at ...>
print(Foo.g) # <function Foo.g at ...>
print()

x = Foo()
print(x.f) # <bound method outerf of <__main__.Foo object at ...>>
print(x.g) # <bound method Foo.g of <__main__.Foo object at ...>>

See how all of these start out as function instances in the class Foo but end up as bound methods in the Foo instance? When an instance is made, Python checks its class attributes for function instances and makes bound methods, no matter if the function was defined in the class or not.

So why does Python make bound methods? To implement instance method call syntax where the instance is automatically passed to the wrapped function’s first argument.

f()
Foo.f()
Foo.g(x)

x.f() # TypeError: outerf() takes 0 positional arguments but 1 was given
x.g() # same as Foo.g(x)

Well okay, this is convenient and all, but what if you want to opt out? You want your function to stay a function in its instance and its class. Well that’s what static methods are for. Now, you’re not going to use the @staticmethod decorator here because decorators only work on function or class definitions, and your function is defined in another module. But you have the staticmethod builtin:

from statsmodels.tsa.seasonal import seasonal_decompose

class Foo4:
    my_seasonal_decompose = staticmethod(seasonal_decompose)

print(seasonal_decompose)         # <function seasonal_decompose at ...>
print(Foo4.my_seasonal_decompose) # <function seasonal_decompose at ...>
x4 = Foo4()
print(x4.my_seasonal_decompose)   # <function seasonal_decompose at ...>