naughty_waves
naughty_waves

Reputation: 275

How to use conditionals to define a class?

I want to generate a class in which the number of input elements is determined by a conditional that is either True or False. I tried something like this

class Test:
    def __init__(self, cond):
        self.cond = cond
        if cond == True:
            def __call__(self, a, b, c):
                d1 = a + b + c
                return d1
        elif cond == False:
            def __call__(self, a, b):
                d2 = a + b
                return d2

result1 = Test(cond=True)(a, b, c)
result2 = Test(cond=False)(a, b)

but it does obviously not work, and it provides the following error:

TypeError: 'Test' object is not callable

I suspect that I am using the wrong kind of def, as __init__ is probably not suitable in this case.

I know it would be fairly easy using a def function and not a class.

Upvotes: 0

Views: 1185

Answers (7)

martineau
martineau

Reputation: 123531

Your code doesn't work because __call__() is a function local to the __init__() method, so defining it there doesn't affect the class' definition. I don't know how "pythonic" it is, but the following seems like the simplest way to do what you want—essentially making __init__() act like a class decorator.

class Test:
    def __init__(self, cond):
        self.cond = cond
        if cond:
            def __call__(self, a, b, c):
                d1 = a + b + c
                return d1
        else:
            def __call__(self, a, b):
                d2 = a + b
                return d2

        setattr(self.__class__, '__call__', __call__)  # Assign to class.


if __name__ == '__main__':

    a, b, c = 10, 30, 2
    result1 = Test(cond=True)(a, b, c)
    result2 = Test(cond=False)(a, b)
    print(f'{result1}, {result2}')  # -> 42, 40

Upvotes: 1

Gnudiff
Gnudiff

Reputation: 4305

While I would agree that perhaps other ways are better, if you remember that functions are first class objects in Python, then the most direct way of doing as close to what you have already done, is to redefine __call__ this way:

class Test:
    def method1(self, a, b, c):
        d1 = a + b + c
        return d1
    def method2(self, a, b):
        d2 = a + b
        return d2

    def __init__(self, cond):
        self.cond = cond
        if cond:
            self.__call__=self.method1
        else:
            self.__call__=self.method2


Test(cond=True)(1, 2, 3)
6
Test(cond=False)(1, 2)
3

Upvotes: 1

sleblanc
sleblanc

Reputation: 3931

Without going into metaclass stuff, the simplest way to implement that is to define __call__ as accepting a variable quantity of arguments instead.

class Test:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, *args):
        if self.cond:
            if len(args) != 3:
                raise ValueError('When cond=True, requires 3 arguments')
        else:
            if len(args) != 2:
                raise ValueError('When cond=False, requires 2 arguments')

        return sum(args)

Upvotes: 2

theberzi
theberzi

Reputation: 2695

In this case you can achieve the same result more pythonically by processing the arguments as a collection of arguments (thus skipping the initial configuration with your cond, since Python doesn't force you to have a fixed number of them), and either:

  1. Using a list as parameter:
class Test:
    def __call__(self, args):
        return sum(args)

my_test = Test()
print(my_test([1, 2, 3]))
# output: 6
  1. Use "varargs" to accept an arbitrary number of positional parameters:
class Test:
    def __call__(self, *args):
        return sum(args)

my_test = Test()
print(my_test(1, 2, 3))
# output: 6

Note that *args is effectively a list itself, the main difference is in how the callable is called.

Both of these work with any number of arguments. If you have specific behaviour corresponding to a specific amount of arguments, you can check the length of args as you would any list, and access its elements in the same way, for example with args[0].

If the initial configuration is required, you can enforce a certain behaviour while still processing the arguments flexibly. For example, the following version of the class rejects input lengths different than 2 if cond is True, and inputs different than 3 if it's False, all with the same method:

class Test:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, *args):
        if (self.cond is True and len(args) != 3) \
        or (self.cond is False and len(args) != 2):
            raise ValueError

        return sum(args)

You can of course add a default value to cond in the __init__ if you want to make it possible to omit it when instancing the class.

Upvotes: 2

tripleee
tripleee

Reputation: 189936

A common way to avoid this in the first place is to define a subclass instead.

class Test:
    def __call__(self, a, b, c):
        return a + b + c

class Test2 (Test):
    def __call__(self, a, b):
        return a + b

result1 = Test()(a, b, c)
result2 = Test2()(a, b)

Superficially, this is hardly an improvement; but in situations where you might use a conditional in more than one place, this restructuring can pay itself back handsomely rather quickly.

Upvotes: 3

Austin
Austin

Reputation: 26057

I would restructure your code. It's not a good idea to add conditionals or logics into __init__() which serves only for initializing of variables.

Instead you should make the dunder __call__() separate so it can be called on the class instantiation.

class Test:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, a, b, c=0):
        if self.cond:
            return a + b + c
        else:
            return a + b

a, b, c = 1, 2, 3

result1 = Test(cond=True)(a, b, c)
result2 = Test(cond=False)(a, b)

print(result1)  # 6
print(result2)  # 3

Upvotes: 5

Tomalak
Tomalak

Reputation: 338406

Don't do conditional function definitions.

class Test:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, a, b, c=0):
        if self.cond:
           return a + b + c
        else:
           return a + b

a, b, c = 1, 2, 3

print(Test(cond=True)(a, b, c))
# => 6

print(Test(cond=False)(a, b))
# => 3

Also, don't do comparisons like if cond == True: and elif cond == False:, that's also an antipattern. If cond is supposed to be a Boolean value, if cond: and else: are perfectly fine.

Upvotes: 3

Related Questions