web-dev-qa-db-fra.com

Caractères valides dans un nom de classe python

Je crée dynamiquement des classes python et je sais que tous les caractères ne sont pas valides dans ce contexte. 

Existe-t-il une méthode quelque part dans la bibliothèque de classes que je puisse utiliser pour nettoyer une chaîne de texte aléatoire, de sorte que je puisse l'utiliser comme nom de classe? Cela ou une liste des caractères autorisés seraient d'une aide précieuse.


Ajout concernant les conflits avec les noms d'identifiant: Comme @Ignacio l'a souligné dans la réponse ci-dessous, tout caractère qui est valide comme identifiant est un caractère valide dans un nom de classe. Et vous pouvez même utiliser un réservé Word comme nom de classe sans aucun problème. Mais il y a un problème. Si vous utilisez un mot réservé, vous ne pourrez pas rendre la classe accessible comme les autres classes (créées de manière non dynamique) (par exemple, en effectuant globals()[my_class.__name__] = my_class). Le mot réservé aura toujours priorité dans ce cas. 

27
Filipe Correia

Référence du langage Python, §2.3, "Identifiants et mots-clés"

Les identifiants (également appelés noms ) sont décrits dans les définitions lexicales suivantes:

identifier ::=  (letter|"_") (letter | digit | "_")*
letter     ::=  lowercase | uppercase
lowercase  ::=  "a"..."z"
uppercase  ::=  "A"..."Z"
digit      ::=  "0"..."9"

Les identifiants ont une longueur illimitée. Le cas est significatif.

34

Conformément à Référence du langage Python, §2.3, "Identifiants et mots clés" , un identifiant Python valide est défini comme suit:

(letter|"_") (letter | digit | "_")*

Ou, dans regex :

[a-zA-Z_][a-zA-Z0-9_]*
6
sevko

Ce qui est intéressant, c’est que le premier caractère d’un identifiant est spécial. Après le premier caractère, les chiffres de 0 à 9 sont valables pour les identificateurs, mais ils ne doivent pas être le premier.

Voici une fonction qui retournera un identifiant valide à partir d’une chaîne de caractères aléatoire. Voici comment ça fonctionne:

Tout d'abord, nous utilisons itr = iter(seq) pour obtenir un itérateur explicite sur l'entrée. Ensuite, il y a une première boucle qui utilise l'itérateur itr pour examiner les caractères jusqu'à ce qu'il trouve un premier caractère valide pour un identificateur. Ensuite, il sort de cette boucle et exécute la deuxième boucle, en utilisant le même itérateur (que nous avons nommé itr) pour la deuxième boucle. L'itérateur itr conserve notre place pour nous; les caractères que la première boucle extraite de l'itérateur ont encore disparu lorsque la deuxième boucle s'exécute.

def gen_valid_identifier(seq):
    # get an iterator
    itr = iter(seq)
    # pull characters until we get a legal one for first in identifer
    for ch in itr:
        if ch == '_' or ch.isalpha():
            yield ch
            break
    # pull remaining characters and yield legal ones for identifier
    for ch in itr:
        if ch == '_' or ch.isalpha() or ch.isdigit():
            yield ch

def sanitize_identifier(name):
    return ''.join(gen_valid_identifier(name))

C'est une façon propre et Pythonique de gérer une séquence de deux manières différentes. Pour un problème aussi simple, nous pourrions simplement avoir une variable booléenne qui indique si nous avons déjà vu le premier caractère ou non:

def gen_valid_identifier(seq):
    saw_first_char = False
    for ch in seq:
        if not saw_first_char and (ch == '_' or ch.isalpha()):
            saw_first_char = True 
            yield ch
        Elif saw_first_char and (ch == '_' or ch.isalpha() or ch.isdigit()):
            yield ch

Je n'aime pas cette version presque autant que la première version. Le traitement spécial pour un caractère est maintenant emmêlé dans tout le flux de contrôle, ce qui sera plus lent que la première version car il doit constamment vérifier la valeur de saw_first_char. Mais c’est ainsi que vous devrez gérer le flux de contrôle dans la plupart des langues! L'itérateur explicite de Python est une fonctionnalité intéressante, et je pense que cela améliore beaucoup ce code.

La boucle sur un itérateur explicite est aussi rapide que de laisser Python obtenir implicitement un itérateur, et l'itérateur explicite nous permet de scinder les boucles qui gèrent les différentes règles pour différentes parties de l'identifiant. Ainsi, l'itérateur explicite nous donne un code plus propre qui s'exécute également plus rapidement. Gagner/Gagner.

5
steveha

C'est une vieille question à ce jour, mais j'aimerais ajouter une réponse sur la manière de procéder dans Python 3 au fur et à mesure de la mise en œuvre.

Les caractères autorisés sont documentés ici: https://docs.python.org/3/reference/lexical_analysis.html#identifiers . Ils comprennent un grand nombre de caractères spéciaux, notamment la ponctuation, le soulignement et toute une série de caractères étrangers. Heureusement, le module unicodedata peut vous aider. Voici mon implémentation implémentant directement ce que dit la documentation Python:

import unicodedata

def is_valid_name(name):
    if not _is_id_start(name[0]):
        return False
    for character in name[1:]:
        if not _is_id_continue(character):
            return False
    return True #All characters are allowed.

_allowed_id_continue_categories = {"Ll", "Lm", "Lo", "Lt", "Lu", "Mc", "Mn", "Nd", "Nl", "Pc"}
_allowed_id_continue_characters = {"_", "\u00B7", "\u0387", "\u1369", "\u136A", "\u136B", "\u136C", "\u136D", "\u136E", "\u136F", "\u1370", "\u1371", "\u19DA", "\u2118", "\u212E", "\u309B", "\u309C"}
_allowed_id_start_categories = {"Ll", "Lm", "Lo", "Lt", "Lu", "Nl"}
_allowed_id_start_characters = {"_", "\u2118", "\u212E", "\u309B", "\u309C"}

def _is_id_start(character):
    return unicodedata.category(character) in _allowed_id_start_categories or character in _allowed_id_start_categories or unicodedata.category(unicodedata.normalize("NFKC", character)) in _allowed_id_start_categories or unicodedata.normalize("NFKC", character) in _allowed_id_start_characters

def _is_id_continue(character):
    return unicodedata.category(character) in _allowed_id_continue_categories or character in _allowed_id_continue_characters or unicodedata.category(unicodedata.normalize("NFKC", character)) in _allowed_id_continue_categories or unicodedata.normalize("NFKC", character) in _allowed_id_continue_characters

Ce code est adapté de ici sous CC0: https://github.com/Ghostkeeper/Luna/blob/d69624cd0dd5648aec2139054fae4d45b634da7e/plugins/data/enumerated/enumerated/enumerated_type.py#L91 . Il a été bien testé.

1
Ghostkeeper