Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

calling a function saved in a class attribute: different behavior with built-in function vs. normal function

With the following script:

import time

class Foo(object):
    func = time.gmtime
    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())

I get the following output:

1970

However, if I make a slight modification and wrap time.gmtime:

import time

def tmp(stamp):
    return time.gmtime(stamp)

class Foo(object):
    func = tmp
    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())

then I get the following error:

Traceback (most recent call last):
  File "./test.py", line 13, in <module>
    print time.strftime('%Y', Foo().go())
  File "./test.py", line 11, in go
    return self.func(0.0)
TypeError: tmp() takes exactly 1 argument (2 given)

Obviously it's trying to call Foo.func as if it was an instance method and passing self as the first argument.

Two questions:

  • Both time.gmtime and tmp are functions that take a single argument, so why do the two scripts behave differently?
  • How can I safely save an ordinary function in a class/instance attribute and call it later from an instance method?
like image 428
Richard Hansen Avatar asked Jan 18 '15 05:01

Richard Hansen


People also ask

What is the difference between class attributes and instance of attributes?

Differences Between Class and Instance Attributes The difference is that class attributes are shared by all instances. When you change the value of a class attribute, it will affect all instances that share the same exact value. The attribute of an instance on the other hand is unique to that instance.

What is the difference between attribute and function?

A variable stored in an instance or class is called an attribute. A function stored in an instance or class is called a method.

What is the difference between a class variable and an attribute?

According to this web-page, class attributes are variables owned by the class itself. The web page says tagDataMap is a class attribute. But according to Tutorialspoint.com, "class variable is a variable that is shared by all instances of a class.

What is the advantage of working with the class attribute?

Advantages of class attributes: All instances of the class inherit them from the class. They store data that is relevant to all the instances. For example, we could have a counter class attribute that increments every time we create a new instance and decrements every time we delete an instance.


1 Answers

  1. Both time.gmtime and tmp are functions that take a single argument, so why do the two scripts behave differently?

Let us understand what Python does when you do

self.func

from the documentation,

When an instance attribute is referenced that isn’t a data attribute, its class is searched. If the name denotes a valid class attribute that is a function object, a method object is created by packing (pointers to) the instance object and the function object just found together in an abstract object: this is the method object. When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list.

So, if func is a valid function object in self, then a bound method object will be created, which expects the first argument to be the object on which this function is invoked on.

Let us compare now,

print type(time.gmtime)

class Foo(object):
    func = time.gmtime
    def go(self):
        print type(self.func)
        return self.func(0.0)

When go is invoked with Foo().go(), it will print

<type 'builtin_function_or_method'>
<type 'builtin_function_or_method'>

That's right. Since time.gmtime is a built-in function (which is different from the function object), there is no bound method object created here. Lets try your second example now,

def tmp(stamp):
    return time.gmtime(stamp)

print type(tmp), tmp

class Foo(object):
    func = tmp
    def go(self):
        print type(self.func), self.func
        return self.func(0.0)

would print

<type 'function'> <function tmp at 0x7f34d9983578>
<type 'instancemethod'> <bound method Foo.tmp of <__main__.Foo object at ...>>

Since tmp is a function object, according to the documentation shown above, a bound method object is created and it expects an Object of Foo to be the first parameter. So, tmp is actually bound to Foo as an instancemethod. When you invoke go like Foo().go(), it internally calls tmp like this

Foo.func(self, 0.0)

which is effectively

tmp(self, 0.0)

That is why you are getting the following error

TypeError: tmp() takes exactly 1 argument (2 given)
  1. How can I safely save an ordinary function in a class/instance attribute and call it later from an instance method?

Solution 1:

Quoting Python documentation again,

It is also important to note that user-defined functions which are attributes of a class instance are not converted to bound methods; this only happens when the function is an attribute of the class.

It means that, when you assign a user defined function to a class variable, the bound method construction will happen. But, if you assign it to an instance then it will not happen.

So, you can use this to your advantage, like this

import time

def tmp(stamp):
    return time.gmtime(stamp)

class Foo(object):
    def __init__(self):
        self.func = tmp       # Instance variable, not a class variable

    def go(self):
        return self.func(0.0)

print time.strftime('%Y', Foo().go())

Here, self.func will translate to tmp(0.0), as there is no bound method construction happening.

Solution 2:

Use staticmethod function like this

class Foo(object):
    func = staticmethod(tmp)

    def go(self):
        # `return Foo.func(0.0)` This will also work
        return self.func(0.0)

Now, self.func would still refer tmp. It is similar to defining your class like this

class Foo(object):

    @staticmethod
    def func(stamp):
        return time.gmtime(stamp)

    def go(self):
        # `return Foo.func(0.0)` This will also work
        return self.func(0.0)
like image 169
thefourtheye Avatar answered Oct 24 '22 14:10

thefourtheye