Executando verificação de segurança...
4

Não estou com tempo pra criar um PR agora, mas seguem algumas sugestões.

Para limpar a string, em vez de ter uma lista do que quer remover, é mais simples filtrar pelo que quer manter, já que é uma lista bem menor (apenas os dígitos de 0 a 9).

Ou seja,

import string

def __clear(string_chain: str) -> str:
    # cria outra string apenas com os dígitos de 0 a 9
    return ''.join(c for c in string_chain if c in string.digits)

Na verdade é questionável se uma string como "abc123xxx456,;.^789{$#@}09" deveria ser considerada um CPF (afinal, estamos limpando todos os caracteres indesejados e verificando os números que sobraram). Talvez ele só devesse aceitar no máximo os separadores (ponto e hífen) em posições específicas, mas enfim...


Se bem que, como vc usa isso para fazer o cálculo dos dígitos verificadores, talvez seja mais simples já converter tudo para int, em vez de criar outra string.

E o cálculo pode ser feito mais diretamente, não precisa separar em tantos métodos. E até o uso de reduce eu acho desnecessário, pois um loop simples é mais que suficiente.

Fiz uma versão mais simplificada, mas que pode ser adaptada no seu código:

import string

# retorna uma lista com os dígitos presentes na string
def extrai_digitos(string_chain):
    return list(int(c) for c in string_chain if c in string.digits)

def calcula_digito(multiplicador, digitos):
    total = 0
    for d in digitos:
        if multiplicador >= 2:
            total += d * multiplicador
            multiplicador -= 1
        else: break
    resto = total % 11
    if resto < 2:
        return 0
    else:
        return 11 - resto

def validar_cpf(cpf):
    digitos = extrai_digitos(cpf)
    if len(digitos) != 11:
        return False

    # primeiro dígito não bate, CPF inválido
    if digitos[9] != calcula_digito(10, digitos):
        return False

    # segundo dígito não bate, CPF inválido
    if digitos[10] != calcula_digito(11, digitos):
        return False

    return True

string_chain = '123@#$%456&*()789-09'
print(validar_cpf(string_chain)) # True

Ah, também dá pra calcular ambos os dígitos verificadores em um único loop:

import string

# retorna uma lista com os dígitos presentes na string
def extrai_digitos(string_chain):
    return list(int(c) for c in string_chain if c in string.digits)

def calcula_resto(resto):
    if resto < 2:
        return 0
    return 11 - resto

def validar_cpf(cpf):
    digitos = extrai_digitos(cpf)
    if len(digitos) != 11:
        return False

    # no mesmo loop, calculo ambos os dígitos verificadores
    total1 = total2 = 0
    multiplicador = 11
    for d in digitos:
        if multiplicador >= 2:
            if multiplicador >= 3:
                total1 += d * (multiplicador - 1)
            total2 += d * multiplicador
            multiplicador -= 1
        else: break

    # CPF válido se os DV's batem
    return digitos[9] == calcula_resto(total1 % 11) and digitos[10] == calcula_resto(total2 % 11)

string_chain = '123@#$%456&*()789-09'
print(validar_cpf(string_chain)) # True

Algoritmos adaptados daqui e daqui.

1

Muito obrigado pelas dicas.
A ideia de usar um método de limpeza que englobe tantos caracteres me veio depois de um cliente precisar persisdir dados de clientes em um bando de dados usando extensos arquivos xlsx gerados por outro sistema. Não sei o porque mas esses relatórios gerados, além de gerar dados bagunçados, ainda gerava saídas com caracteres estranhos e indesejáveis em meio aos dados.
Tentei usar regex para selecionar somente os números, mas mesmo assim não tive êxito.
Ah, e como os arquivos xlsx do cliente tinham muitos dados, com reduce consegui um certo desempenho melhorado, nada tão melhor assim, mas no final melhorou.
Vou aplicar sua ideia de realizar o cálculo dos dígitos em um único momento.
Forte abraço.

2

Embora seja possível com regex, acho desnecessário. O loop que sugeri acima é bem mais simples, além de ser mais rápido. Fiz um teste básico pra verificar:

import string, re
from timeit import timeit

r = re.compile('[0-9]')
string_chain = '123@#$%456&*()789-09'

# executa 1 milhão de vezes cada teste
params = { 'number' : 1000000, 'globals': globals() }

# imprime os tempos em segundos
# todos retornam uma lista com os dígitos já convertidos para int

# loop simples
print('          loop:', timeit('list(int(c) for c in string_chain if c in string.digits)', **params))

# loop simples, usando um set com os dígitos (ligeiramente mais rápido)
digitos = set(string.digits)
print('  loop com set:', timeit('list(int(c) for c in string_chain if c in digitos)', **params))

# regex finditer (o mais lento)
print('regex finditer:', timeit('list(int(c[0]) for c in r.finditer(string_chain))', **params))

# regex findall (o segundo mais lento)
print(' regex findall:', timeit('list(int(c) for c in r.findall(string_chain))', **params))

Os tempos exatos variam de acordo com o hardware, mas de forma geral, regex foi o mais lento. Na minha máquina o resultado foi (tempos em segundos, quanto menor, mais rápido):

          loop: 2.2100358860002416
  loop com set: 1.788499561000208
regex finditer: 3.175833621000038
 regex findall: 2.61522052600003

Claro que para poucas strings pequenas, a diferença será imperceptível. Mas se tiver milhões de strings para validar, começa a fazer diferença.