Skip to content

Quelle est la relation entre le modèle de données Python et les fonctions intégrées ?

Solution:

Quelle est la relation entre le modèle de données Python et les fonctions intégrées ?

  • Les fonctions intégrées et les opérateurs utilisent les méthodes ou attributs de modèle de données sous-jacents.
  • Les commandes intégrées et les opérateurs ont un comportement plus élégant et sont en général plus compatibles vers l’avant.
  • Les méthodes spéciales du modèle de données sont des interfaces sémantiquement non publiques.
  • Les commandes intégrées et les opérateurs de langage sont spécifiquement destinés à être l’interface utilisateur pour le comportement implémenté par des méthodes spéciales.

Ainsi, vous devriez préférer utiliser les fonctions et opérateurs intégrés lorsque cela est possible plutôt que les méthodes et attributs spéciaux du modèle de données.

Les API sémantiquement internes sont plus susceptibles de changer que les interfaces publiques. Bien que Python ne considère rien de “privé” et expose les éléments internes, cela ne signifie pas que c’est une bonne idée d’abuser de cet accès. Cela comporte les risques suivants :

  • Vous pouvez constater que vous avez plus de changements de rupture lors de la mise à niveau de votre exécutable Python ou du passage à d’autres implémentations de Python (comme PyPy, IronPython ou Jython, ou une autre implémentation imprévue.)
  • Vos collègues auront probablement une mauvaise opinion de vos compétences linguistiques et de votre conscience, et considéreront cela comme une odeur de code, vous amenant ainsi que le reste de votre code à un examen plus approfondi.
  • Les fonctions intégrées sont faciles à intercepter le comportement pour. L’utilisation de méthodes spéciales limite directement la puissance de votre Python pour l’introspection et le débogage.

En profondeur

Les fonctions et opérateurs intégrés invoquent les méthodes spéciales et utilisent les attributs spéciaux du modèle de données Python. Ils sont le placage lisible et maintenable qui cache l’intérieur des objets. En général, les utilisateurs doivent utiliser les fonctions intégrées et les opérateurs donnés dans le langage au lieu d’appeler les méthodes spéciales ou d’utiliser directement les attributs spéciaux.

Les fonctions et opérateurs intégrés peuvent également avoir un comportement de repli ou plus élégant que les méthodes spéciales plus primitives du modèle de données. Par exemple:

  • next(obj, default) vous permet de fournir une valeur par défaut au lieu d’augmenter StopIteration lorsqu’un itérateur s’épuise, tandis que obj.__next__() ne fait pas.
  • str(obj) repli sur obj.__repr__() lorsque obj.__str__() n’est pas disponible – alors que vous appelez obj.__str__() déclencherait directement une erreur d’attribut.
  • obj != other repli sur not obj == other en Python 3 quand non __ne__ – appeler obj.__ne__(other) n’en profiterait pas.

(Les fonctions intégrées peuvent également être facilement éclipsées, si nécessaire ou souhaitable, sur la portée globale d’un module ou la builtins module, pour personnaliser davantage le comportement.)

Mappage des fonctions intégrées et des opérateurs au modèle de données

Voici un mappage, avec des notes, des fonctions et opérateurs intégrés vers les méthodes et attributs spéciaux respectifs qu’ils utilisent ou renvoient – notez que la règle habituelle est que la fonction intégrée correspond généralement à une méthode spéciale du même nom, mais ceci n’est pas assez cohérent pour justifier de donner cette carte ci-dessous :

builtins/     special methods/
operators  -> datamodel               NOTES (fb == fallback)

repr(obj)     obj.__repr__()          provides fb behavior for str
str(obj)      obj.__str__()           fb to __repr__ if no __str__
bytes(obj)    obj.__bytes__()         Python 3 only
unicode(obj)  obj.__unicode__()       Python 2 only
format(obj)   obj.__format__()        format spec optional.
hash(obj)     obj.__hash__()
bool(obj)     obj.__bool__()          Python 3, fb to __len__
bool(obj)     obj.__nonzero__()       Python 2, fb to __len__
dir(obj)      obj.__dir__()
vars(obj)     obj.__dict__            does not include __slots__
type(obj)     obj.__class__           type actually bypasses __class__ -
                                      overriding __class__ will not affect type
help(obj)     obj.__doc__             help uses more than just __doc__
len(obj)      obj.__len__()           provides fb behavior for bool
iter(obj)     obj.__iter__()          fb to __getitem__ w/ indexes from 0 on
next(obj)     obj.__next__()          Python 3
next(obj)     obj.next()              Python 2
reversed(obj) obj.__reversed__()      fb to __len__ and __getitem__
other in obj  obj.__contains__(other) fb to __iter__ then __getitem__
obj == other  obj.__eq__(other)
obj != other  obj.__ne__(other)       fb to not obj.__eq__(other) in Python 3
obj < other   obj.__lt__(other)       get >, >=, <= with @functools.total_ordering
complex(obj)  obj.__complex__()
int(obj)      obj.__int__()
float(obj)    obj.__float__()
round(obj)    obj.__round__()
abs(obj)      obj.__abs__()

