Reputation: 377
I was trying some examples on class inheritance and stumbled, on how to access the methods. In tutorials, the proper form is always Class().method(), however apparently, that's not the only solution.
class A():
def open(self):
print("class A method open")
class B():
def open(self):
print("class B method open")
def close(self):
print("class B method close")
class D(A, B):
def end_reached(self):
print("end reached")
## Why do both of the following lines work & produce the same output?
D().open()
D.open(1)
I expected the last line to give an error, however, the output is the same as from the line above. It did give an error on missing parameter, if the method gets called like D.open(), but giving it just any parameter works.
Is there any difference between the two lines?
Upvotes: 0
Views: 107
Reputation: 530960
The difference lays in the descriptor protocol, specifically in how descriptors provide Python's object-oriented features.
open
is a class attribute of various classes; in each case, though, it has the type function
. That type implements the descriptor protocol, via the definition of function.__get__
, by returning either a function
object or a method
object, depending on whether open
is accessed via the class itself or an instance of the class.
Given
d = D()
the attribute lookup d.open
is equivalent to
type(d).__dict__['open'].__get__(d, D)
which returns a method
which wraps the original function. When called, the method passes a reference to d
and any of its own arguments to the wrapped function.
f = d.open # The return value of __get__
f() # open(d)
The attribute lookup D.open
is equivalent to
D.__dict__['open'].__get__(None, D)
which returns the function open
itself.
f = D.open # The return value of __get__
f(1) # open(1)
Upvotes: 1
Reputation: 5459
That's not an inheritance problem.
As you can see in your method declaration, first argument is self
.
D()
just creates an object of class D
. If you do anything on the object, that object is automatically passed as first argument.
However, when doing D.open(1)
you don't invoke the method on the object, so the first argument in the method is first argument passed to it == your code assumes self=1
. Because your methods behave like static methods (you don't use self
inside them), you don't access self
's inside, so the code doesn't break.
But if your open
method invoked anything from self (another method or a field),
then you would get an attribute error - because int
object probably doesn't have a method like that.
Examples from build-in classes:
"aaa".split()
str.split("aaa")
They both behave the same.
"aaa"
is of type str
, so interpreted looks into class str
for the method and automatically passes "aaa"
as self. (Just like your D()
is of type D
. And d=D()
, then d.open()
will still know the type.)
The other one shows which class to ask for the method (str
) but still needs an argument self
. The way we define methods shows that "self" is really an argument as well, so we can just pass it.
Upvotes: 0
Reputation: 1
class A():
def open(self):
print(self)
print("class A method open")
class B():
def open(self):
print("class B method open")
def close(self):
print("class B method close")
class D(A, B):
def end_reached(self):
print("end reached")
## Why do both of the following lines work & produce the same output?
D().open() # Okay
D.open(1) # BAD
output
<__main__.D object at 0x0045FE90>
class A method open
1
class A method ope
You might want to look at a static method
class A():
@staticmethod
def open():
print("open ran")
class D(A):
def end_reached(self):
print("end reached")
D.open() # Good
output
open ran
Upvotes: 0
Reputation: 91017
In order to learn about this, I have to go in depth about the properties of functions and methods.
Accessing a class field of a class via its instances checks if there are __set__
and/or __get__
methods. If they are, the call happens via __get__
.
Functions work this way: if a function is a class field of a class (in short, methods), they are called via function.__get__(object)(*args, **kwargs)
.
And here, we have a difference between Python 2 and Python 3:
Python 2 unconditionally prepends the given object
to the argumends and calls the underlying function with function(object, *args, **kwargs)
. object
gets assigned to self
then, which by convention is the name of the first parameter of a method.
Python 3 checks if the given object
is an instance of the class as it should be. If it isn't, we get an exception. If it is, the call gets processed as above.
Example:
def outer_f(self):
return "f called with", self
class C(object):
f = outer_f
def g(self):
return "g called with", self
c = C()
print(outer_f(c))
print(C.f(c))
print(C.f.__get__(c)())
print(c.f())
print(C.g(c))
print(C.g.__get__(c)())
print(c.g())
Upvotes: 0