9

Consider:

class Parent():
    def __init__(self, last_name, eye_color):
        self.last_name = last_name
        self.eye_color = eye_color

    def show_info(self):
        print("Last Name - "+self.last_name)
        print("Eye Color - "+self.eye_color)

billy_cyrus = Parent("Cyrus", "blue")

The above is from the Udacity Python course. I discovered I'm able to call show_info for instance billy_cyrus using either of the following:

billy_cyrus.show_info()
Parent.show_info(billy_cyrus)

I'm curious as to why. Is there a difference between the two methods? If so when would one be used vs. the other? I'm using Python 3.6 if that matters.

8
  • 5
    There's no difference at all. The first one is kind if syntactic sugar for the latter one. That's where the self parameter is from. It can, however, be useful when you want to pass a method around as a parameter (e.g. for map or for a callback) to use either Class.method or instance.method (the latter being a bound method). Commented Oct 13, 2017 at 19:51
  • @tobias_k. That is not strictly, pedantically 100% true. Commented Oct 13, 2017 at 19:53
  • @MadPhysicist That's why it's a comment and not an answer. Feel free to elaborate. Commented Oct 13, 2017 at 19:54
  • I am drafting an answer as we speak :) I think this is an important question to ask. Commented Oct 13, 2017 at 19:54
  • 2
    It's useful to be able to deal with objects without knowing their exact class. Parent.show_info(billy_cyrus) breaks down if someone wants to pass in a Grandparent or a Guardian in place of a Parent. Commented Oct 13, 2017 at 20:14

2 Answers 2

11

In terms of just calling the method, there is no difference most of the time. In terms of how the underlying machinery, works, there is a bit of a difference.

Since show_info is a method, it is a descriptor in the class. That means that when you access it through an instance in which it is not shadowed by another attribute, the . operator calls __get__ on the descriptor to create a bound method for that instance. A bound method is basically a closure that passes in the self parameter for you before any of the other arguments you supply. You can see the binding happen like this:

>>> billy_cyrus.show_info
<bound method Parent.show_info of <__main__.Parent object at 0x7f7598b14be0>>

A different closure is created every time you use the . operator on a class method.

If you access the method through the class object, on the other hand, it does not get bound. The method is a descriptor, which is just a regular attribute of the class:

>>> Parent.show_info
<function __main__.Parent.show_info>

You can simulate the exact behavior of binding a method before calling it by calling its __get__ yourself:

>>> bound_meth = Parent.show_info.__get__(billy_cyrus, type(billy_cyrus))
>>> bound_meth
<bound method Parent.show_info of <__main__.Parent object at 0x7f7598b14be0>>

Again, this will not make any difference to you in 99.99% of cases, since functionally bound_meth() and Parent.bound_meth(billy_cyrus) end up calling the same underlying function object with the same parameters.

Where it matters

There are a couple of places where it matters how you call a class method. One common use case is when you override a method, but want to use the definition provided in the parent class. For example, say I have a class that I made "immutable" by overriding __setattr__. I can still set attributes on the instance, as in the __init__ method shown below:

class Test:
    def __init__(self, a):
        object.__setattr__(self, 'a', a)
    def __setattr__(self, name, value):
        raise ValueError('I am immutable!')

If I tried to do a normal call to __setattr__ in __init__ by doing self.a = a, a ValueError would be raised every time. But by using object.__setattr__, I can bypass this limitation. Alternatively, I could do super().__setattr__('a', a) for the same effect, or self.__dict__['a'] = a for a very similar one.

@Silvio Mayolo's answer has another good example, where you would deliberately want to use the class method as a function that could be applied to many objects.

Another place it matters (although not in terms of calling methods), is when you use other common descriptors like property. Unlike methods, properties are data-descriptors. This means that they define a __set__ method (and optionally __delete__) in addition to __get__. A property creates a virtual attribute whose getter and setter are arbitrarily complex functions instead of just simple assignments. To properly use a property, you have to do it through the instance. For example:

class PropDemo:
    def __init__(self, x=0):
        self.x = x
    @property
    def x(self):
        return self.__dict__['x']
    @x.setter
    def x(self, value):
        if value < 0:
            raise ValueError('Not negatives, please!')
        self.__dict__['x'] = value

Now you can do something like

>>> inst = PropDemo()
>>> inst.x
0
>>> inst.x = 3
>>> inst.x
3

If you try to access the property through the class, you can get the underlying descriptor object since it will be an unbound attribute:

>>> PropDemo.x
<property at 0x7f7598af00e8>

On a side note, hiding attributes with the same name as a property in __dict__ is a neat trick that works because data descriptors in a class __dict__ trump entries in the instance __dict__, even though instance __dict__ entries trump non-data-descriptors in a class.

Where it can Get Weird

You can override a class method with an instance method in Python. That would mean that type(foo).bar(foo) and foo.bar() don't call the same underlying function at all. This is irrelevant for magic methods because they always use the former invocation, but it can make a big difference for normal method calls.

There are a few ways to override a method on an instance. The one I find most intuitive is to set the instance attribute to a bound method. Here is an example of a modified billy_cyrus, assuming the definition of Parent in the original question:

def alt_show_info(self):
    print('Another version of', self)

billy_cyrus.show_info = alt_show_info.__get__(billy_cyrus, Parent)

In this case, calling the method on the instance vs the class would have completely different results. This only works because methods are non-data descriptors by the way. If they were data descriptors (with a __set__ method), the assignment billy_cyrus.show_info = alt_show_info.__get__(billy_cyrus, Parent) would not override anything but would instead just redirect to __set__, and manually setting it in b billy_cyrus's __dict__ would just get it ignored, as happens with a property.

Additional Resources

Here are a couple of resources on descriptors:

Sign up to request clarification or add additional context in comments.

Comments

6

There is no semantic difference between the two. It's entirely a matter of style. You would generally use billy_cyrus.show_info() in normal use, but the fact that the second approach is allowed permits you to use Parent.show_info to get the method as a first-class object itself. If that was not allowed, then it would not be possible (or at least, it would be fairly difficult) to do something like this.

function = Parent.show_info
so_many_billy_cyrus = [billy_cyrus, billy_cyrus, billy_cyrus]
map(function, so_many_billy_cyrus)

8 Comments

If by "fairly difficult" you mean "use lambda b: b.show_info()"... but, yes, that's one case where that style could be preferred.
I'm curious - if you were intending to use show_info like that wouldn't you define it outside of the class as a standalone function and not as an instance attribute?
@wwii It is possible that you do not have control over the source code of show_info. I frequently work with codebases written by people who are significantly invested in the object-oriented ideas of Java, so every API call ends up, for better or worse, being a method.
@MadPhysicist I'm sure you meant "in Python 2, map is equivalent to a list comprehension". You know, being strictly pedantically 100% correct...;-)
In Python3, replace [] with () to get the equivalent generator expression. My mistake.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.