Les operator le module a length_hint qui a une solution de repli implémentée par une méthode spéciale respective si __len__ n’est pas implémenté :

length_hint(obj)  obj.__length_hint__() 

Recherches en pointillés

Les recherches en pointillés sont contextuelles. Sans implémentation de méthode spéciale, regardez d’abord dans la hiérarchie des classes pour les descripteurs de données (comme les propriétés et les slots), puis dans l’instance __dict__ (par exemple les variables), puis dans la hiérarchie des classes pour les descripteurs non-données (comme les méthodes). Des méthodes spéciales implémentent les comportements suivants :

obj.attr      obj.__getattr__('attr')       provides fb if dotted lookup fails
obj.attr      obj.__getattribute__('attr')  preempts dotted lookup
obj.attr = _  obj.__setattr__('attr', _)    preempts dotted lookup
del obj.attr  obj.__delattr__('attr')       preempts dotted lookup

Descripteurs

Les descripteurs sont un peu avancés – n’hésitez pas à ignorer ces entrées et à revenir plus tard – rappelez-vous que l’instance de descripteur se trouve dans la hiérarchie des classes (comme les méthodes, les emplacements et les propriétés). Un descripteur de données implémente soit __set__ ou __delete__:

obj.attr        descriptor.__get__(obj, type(obj)) 
obj.attr = val  descriptor.__set__(obj, val)
del obj.attr    descriptor.__delete__(obj)

Lorsque la classe est instanciée (définie) la méthode de descripteur suivante __set_name__ est appelé si un descripteur l’a pour informer le descripteur de son nom d’attribut. (Ceci est nouveau dans Python 3.6.) cls est le même que type(obj) ci-dessus, et 'attr' remplace le nom de l’attribut :

class cls:
    @descriptor_type
    def attr(self): pass # -> descriptor.__set_name__(cls, 'attr') 

Articles (notation en indice)

La notation en indice est également contextuelle :

obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)

Un cas particulier pour les sous-classes de dict, __missing__ est appelé si __getitem__ ne trouve pas la clé :

obj[name]         -> obj.__missing__(name)  

Les opérateurs

Il existe également des méthodes spéciales pour +, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, | opérateurs, par exemple :

obj + other   ->  obj.__add__(other), fallback to other.__radd__(obj)
obj | other   ->  obj.__or__(other), fallback to other.__ror__(obj)

et opérateurs en place pour une affectation augmentée, +=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=, par exemple:

obj += other  ->  obj.__iadd__(other)
obj |= other  ->  obj.__ior__(other)

(Si ces opérateurs sur place ne sont pas définis, Python revient, par exemple, pour obj += other à obj = obj + other)

et opérations unaires :

+obj          ->  obj.__pos__()
-obj          ->  obj.__neg__()
~obj          ->  obj.__invert__()

Gestionnaires de contexte

Un gestionnaire de contexte définit __enter__, qui est appelée lors de la saisie du bloc de code (sa valeur de retour, généralement self, est aliasée avec as), et __exit__, dont l’appel est garanti à la sortie du bloc de code, avec une information d’exception.

with obj as enters_return_value: #->  enters_return_value = obj.__enter__()
    raise Exception('message')
                                 #->  obj.__exit__(Exception, 
                                 #->               Exception('message'), 
                                 #->               traceback_object)

Si __exit__ obtient une exception puis renvoie une valeur fausse, il la relancera en quittant la méthode.

Si aucune exception, __exit__ obtient None pour ces trois arguments à la place, et la valeur de retour n’a pas de sens :

with obj:           #->  obj.__enter__()
    pass
                    #->  obj.__exit__(None, None, None)

Quelques méthodes spéciales de métaclasse

De même, les classes peuvent avoir des méthodes spéciales (de leurs métaclasses) qui prennent en charge les classes de base abstraites :

isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)

Un point important à retenir est que, bien que les éléments intégrés comme next et bool ne pas changer entre Python 2 et 3, les noms d’implémentation sous-jacents sommes en changeant.

Ainsi, l’utilisation des fonctions intégrées offre également une plus grande compatibilité ascendante.

Quand dois-je utiliser les noms spéciaux ?

En Python, les noms qui commencent par des traits de soulignement sont des noms sémantiquement non publics pour les utilisateurs. Le trait de soulignement est la façon dont le créateur dit : “Ne touchez pas aux mains”.

