How can a group of parameters be required if at least one of them is informed?

Asked

Viewed 461 times

6

In Python we have the default values, which are optional when we call a function. I was wondering if there’s any way to make a group of values optional, like if you pass all those values or none.

def func(nome=None, cpf=None):
    pass

If you just pass the name or Cpf would give an error, it would only work if you pass both or none. I don’t know if my question made sense, I hope so.

4 answers

3

TL;DR
To specifically answer the question, go straight to the topic "Specifically answering the question."

In Python we can call functions, sending "positional" and/or "named" arguments, to understand this we see a function that takes 3 arguments and only returns them in a tuple:

def f1(arg1, arg2, arg3):
    return (arg1, arg2, arg3)

As these arguments have no values assigned (default values), they are considered mandatory arguments, so to call this function we have to pass the 3 arguments obligatorily, but we have two ways to do this:

Calling the arguments positionally:

>>> f1(1,2,3)
(1,2,3)

Or by name:

>>> f1(arg2=2, arg1=3, arg3=1)
(3,2,1)

Note that despite the mandatory sending of the 3 arguments the order of the arguments need not be the same when we call the function with the named form.

We can also make a mix, calling part positionally and part nominally, provided that the positional form anticipates the positioned form:

# Ok
>>> f1(1, 2, arg3=99) 
(1, 2, 99)

# Error
>>> f1(1, arg2=2, 3)
SyntaxError: positional argument follows keyword argument

It is important to note that we must always send the 3 arguments, without repeating them, calls as the examples below will raise error exception:

>>> f1(1, 2, arg1=10)
TypeError: f1() got multiple values for argument 'arg1'

>>> f1(arg1=1, arg3=3)
f1() missing 1 required positional argument: 'arg2'

>>> f1(1,2)
TypeError: f1() missing 1 required positional argument: 'arg3'

Call the functions sending arguments positionally or named, which is best?

Most programmers use positional form, but the named form makes calls to functions clearer, more flexible and explicit, more in accordance with the python zen, let’s see that code:

def save_txt(out_file, contents):
    with open(out_file,'w+') as f:
        for line in contents:
             f.write(line)

This function takes as a parameter out_file and contents takes lines of text and writes to the output file, which of the calls below are clearer and more flexible?

lines = '''
        Por que usar argumentos nomeados?
        Porque é mais claro e explícito
        '''
file_name = 'lines.txt' 

# Chamada com argumentos de forma posicional
save_txt(file_name, lines)

# Chamadas com argumentos na forma nomeada:
save_txt(out_file=file_name, contents=lines)

# Segunda opcao para chamada nomeada 
save_txt(contents=lines, out_file=file_name) 

Arguments "default":

Python also allows the arguments of a function to have values assigned by default (Default), these values are assigned in the function definition:

def f1(arg1, arg2, arg3=99):
    return (arg1, arg2, arg3)

It is now mandatory to send the first 2 parameters in the call to the function, but the last one is optional, the call requires the same rules for the previous calls:

Valid:

>>> f1(1,2)
(1, 2, 99)

>>> f1(1,2,arg3=88)
(1, 2, 88)

>>> f1(arg3=3, arg2=2, arg1=1) 
(1, 2, 3)

Invalid:

>>> f1(arg3=77,1,2)
SyntaxError: positional argument follows keyword argument

>>> f1(arg1=1,2,3)
SyntaxError: positional argument follows keyword argument

Arbitrary number of arguments, followed by obligation of named arguments:

One can also define a function that takes a variable number of arguments followed by mandatorily named arguments, using the operator *:

def f2(*args, default=99):
    return (args, default)

In this example the first arguments (necessarily positional), no matter how many, will be received in the variable args who’s kind tuple, note that the example function returns a tuple, so when calling it we will return a tuple that contains the tuple args and the value of the variable default, let’s see some examples:

>>> f2(1,2,3)
((1, 2, 3), 99)

>>> f2(1,2,3,4,5,6,'teste',[1,2,3]) 
((1, 2, 3, 4, 5, 6, 'teste', [1, 2, 3]), 99)

>>> f2(1,2,default=3)
((1, 2), 3)

Only named arguments, mandatorily:

For the function to accept only named arguments, on a mandatory basis, without the need to accept an arbitrary number of positional arguments through the operator *, just use that same operator, with nothing in front of it:

def f3(*, arg1, arg2, arg3):
    return(arg1, arg2, arg3)

Note that now it is mandatory that the call is made through the form of named arguments and obligatory sending all of them, although we can still vary the order of the arguments, so we have as valid calls:

>>> f3(arg3=1,arg2=2,arg1=3)
(3, 2, 1)

>>> f3(arg1=3,arg2=2,arg3=1)
(3, 2, 1)

But the calls below are invalid:

>>> f3(1, 2, 3)
f3() takes 0 positional arguments but 3 were given

