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.

Chris Decker
  • 478
  • 3
  • 11
  • 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). – tobias_k Oct 13 '17 at 19:51
  • @tobias_k. That is not strictly, pedantically 100% true. – Mad Physicist Oct 13 '17 at 19:53
  • @MadPhysicist That's why it's a comment and not an answer. Feel free to elaborate. – tobias_k Oct 13 '17 at 19:54
  • I am drafting an answer as we speak :) I think this is an important question to ask. – Mad Physicist Oct 13 '17 at 19:54
  • For your reading pleasure: [9. Classes](https://docs.python.org/3/tutorial/classes.html) from the Tutorial. – wwii Oct 13 '17 at 19:55
  • 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`. – Charles Duffy Oct 13 '17 at 20:14
  • @CharlesDuffy. You could always do `type(billy_cyrus).show_info(billy_cyrus)`. – Mad Physicist Oct 15 '17 at 15:08
  • @ChrisDecker. You should consider selecting an answer. You get points for it, the question gets marked as answered, the answerer gets points, and it's just the courteous thing to do. – Mad Physicist Oct 16 '17 at 04:58

2 Answers2

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:

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
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)
Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 1
    If by "fairly difficult" you mean "use `lambda b: b.show_info()`"... but, yes, that's one case where that style could be preferred. – tobias_k Oct 13 '17 at 19:56
  • 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 Oct 13 '17 at 20:00
  • @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. – Silvio Mayolo Oct 13 '17 at 20:01
  • In fact, my first ever programming class (which was taught in Python) taught me that standalone functions are poor design and should never be used. Of course, that's a rather peculiar ideology, but nonetheless some people are trained on that rhetoric. – Silvio Mayolo Oct 13 '17 at 20:02
  • In general, `map` is equivalent to a list comprehension, so `[x.show_info() for x in so_many_billy_cyrus]` would work just fine. But that only emphasizes your original point about equivalence, so +1 – Mad Physicist Oct 13 '17 at 20:14
  • 2
    @MadPhysicist I'm sure you meant "_in Python 2_, `map` is equivalent to a list comprehension". You know, being strictly pedantically 100% correct...;-) – tobias_k Oct 13 '17 at 20:17
  • 1
    In Python3, replace `[]` with `()` to get the equivalent generator expression. My mistake. – Mad Physicist Oct 13 '17 at 20:18
  • @wwii. That is something I have actually dealt with quite a bit while doing unholy things with the logging system. I've addressed instance level overrides in my answer. By the time someone gets comfortable doing stuff like that though, they usually won't need it spelled out. That's why I prefer this answer. – Mad Physicist Oct 16 '17 at 05:00