Ce n’est pas seulement culturel, mais c’est aussi dans le traitement des API par Python. Lorsqu’un colis est __init__.py les usages import * pour fournir une API à partir d’un sous-package, si le sous-package ne fournit pas de __all__, il exclut les noms commençant par des traits de soulignement. Le sous-paquet est __name__ seraient également exclus.

Les outils de saisie semi-automatique IDE sont mélangés dans leur prise en compte des noms qui commencent par des traits de soulignement pour être non publics. Cependant, j’apprécie beaucoup de ne pas voir __init__, __new__, __repr__, __str__, __eq__, etc. (ni aucune des interfaces non publiques créées par l’utilisateur) lorsque je tape le nom d’un objet et un point.

Ainsi j’affirme :

Les méthodes spéciales “dunder” ne font pas partie de l’interface publique. Évitez de les utiliser directement.

Alors quand les utiliser ?

Le cas d’utilisation principal est l’implémentation de votre propre objet personnalisé ou sous-classe d’un objet intégré.

Essayez de ne les utiliser qu’en cas d’absolue nécessité. Voici quelques exemples:

Utilisez le __name__ attribut spécial sur les fonctions ou les classes

Lorsque nous décorons une fonction, nous obtenons généralement une fonction wrapper en retour qui masque des informations utiles sur la fonction. Nous utiliserions le @wraps(fn) décorateur pour s’assurer que nous ne perdons pas cette information, mais si nous avons besoin du nom de la fonction, nous devons utiliser le __name__ attribut directement :

from functools import wraps

def decorate(fn): 
    @wraps(fn)
    def decorated(*args, **kwargs):
        print('calling fn,', fn.__name__) # exception to the rule
        return fn(*args, **kwargs)
    return decorated

De même, je fais ce qui suit lorsque j’ai besoin du nom de la classe de l’objet dans une méthode (utilisée dans, par exemple, un __repr__):

def get_class_name(self):
    return type(self).__name__
          # ^          # ^- must use __name__, no builtin e.g. name()
          # use type, not .__class__

Utilisation d’attributs spéciaux pour écrire des classes personnalisées ou des fonctions intégrées sous-classées

Lorsque nous voulons définir un comportement personnalisé, nous devons utiliser les noms de modèle de données.

Cela a du sens, puisque nous sommes les implémenteurs, ces attributs ne nous sont pas privés.

class Foo(object):
    # required to here to implement == for instances:
    def __eq__(self, other):      
        # but we still use == for the values:
        return self.value == other.value
    # required to here to implement != for instances:
    def __ne__(self, other): # docs recommend for Python 2.
        # use the higher level of abstraction here:
        return not self == other  

Cependant, même dans ce cas, nous n’utilisons pas self.value.__eq__(other.value) ou not self.__eq__(other) (voir ma réponse ici pour la preuve que ce dernier peut conduire à un comportement inattendu.) Au lieu de cela, nous devrions utiliser le niveau d’abstraction le plus élevé.

Un autre point auquel nous aurions besoin d’utiliser les noms de méthodes spéciales est lorsque nous sommes dans l’implémentation d’un enfant et que nous voulons déléguer au parent. Par exemple:

class NoisyFoo(Foo):
    def __eq__(self, other):
        print('checking for equality')
        # required here to call the parent's method
        return super(NoisyFoo, self).__eq__(other) 

Conclusion

Les méthodes spéciales permettent aux utilisateurs d’implémenter l’interface pour les objets internes.

Utilisez les fonctions et les opérateurs intégrés partout où vous le pouvez. N’utilisez les méthodes spéciales que lorsqu’il n’y a pas d’API publique documentée.

Je vais montrer une utilisation à laquelle vous n’avez apparemment pas pensé, commenter les exemples que vous avez montrés et argumenter contre la revendication de confidentialité à partir de votre propre réponse.


Je suis d’accord avec ta propre réponse que par exemple len(a) doit être utilisé, non a.__len__(). Je le dirais comme ça : len existe pour que nous puissions l’utiliser, et __len__ existe donc len peut l’utiliser. Ou pourtant ça marche vraiment en interne, puisque len(a) peut effectivement être beaucoup plus rapide, au moins par exemple pour les listes et les chaînes :

>>> timeit('len(a)', 'a = [1,2,3]', number=10**8)
4.22549770486512
>>> timeit('a.__len__()', 'a = [1,2,3]', number=10**8)
7.957335462257106

>>> timeit('len(s)', 's = "abc"', number=10**8)
4.1480574509332655
>>> timeit('s.__len__()', 's = "abc"', number=10**8)
8.01780160432645

Mais en plus de définir ces méthodes dans mes propres classes pour une utilisation par les fonctions et opérateurs intégrés, je les utilise occasionnellement comme suit :