>>> f3(arg1=1, arg2=2)
TypeError: f3() missing 1 required keyword-only argument: 'arg3'

Named arguments, including defaults:

it is also allowed to mix the mandatory named arguments with arguments defaults, so the last call above would not produce error, for that the function would have to be defined as below:

def f3(*, arg1, arg2, arg3=99):
    return(arg1, arg2, arg3)

The asterisk can be positioned anywhere, for example one could define the function with only the first parameter being positional and the other two being mandatory named, thus:

def f4(arg1, *, arg2,  arg3):
    return(arg1, arg2, arg3)

In this case all arguments would be mandatory, and the last two would have to be called, obligatorily named, so we would have as invalid call, for example:

>>> f4(1, arg3=3)
f4() missing 1 required keyword-only argument: 'arg2'

In order for this last call to be valid, we would have to have defined the function as follows:

def f4(arg1, *, arg2=2,  arg3):
    return(arg1, arg2, arg3)

Arguments named arbitrarily

With Python it is also possible for a function to receive an arbitrary number of named arguments, let’s use as an example the function media_curso() of a free course school that receives as a parameter, the name of the course and the names and grades of the students, to calculate the arithmetic average of the class.

def media_curso(curso, **notas):
    return sum(list(notas.values()))/len(notas)

Let’s call the function to the class of 4 students of the Python course:

>>> media_curso('python', john=7, Doe=9, Foo=5, Bar=6)
6.75

Let’s say a student enters late in the course and next month the function would have to be called including her grade, so the call would be:

>>>> media_curso('python', john=9, Doe=6, Foo=8, Bar=5, Lena=9 )
7.4

What the two asterisks (**) do is take all named arguments that have been sent in sequence, package in a dictionary and assign to the argument in front of you (in this case notas), another way to call the function would be by sending an already "solved" or "unpacked" dictionary, thus:

>>> d = {'john': 9, 'Doe': 6, 'Foo': 8, 'Bar': 5, 'Lena': 9}
>>> media_curso('python', **d )  
7.4  

All mixed up:

As seen, we can either pass or receive arbitrary numbers of arguments, whether named or not. This is why we often see calls of the type below, especially in the use of inheritance, very common in frameworks like Django.

def some_method(self, *args, **kwargs):
    # do something
    # ...
    super().some_method(*args, **kwargs)

Note that the convention kwargs is to denote arguments sent through "key/Value". This is a common practice when overriding methods of frameworks where you want to change the access parameters and then call the "parent" method in the call super().

Specifically answering the question:

After editing the question title I noticed that nay I explicitly left a way to meet it specifically, so I elaborated, based on the above, the following solution:

def f(**args):
    if len(args)>0:
        if ['nome', 'cpf'] != list(args):
            return 'Erro' # Ou levante uma exceção aqui
    # Faça o que for necessário 
    return 'Ok'   

Testing:

# Sem nenhum argumento:
f()
'Ok'

# Somente com um argumento
f(nome='Foo') 
'Erro'

# Com dois argumentos, sendo um com o nome errado
f(nome='Foo', idade=21) 
'Erro'

# Com os dois argumentos exigidos
f(nome='Foo', cpf=1234) 
'Ok'

Note that with this solution, the two arguments are mandatory if one of them is delivered, that is, either you send both or you send nothing. But you can put a mandatory parameter in front, for example:

def f(cidade, **args):
    if len(args)>0:
        if ['nome', 'cpf'] != list(args):
           return 'Erro' # Ou levante uma exceção aqui
    # Faça o que for necessário 
    return cidade

Now you can no longer call if you do not send at least the city argument, although the previous conditions for nome and cpf remain valid.

  • 1

    I would recommend a book where that answer is one of the chapters! : -) But I think it directly addresses only half of the question’s problem: how to force all arguments. But it does not address "how to allow any improvement" - of course understanding so far, one can build a very solid application. (myself, seeing all the possibilities described so I’m wondering if there is a simpler way than the decorators I suggested)

  • 1

    @jsbueno, After editing the title by Anderson, I had another understanding of the question, so based on what I had already written, I worked out an approach to meet, I believe with a little more time could get better.

3

Of course the language has no way to bring ready all imaginable uses possible - either for the list of parameters, or for other things.

But it includes enough mechanisms for you to implement it - in case, a good way is to make use of decorators! Decorators are a syntax for functions that modify other functions.

This answer uses very advanced aspects of the language - no problem if despite understanding the concepts can not understand all the code now - the examples below use concepts and mechanisms solid enough of the language to be incorporated into a production project (i.e.: do not depend on implementation details, or undocumented functionalities).

Decorator that checks the parameters:

So in this case, you take the controls that, without this mechanism, would have to be in normal code, within the function - an "if" to check if the values were passed, for example, and move this logic to the decorator.

This is all quiet. Another thing we need to do is make it work for generic functions: that is, the decorator will not know if the function in which it will be applied will have 1, 2 or 10 optional parameters.

