-1

Overview

I have a python class inheritance structure in which most methods are defined in the base class and most attributes on which those methods rely are defined in child classes.

The base class looks roughly like this:

class Base(object):

    __metaclass__ = ABCMeta

    @abstractproperty
    def property1(self):
        pass

    @abstractproperty
    def property2(self):
        pass

    def method1(self):
        print(self.property1)

    def method2(self, val):
        return self.property2(val)

while the child class looks like this:

class Child(Base):
    property1 = 'text'
    property2 = function

where function is a function that looks like this:

def function(val):
    return val + 1

Obviously the code above is missing details, but the structure mirrors that of my real code.

The Problem

When I attempt to use method1 in the base class everything works as expected:

>>> child = Child()
>>> child.method1()
'text'

However, attempting the same for method2 spits an error:

>>> child = Child()
>>> child.method2(1)  # expected 2
TypeError: method2() takes exactly 1 argument (2 given)

The second passed argument is the Child class itself.

I'm wondering if there's a way to avoid passing this second Child parameter when calling method2.

Attempts

One workaround I've found is to define an abstract method in the base class then build that function in the child classes like so:

class Base(object):

    __metaclass__ = ABCMeta

    @abstractproperty
    def property1(self):
        pass

    @abstractmethod
    def method2(self, val):
        pass

    def method1(self):
        print(self.property1)
class Child(Base):

    property1 = 'text'

    def method2(self, val):
        return function(val)

However, I would prefer that this method live in the base class. Any thoughts? Thanks in advance!

Kyle
  • 35
  • 3
  • 1
    If you are assigning `function` to `property2`, it has to take 2 arguments, because its `__get__` method will return an instance method when you call `self.property2(...)`, despite it not being defined in the class. – chepner Apr 01 '19 at 19:56

2 Answers2

0

Methods implicitly receive self as the first argument, even if it seems that it is not passed. For example:

class C:
    def f(self, x):
        print(x)

C.f takes two arguments, but you'd normally call it with just one:

c = C()
c.f(1)

The way it is done is that when you access c.f a "bound" method is created which implicitly takes c as the first argument.

The same happens if you assign an external function to a class and use it as a method, as you did.

Solution 1

The usual way to implement a method in a child class is to do it explicitly there, rather than in an external function, so rather than what you did, I would do:

class Child(Base):
    property1 = 'text'

    # instead of: property2 = function
    def property2(self, val):
        return val + 1

Solution 2

If you really want to have property2 = function in the class (can't see why) and function out of the class, then you have to take care of self:

class Child(Base):
    property1 = 'text'
    property2 = function

def function(self, val):
    return val + 1

Solution 3

If you want the previous solution, but without self in function:

class Child(Base):
    property1 = 'text'

    def property2(self, val):
        return function(val)

def function(val):
    return val + 1
zvone
  • 18,045
  • 3
  • 49
  • 77
  • What about defining property2 as `staticmethod` with `property2 = staticmethod(function)`? – dudenr33 Apr 01 '19 at 20:54
  • Thanks @zvone for the response. All of those solutions would work for this purpose but I think @dudenr33 's use of `staticmethod()` requires the least code to implement. Unless anyone sees something obviously wrong with that implementation, I think I'll roll with that – Kyle Apr 01 '19 at 21:28
  • Glad to hear that it works for you. I wrote a little bit more detailed answer with explanation of how and why it works. – dudenr33 Apr 02 '19 at 18:20
0

Solution

Make your method static:

class Child(Base)
    property2 = staticmethod(function)

Explanation

As zvone already explained, bound methods implicitly receive self as the first parameter.
To create a bound method you don't necessarily need to define it in the class body.
This:

def foo(self):
    print("foo")

class Foo:
    bar = foo

f = Foo()
print(f.bar)

will output:

>>> <bound method foo of <__main__.Foo object at 0x014EC790>>

A function assigned to a class attribute will therefore behave just as a normal class method, meaning that if you call it as f.bar() it is treated as a bound method and self is implicitly passed as first parameter.

To control what is and what is not implicitly passed to a class method as first argument is normally controlled with the decorators

  • @classmethod: the class itself is passed as the first argument
  • @staticmethod: no arguments are implicitly passed to the method

So you want the behavior of a staticmethod, but since you are simply assigning a already defined function to a class attribute you cannot use the decorator syntax.
But since decorators are just normal functions which take a function as parameter and return a wrapped function, this:

class Child(Base):
    property2 = staticmethod(function)

is equivalent (*) to this:

class Child(Base):
    @staticmethod
    def property2():
        function()

Further improvements

I would suggest a small additional modification to your Base class:
Rename property2 and mark it not as abstractproperty but as abstractstaticmethod(**). This will help colleagues (and eventually yourself) to understand better what kind of implementation is expected in the child class.

class Base(object):

    __metaclass__ = ABCMeta

    @abstractstaticmethod
    def staticmethod1(self):
        pass

(*) well, more or less. The former actually assigns function to property2, the latter creates a new static method which delegates to function.

(**) abstractstaticmethod is deprecated since Python 3.3, but since you are also using abstractproperty I wanted to be consistent.

dudenr33
  • 1,119
  • 1
  • 10
  • 26