Disons que je dois donner une fonction de filtre à une fonction et que je veux utiliser un ensemble s comme filtre. Je ne vais pas créer de fonction supplémentaire lambda x: x in s ou def f(x): return x in s. Non. J’ai déjà une fonction parfaitement bien que je peux utiliser : l’ensemble __contains__ méthode. C’est plus simple et plus direct. Et encore plus rapide, comme indiqué ici (ignorez que je l’enregistre sous f ici, c’est juste pour cette démo de chronométrage):

>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = s.__contains__', number=10**8)
6.473739433621368
>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = lambda x: x in s', number=10**8)
19.940786514456924
>>> timeit('f(2); f(4)', 's = {1, 2, 3}ndef f(x): return x in s', number=10**8)
20.445680107760325

Alors pendant que je ne appeler directement méthodes magiques comme s.__contains__(x), je fais de temps en temps passe eux quelque part comme some_function_needing_a_filter(s.__contains__). Et je pense que c’est parfaitement bien, et mieux que l’alternative lambda/def.


Mon avis sur les exemples que vous avez montrés :

  • Exemple 1 : Lorsqu’on lui a demandé comment obtenir la taille d’une liste, il a répondu items.__len__(). Même sans aucun raisonnement. Mon verdict : c’est tout simplement faux. Devrait être len(items).
  • Exemple 2 : mentionne-t-il d[key] = value premier! Et puis ajoute d.__setitem__(key, value) avec le raisonnement “s’il manque les touches crochets sur votre clavier”, qui s’applique rarement et dont je doute qu’elle soit sérieuse. Je pense que c’était juste le pied dans la porte pour le dernier point, mentionnant que c’est ainsi que nous pouvons prendre en charge la syntaxe des crochets dans nos propres classes. Ce qui revient à une suggestion d’utiliser des crochets.
  • Exemple 3 : Suggestions obj.__dict__. Mauvais, comme le __len__ Exemple. Mais je soupçonne qu’il ne savait tout simplement pas vars(obj), et je peux le comprendre, comme vars est moins commun/connu et le nom diffère du “dict” dans __dict__.
  • Exemple 4 : Suggestions __class__. Devrait être type(obj). Je soupçonne que c’est similaire au __dict__ histoire, même si je pense type est plus connu.

À propos de la confidentialité : dans votre propre réponse, vous dites que ces méthodes sont « sémantiquement privées ». Je suis fortement en désaccord. Simple et double premier les traits de soulignement sont destinés à cela, mais pas les méthodes spéciales “dunder/magic” du modèle de données avec des traits de soulignement doubles de début et de fin.

  • Les deux éléments que vous utilisez comme arguments sont le comportement d’importation et l’autocomplétion de l’IDE. Mais l’importation et ces méthodes spéciales sont des domaines différents, et le seul IDE que j’ai essayé (le populaire PyCharm) n’est pas d’accord avec vous. J’ai créé une classe/un objet avec des méthodes _foo et __bar__ puis la saisie semi-automatique n’a pas offert _foo mais fait offre __bar__. Et quand j’ai utilisé les deux méthodes de toute façon, PyCharm m’a seulement mis en garde contre _foo (le qualifiant de « membre protégé »), ne pas À propos __bar__.
  • PEP 8 dit ‘indicateur “usage interne” faible’ explicitement pour Célibataire trait de soulignement principal, et explicitement pour le double premier souligne qu’il mentionne le nom mutilation et explique plus tard que c’est pour “attributs que vous ne voulez pas que les sous-classes utilisent”. Mais le commentaire sur double avant + arrière les traits de soulignement ne disent rien de tel.
  • La page de modèle de données à laquelle vous vous connectez indique que ces noms de méthodes spéciales sont “Approche Python de la surcharge des opérateurs”. Rien sur la vie privée là-bas. Les mots privé/confidentiel/protégé n’apparaissent même nulle part sur cette page.

    Je recommande également de lire cet article d’Andrew Montalenti sur ces méthodes, en soulignant que “La convention dunder est un espace de noms réservé à l’équipe Python principale” et “Jamais, jamais, inventez vos propres dunders” car “L’équipe Python principale s’est réservé un espace de noms quelque peu moche”. Qui correspond à l’instruction de PEP 8 “Ne jamais inventer [dunder/magic] noms; utilisez-les uniquement comme documenté”. Je pense qu’Andrew est sur place – c’est juste un espace de noms moche de l’équipe de base. Et c’est dans le but de surcharger l’opérateur, pas sur la confidentialité (pas le point d’Andrew mais le mien et celui de la page du modèle de données).

Outre l’article d’Andrew, j’ai également vérifié plusieurs autres sur ces méthodes “magiques”https://advancedweb.fr/”dunder”, et je n’en ai trouvé aucune qui parlait de confidentialité. Ce n’est tout simplement pas de cela qu’il s’agit.

Encore une fois, nous devrions utiliser len(a), ne pas a.__len__(). Mais pas à cause de la vie privée.



Articles Similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.