The module inspect has functions that allow you to check this - and then we can raise a Typeerror with the appropriate message (or return a fixed error value, if you prefer).

In this case, the module inspect yes, it has classes and utilities to handle all possible forms of parameters - including annotations, lists of arguments in order, parameters passed as dictionaries, etc...this makes its use a little complicated -

For the case in question, there are a few steps: Use inspect.signature to extract the function signature, in the returned object, (which is a class Signature module Inspect), we call the method bind to apply the parameters passed, and on the object returned so we call the method apply_defaults and then we look if, once applied the parameters, left some of the default values that you determined = for this we compare the arguments that were linked with what the Signature has in its attribute Parameters.

It sounds complicated, but you have to do it once - and you can keep it in a module utils.py in your project:

from functools import wraps
import inspect

def all_or_nothing(func):
    signature = inspect.signature(func)

    @wraps(func)
    def wrapper(*args, **kw):
        bound_args = signature.bind(*args, **kw)
        bound_args.apply_defaults()

        all_ = True
        any_ = False
        for parameter, argument in zip(signature.parameters.values(), bound_args.arguments.values()):
            if parameter.default == argument:
                all_ = False
            else:
                any_ = True
        if any_ and not all_:
            raise TypeError(f"Either pass all optional arguments or no argument in call to {func.__name__}")
        return func(*args, **kw)

    return wrapper

(It would be possible to make a simpler decorator, without using the "Inspect" module, based only on the number of arguments passed - but he would have no advantages over that unless he did not require the decorator himself to know how to use the functionality of the module).

Working:

In [137]: @all_or_nothing
     ...: def func(nome="", cpf=None):
     ...:     pass
     ...: 
     ...: 

In [138]: func("alo", 123)

In [139]: func()

In [140]: func("alo")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
(...)
TypeError: Either pass all optional arguments or no argument in call to func

Using Polymorphism

Another approach, also with decorators, allows you to emulate polymorphism - a feature that exists in statically typed languages like Java and C++ - in this case, you create functions with the same name, but with a list of different parameters, and the decorator mechanism dispatches the call to the appropriate function according to the parameters passed. (The same Python mechanism also has to make a decorated way to have the same name).

Python itself already has a special decorator who does something similar, but only based on the type of the first parameter - the functools.singledispatch - but it does not dispatch between numbers of different parameters (and nor does it allow decorated functions to have the same name). So basically this mechanism would have to be recreated - this, like the previous one, although complex, is not extensive, and can be done only once:

from functools import wraps
import inspect

def polymorph(original_func):
    registry = {}
    original_signature = inspect.signature(original_func)
    def register(func):
        registry[tuple(inspect.signature(func).parameters.keys())] = func
        return wrapper

    @wraps(func)
    def wrapper(*args, **kw):
        arg_names = tuple(original_signature.bind(*args, **kw).arguments.keys())
        try:
            dispatchee = registry[arg_names]
        except KeyError:
            raise TypeError(f"No registered version of {func.__name__} found requiring parameters {arg_names}")
        return dispatchee(*args, **kw)

    register(original_func)
    wrapper.register = register
    wrapper.registry = registry
    return wrapper

And that working:

In [184]: @polymorph
     ...: def func(nome="", cpf=123):
     ...:     print(nome, cpf)
     ...: 
     ...:     
     ...:     
     ...:     
     ...:         

In [185]: @func.register
     ...: def func():
     ...:     print("no parameters")
     ...:         
     ...:         

In [186]: func()
no parameters

In [187]: func("alo", 123)
alo 123

In [188]: func("alo")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
(...)
TypeError: No registered version of func found requiring parameters ('nome',)

Note that it would not be difficult to extend this mechanism to act also check the types of the parameters given by annotations - and this would enable "full polymorphism" - as exists in languages like Java or C++ in just over 20 lines of code

2

First, read the answers:

But a specific solution to your problem - that is, something that is not generic and independent of function - is to manually validate the conditions. Having two parameters which, by default, are None, you can check if both are None or if both are different from None. If only one of them is None means that not all parameters have been informed.

The logical operator XOR solves the problem well:

def func(nome=None, cpf=None):
    if ((nome is None) ^ (cpf is None)):
        raise ValueError('Nem todos os parâmetros foram informados')
    print('Ok')

Thus:

>>> func()
'Ok'

>>> func('Anderson', 123)
'Ok'

>>> func('Anderson')
ValueError: Nem todos os parâmetros foram informados

>>> func(cpf=123)
ValueError: Nem todos os parâmetros foram informados

1

From what I understand, what you want is to force the passage of the two values or not pass any, the middle ground does not interest you, that’s it?

In some languages the method superscript is created by creating a new one with the same name, but with other parameters and so another logic, in python this does not occur, in it you have this feature to define a default value.

For this your case, the best thing to do is to create an internal check, if both fields were properly filled or none, then launches a return, otherwise launches another return.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.