Python function call with optional Data class parameter causes error

1.8k views Asked by At

I am trying to pass a data class object as a optional parameter to a function, but got stuck. So, here is a simplified code describing the problem. First, I am defining a data class:

@dataclass
class SomeObject:
    A: str

If i make the parameter non-optional, there is no problem:

def printObject0 (Index, AnyObject):
    print(Index, AnyObject.A)

Object1 = SomeObject ("A")

printObject0 (1, Object1)

# gives:
# 1 A

However, if i try to make it an optional parameter *args like this...

def printObject1 (Index, *AnyObject):
    print(Index, AnyObject.A)

Object1 = SomeObject ("A")

printObject1 (1, Object1)
# gives:
# AttributeError: 'tuple' object has no attribute 'A'

...or with **kwargs like this...

def printObject2 (Index, **AnyObject):
    print(Index, AnyObject.A)
    
Object1 = SomeObject ("A")

printObject2 (1, Object1)
# gives:
# TypeError: printObject2() takes 1 positional argument but 2 were given

i get the above-stated errors.

Does anyone have an idea how i can make the object optional?

1

There are 1 answers

1
fhgd On BEST ANSWER

The function argument *AnyObject means the AnyObject is a tuple of all remaining positional arguments. So you can do

from dataclasses import dataclass


@dataclass
class SomeObject:
    A: str

def printObject_1(idx, *objs):
    print(idx, objs[0].A)

printObject_1(1, SomeObject('A'))  # 1 A

Furthermore, the function argument **AnyObject means that AnyObject is a dictionary of all remaining keyword arguments. So you can do:

def printObject_2(idx, **objs):
    print(idx, objs['foo'].A)

printObject_2(1, foo=SomeObject('A'))  # 1 A

BTW: Usually, optional function arguments have default values, like

def printObject_3(idx, obj=SomeObject('A')):
    print(idx, obj.A)


printObject_3(1)  # 1 A
printObject_3(1, SomeObject('ABC'))  # 1 ABC
printObject_3(1, obj=SomeObject('ABC'))  # 1 ABC

Normally, the second function call should be avoided because it is more error-prone.

But since the default argument value is mutable in this case (see below) it is safer to do

def printObject_4(idx, obj=None):
    if obj is None:
        obj = SomeObject('A')
    print(idx, obj.A)

printObject_4(1)  # 1 A
printObject_4(1, obj=SomeObject('ABC'))  # 1 ABC

The problem with printObject_3 is that you can change the default value inside the function:

def printObject_3_buggy(idx, obj=SomeObject('A')):
    print(idx, obj.A)
    obj.A = 321

printObject_3_buggy.__defaults__  # (SomeObject(A='A'),)

printObject_3_buggy(1)  # 1 A

printObject_3_buggy.__defaults__  # (SomeObject(A=312),)

printObject_3_buggy(1)  # 1 321

The reason for that unexpected behavior is that the default value is mutable. Therefore, use the pattern in printObject_4 when you want to use mutable default values or be careful and dont write to the keyword argument.