Are there more than three types of methods in Python?
Yes. There are the three built-in kinds that you mention (instance method, class method, static method), four if you count @property
, and anyone can define new method types.
Once you understand the mechanism for doing this, it's easy to explain why unknown_mthd
is callable from the class in Python 3.
A new kind of method
Suppose we wanted to create a new type of method, call it optionalselfmethod
so that we could do something like this:
class Test(object):
@optionalselfmethod
def optionalself_mthd(self, *args):
print('Optional-Self Method:', self, *args)
The usage is like this:
In [3]: Test.optionalself_mthd(1, 2)
Optional-Self Method: None 1 2
In [4]: x = Test()
In [5]: x.optionalself_mthd(1, 2)
Optional-Self Method: <test.Test object at 0x7fe80049d748> 1 2
In [6]: Test.instance_mthd(1, 2)
Instance method: 1 2
optionalselfmethod
works like a normal instance method when called on an instance, but when called on the class, it always receives None
for the first parameter. If it were a normal instance method, you would always have to pass an explicit value for the self
parameter in order for it to work.
So how does this work? How you can you create a new method type like this?
The Descriptor Protocol
When Python looks up a field of an instance, i.e. when you do x.whatever
, it check in several places. It checks the instance's __dict__
of course, but it also checks the __dict__
of the object's class, and base classes thereof. In the instance dict, Python is just looking for the value, so if x.__dict__['whatever']
exists, that's the value. However, in the class dict, Python is looking for an object which implements the Descriptor Protocol.
The Descriptor Protocol is how all three built-in kinds of methods work, it's how @property
works, and it's how our special optionalselfmethod
will work.
Basically, if the class dict has a value with the correct name1, Python checks if it has an __get__
method, and calls it like type(x).whatever.__get__(x, type(x))
Then, the value returned from __get__
is used as the field value.
So for example, a trivial descriptor which always returns 3:
class GetExample:
def __get__(self, instance, cls):
print("__get__", instance, cls)
return 3
class Test:
get_test = GetExample()
Usage is like this:
In[22]: x = Test()
In[23]: x.get_test
__get__ <__main__.Test object at 0x7fe8003fc470> <class '__main__.Test'>
Out[23]: 3
Notice that the descriptor is called with both the instance and the class type. It can also be used on the class:
In [29]: Test.get_test
__get__ None <class '__main__.Test'>
Out[29]: 3
When a descriptor is used on a class rather than an instance, the __get__
method gets None for self, but still gets the class argument.
This allows a simple implementation of methods: functions simply implement the descriptor protocol. When you call __get__
on a function, it returns a bound method of instance. If the instance is None
, it returns the original function. You can actually call __get__
yourself to see this:
In [30]: x = object()
In [31]: def test(self, *args):
...: print(f'Not really a method: self<{self}>, args: {args}')
...:
In [32]: test
Out[32]: <function __main__.test>
In [33]: test.__get__(None, object)
Out[33]: <function __main__.test>
In [34]: test.__get__(x, object)
Out[34]: <bound method test of <object object at 0x7fe7ff92d890>>
@classmethod
and @staticmethod
are similar. These decorators create proxy objects with __get__
methods which provide different binding. Class method's __get__
binds the method to the instance, and static method's __get__
doesn't bind to anything, even when called on an instance.
The Optional-Self Method Implementation
We can do something similar to create a new method which optionally binds to an instance.
import functools
class optionalselfmethod:
def __init__(self, function):
self.function = function
functools.update_wrapper(self, function)
def __get__(self, instance, cls):
return boundoptionalselfmethod(self.function, instance)
class boundoptionalselfmethod:
def __init__(self, function, instance):
self.function = function
self.instance = instance
functools.update_wrapper(self, function)
def __call__(self, *args, **kwargs):
return self.function(self.instance, *args, **kwargs)
def __repr__(self):
return f'<bound optionalselfmethod {self.__name__} of {self.instance}>'
When you decorate a function with optionalselfmethod
, the function is replaced with our proxy. This proxy saves the original method and supplies a __get__
method which returns a boudnoptionalselfmethod
. When we create a boundoptionalselfmethod
, we tell it both the function to call and the value to pass as self
. Finally, calling the boundoptionalselfmethod
calls the original function, but with the instance or None
inserted into the first parameter.
Specific Questions
Was making a method this way (without args while not explicitly
decorated as staticmethods) intentional in Python 3's design? UPDATED
I believe this was intentional; however the intent would have been to eliminate unbound methods. In both Python 2 and Python 3, def
always creates a function (you can see this by checking a type's __dict__
: even though Test.instance_mthd
comes back as <unbound method Test.instance_mthd>
, Test.__dict__['instance_mthd']
is still <function instance_mthd at 0x...>
).
In Python 2, function
's __get__
method always returns a instancemethod
, even when accessed through the class. When accessed through an instance, the method is bound to that instance. When accessed through the class, the method is unbound, and includes a mechanism which checks that the first argument is an instance of the correct class.
In Python 3, function
's __get__
method will return the original function unchanged when accessed through the class, and a method
when accessed through the instance.
I don't know the exact rationale but I would guess that type-checking of the first argument to a class-level function was deemed unnecessary, maybe even harmful; Python allows duck-typing after all.
Among the classic method types, what type of method is unknown_mthd?
unknown_mthd
is a plain function, just like any normal instance method. It only fails when called through the instance because when method.__call__
attempts to call the function
unknown_mthd
with the bound instance, it doesn't accept enough parameters to receive the instance
argument.
Why can unknown_mthd be called by the class without passing an
argument?
Because it's just a plain function
, same as any other function
. I just doesn't take enough arguments to work correctly when used as an instance method.
You may note that both classmethod
and staticmethod
work the same whether they're called through an instance or a class, whereas unknown_mthd
will only work correctly when when called through the class and fail when called through an instance.
1. If a particular name has both a value in the instance dict and a descriptor in the class dict, which one is used depends on what kind of descriptor it is. If the descriptor only defines __get__
, the value in the instance dict is used. If the descriptor also defines __set__
, then it's a data-descriptor and the descriptor always wins. This is why you can assign over top of a method but not a @property
; method only define __get__
, so you can put things in the same-named slot in the instance dict, while @properties
define __set__
, so even if they're read-only, you'll never get a value from the instance __dict__
even if you've previously bypassed property lookup and stuck a value in the dict with e.g. x.__dict__['whatever'] = 3
.