Função partial da biblioteca functools

Bom dia pessoal,a minha duvida é um tanto quanto simples porem para mim esta confuso, alguém poderia me explicar como funciona a função partial da biblioteca functools ? Vcs poderiam me dar um exemplo simples para q eu possa entender? Desde ja obrigado!!!

Rapaz!
Essa foi na ferida, mas vamos lá. (risos)
Primeiramente vamos entender o que é o modulo functools, a primeiro momento esse modulo não se mostra muito importante, mas ele é repleto de funções de suma importância e a medida que você vai se aprofundando no Python ou desenvolvendo grandes projetos ele se mostra cada vez mais útil, esse modulo trabalha com funções que retornam funções e isso nos possibilita fazer alguns testes, melhorias, ou alterações nas funções deixando a função original “intacta”, um bom exemplo seriam os decoradores de funções, pois os mesmo permitem de uma maneira simples modificar o comportamento de uma função sem necessariamente alterá-la.

1 - Exemplo de um decorador:
Imagine que eu tenha uma função soma(x,y) que recebe dois paramentos e imprime a soma dos mesmos, mas por alguma razão eu queria personalizar minha função de forma que ela imprima também os dois argumentos para que eu tenha nota do que esta sendo somado, mas eu quero fazer isso sem alterar minha função soma, como resolver?! simples, com um decorador.

#FUNÇÃO DECORADOR
def soma_decorador(func):
    def soma_decorada(x,y):
        print('x:',x)
        print('y:',y)
        return func(x,y)
    return soma_decorada

@soma_decorador  #DECORADOR
def soma(x,y):
    print('Total:', x+y)

soma(2,3)  
#SAIDA SEM DECORADOR
# Total: 5

#SAIDA COM DECORADOR
# x: 2
# y: 3 
# Total: 5

2 - Exemplo da função .reduce():
A função reduce() é uma das mais famosas do modulo functools. Você lembra do método soma(x,y)?! Pois bem, vamos usa-lo mais uma vez, imagine que você tem um método muito importante chamado soma(x,y), que retorna a soma de dois números, mas em algum momento você precisa somar uma lista enorme de números por alguma razão, mas você já tem uma função que faz isso, mas ela soma de dois em dois, todavia você não quer corromper a integridade de sua função preciosa alterando algum código dela, o que fazer?! Simples, basta usar a função reduce().

import functools

list_numbers = [1, 3, 4, 5, 6, 7, 8, 9] #lista

#seu método soma.
def soma(x, y):
   return x + y

#Usando o método soma
result = soma(1,3)
print(result) #saida --> 4

#agora, com a função reduce
reduce_soma = functools.reduce(soma, list_numbers)
print(reduce_soma) #saida --> 43

Deu para perceber a importância do modulo functools?!

3 - Exemplo da função .partial():
Para finalizar e fechar com a tão esperada chave de ouro, a função partial, é necessário que você entenda o uso do *args e do **kwargs, partindo do principio que você entenda o uso dos mesmo vamos ao “ataque”.

A função partial permite consertar o número de argumentos de uma função e até mesmo gerar uma nova função, essa função retorna uma função parcial, que podemos dizer que é “quase” idêntica a original, mas dispõe de algumas alterações.

Mais uma vez, vamos usar a função soma para uma pequena demonstração. Agora imagine que por alguma razão você não deseje mais informar um valor para o argumento y, por alguma razão ele não é mais necessário momentaneamente, talvez em um futuro você volte a usar esse argumento novamente, o que fazer?! Lembrando que você quer preservar seu código para uma possível necessidade de usar o mesmo em um futuro próximo, há alguma solução?! Sim, a função .partial().

import functools
  
def soma(x,y):
    return x + y 
  
nova_soma = functools.partial(soma, 0) #Atribuindo um valor padrão para x.  

#agora x esta "desativado" na função nova_soma() e não pode ser setado.
print(nova_soma(4)) #saída --> 4

nova_soma_v2 = functools.partial(soma, y=3) #Atribuindo um valor padrão para y.  
print(nova_soma_v2(5)) #saída --> 8
print(nova_soma_v2(5, y=10)) #saída --> 15

Outro bom exemplo seria desativar as **kwargs
Imagina que você tem um sistema quem tem duas funções opcionais elas são desconto e imposto, mas imagine que por alguma razão você quer desativar essas duas funcionalidades opcionais e quer a certeza que ainda que as mesmas sejam setados não interferirão no seu código, em outras palavras, você quer uma nova versão da sua função, mas também quer que a original fique intacta. o que faz?!

def compra(total, **kwargs):
    desconto = kwargs.get('desconto')
    tax = kwargs.get('tax')

    if desconto:
        total -= total * (desconto / 100)         

    if tax:
        total += total * (tax / 100)
    
    return total

total_compra = compra(121)
print(total_compra) #Saida --> 121

total_compra = compra(120, tax=5)
print(total_compra) #Saida --> 126

total_compra = compra(100, tax=7, desconto=10)
print(total_compra)  #Saida --> 96.3

Como você pode ver, até o momento o desconto e o imposto são opcionais, mas eles influenciam no seu programa quando setados, a partir de agora você quer desativa-los e ainda que sejam setados eles não podem interferir no seu código, como resolver isso?! Simples, é só usar a função partial.

def partial(compra, total, tax, desconto):
    def compra_nova(total, tax=None, desconto=None):
        return compra(total)
    return compra_nova

compra_nova = partial(compra,100,None,None) 

print(compra_nova(100, desconto=100, tax=100)) #saida --> 100
print(compra_nova(100, desconto=100)) #saida --> 100
print(compra_nova(100, tax=100)) #saida --> 100
print(compra_nova(100)) #saida --> 100

A partial nos permite criar milhares de versões de nossas funções, para esses exemplos, talvez deixe parecer mais fácil ir lá e só editar, mas imagina que você tenha que fazer isso 10, 15 ou 20x, quem sabe até mais, você reescrevia função fazendo algumas alterações diversas vezes, já imaginou o tanto de linhas que você precisaria e quanto cansativo seria?! Já com o auxilio do modulo functools e seus métodos você escreve menos e tem o mesmo resultado, de uma maneira breve, a partil trabalha com os argumentos de um método, lembrando que o modulo functools é caracterizado como um modulo avançado dentro da linguagem Python.

Segue alguns links importantes:
functools — Higher-order functions and operations on callable objects
How does functools partial do what it does?
Python Partials are Fun!
Partial Functions in Python

Abraços!