C'est une nouvelle sélection de trucs et astuces sur Python et la programmation de mon canal Telegram @pythonetc.
Publications précédentes .
Structures comparant
Parfois, vous souhaitez comparer des structures complexes dans des tests en ignorant certaines valeurs. Habituellement, cela peut être fait en comparant des valeurs particulières avec la structure:
>>> d = dict(a=1, b=2, c=3) >>> assert d['a'] == 1 >>> assert d['c'] == 3
Cependant, vous pouvez créer une valeur spéciale qui indique qu'elle est égale à toute autre valeur:
>>> assert d == dict(a=1, b=ANY, c=3)
Cela peut être facilement fait en définissant la méthode
__eq__
:
>>> class AnyClass: ... def __eq__(self, another): ... return True ... >>> ANY = AnyClass()
sys.stdout
est un wrapper qui vous permet d'écrire des chaînes au lieu d'octets bruts. La chaîne est encodée automatiquement à l'aide de
sys.stdout.encoding
:
>>> _ = sys.stdout.write('Straße\n') Straße >>> sys.stdout.encoding 'UTF-8'
sys.stdout.encoding
est en lecture seule et est égal au codage par défaut Python, qui peut être modifié en définissant la variable d'environnement
PYTHONIOENCODING
:
$ PYTHONIOENCODING=cp1251 python3 Python 3.6.6 (default, Aug 13 2018, 18:24:23) [GCC 4.8.5 20150623 (Red Hat 4.8.5-28)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> sys.stdout.encoding 'cp1251'
Si vous souhaitez écrire des octets dans
stdout
vous pouvez contourner le codage automatique en accédant au tampon
sys.stdout.buffer
avec
sys.stdout.buffer
:
>>> sys.stdout <_io.TextIOWrapper name='<stdout>' mode='w' encoding='cp1251'> >>> sys.stdout.buffer <_io.BufferedWriter name='<stdout>'> >>> _ = sys.stdout.buffer.write(b'Stra\xc3\x9fe\n') Straße
sys.stdout.buffer
est également un wrapper qui effectue la mise en mémoire tampon pour vous. Il peut être contourné en accédant au gestionnaire de fichiers bruts avec
sys.stdout.buffer.raw
:
>>> _ = sys.stdout.buffer.raw.write(b'Stra\xc3\x9fe') Straße
Ellipsis constant
Python a une très courte liste de constantes intégrées. L'un d'eux est
Ellipsis
qui peut également être écrit comme
...
Cette constante n'a pas de signification particulière pour l'interpréteur mais est utilisée dans des endroits où une telle syntaxe semble appropriée.
numpy
supporte
Ellipsis
comme argument
__getitem__
, par exemple
x[...]
renvoie tous les éléments de
x
.
PEP 484 définit une signification supplémentaire:
Callable[..., type]
est un moyen de définir un type de callables sans type d'argument spécifié.
Enfin, vous pouvez utiliser
...
pour indiquer que la fonction n'est pas encore implémentée. Ceci est un code Python complètement valide:
def x(): ...
Cependant, en Python 2, les
Ellipsis
ne peuvent pas être écrits comme
...
La seule exception est
a[...]
qui signifie
a[Ellpsis]
.
Toutes les syntaxes suivantes sont valides pour Python 3, mais seule la première ligne est valide pour Python 2:
a[...] a[...:2:...] [..., ...] {...:...} a = ... ... is ... def a(x=...): ...
Réimportation des modules
Les modules déjà importés ne seront pas chargés à nouveau.
import foo
ne fait rien. Cependant, il s'est avéré utile de réimporter des modules tout en travaillant dans un environnement interactif. La bonne façon de le faire dans Python 3.4+ est d'utiliser
importlib
:
In [1]: import importlib In [2]: with open('foo.py', 'w') as f: ...: f.write('a = 1') ...: In [3]: import foo In [4]: foo.a Out[4]: 1 In [5]: with open('foo.py', 'w') as f: ...: f.write('a = 2') ...: In [6]: foo.a Out[6]: 1 In [7]: import foo In [8]: foo.a Out[8]: 1 In [9]: importlib.reload(foo) Out[9]: <module 'foo' from '/home/v.pushtaev/foo.py'> In [10]: foo.a Out[10]: 2
ipython
possède également l'extension de
autoreload
automatique qui réimporte automatiquement les modules si nécessaire:
In [1]: %load_ext autoreload In [2]: %autoreload 2 In [3]: with open('foo.py', 'w') as f: ...: f.write('print("LOADED"); a=1') ...: In [4]: import foo LOADED In [5]: foo.a Out[5]: 1 In [6]: with open('foo.py', 'w') as f: ...: f.write('print("LOADED"); a=2') ...: In [7]: import foo LOADED In [8]: foo.a Out[8]: 2 In [9]: with open('foo.py', 'w') as f: ...: f.write('print("LOADED"); a=3') ...: In [10]: foo.a LOADED Out[10]: 3
\ G
Dans certaines langues, vous pouvez utiliser l'assertion
\G
Il correspond à la position où le match précédent est terminé. Cela permet d'écrire des automates finis qui parcourent la chaîne mot par mot (où le mot est défini par l'expression régulière).
Cependant, il n'y a rien de tel en Python. La solution de contournement appropriée consiste à suivre manuellement la position et à passer la sous-chaîne aux fonctions d'expression régulière:
import re import json text = '<a><b>foo</b><c>bar</c></a><z>bar</z>' regex = '^(?:<([az]+)>|</([az]+)>|([az]+))' stack = [] tree = [] pos = 0 while len(text) > pos: error = f'Error at {text[pos:]}' found = re.search(regex, text[pos:]) assert found, error pos += len(found[0]) start, stop, data = found.groups() if start: tree.append(dict( tag=start, children=[], )) stack.append(tree) tree = tree[-1]['children'] elif stop: tree = stack.pop() assert tree[-1]['tag'] == stop, error if not tree[-1]['children']: tree[-1].pop('children') elif data: stack[-1][-1]['data'] = data print(json.dumps(tree, indent=4))
Dans l'exemple précédent, nous pouvons gagner du temps en évitant de couper la chaîne encore et encore mais en demandant au module re de rechercher à partir d'une position différente à la place.
Cela nécessite quelques changements. Tout d'abord,
re.search
ne prend pas en charge la recherche à partir d'une position personnalisée, nous devons donc compiler l'expression régulière manuellement. Deuxièmement,
^
signifie le vrai début de la chaîne, pas la position où la recherche a commencé, nous devons donc vérifier manuellement que la correspondance s'est produite à la même position.
import re import json text = '<a><b>foo</b><c>bar</c></a><z>bar</z>' * 10 def print_tree(tree): print(json.dumps(tree, indent=4)) def xml_to_tree_slow(text): regex = '^(?:<([az]+)>|</([az]+)>|([az]+))' stack = [] tree = [] pos = 0 while len(text) > pos: error = f'Error at {text[pos:]}' found = re.search(regex, text[pos:]) assert found, error pos += len(found[0]) start, stop, data = found.groups() if start: tree.append(dict( tag=start, children=[], )) stack.append(tree) tree = tree[-1]['children'] elif stop: tree = stack.pop() assert tree[-1]['tag'] == stop, error if not tree[-1]['children']: tree[-1].pop('children') elif data: stack[-1][-1]['data'] = data def xml_to_tree_slow(text): regex = '^(?:<([az]+)>|</([az]+)>|([az]+))' stack = [] tree = [] pos = 0 while len(text) > pos: error = f'Error at {text[pos:]}' found = re.search(regex, text[pos:]) assert found, error pos += len(found[0]) start, stop, data = found.groups() if start: tree.append(dict( tag=start, children=[], )) stack.append(tree) tree = tree[-1]['children'] elif stop: tree = stack.pop() assert tree[-1]['tag'] == stop, error if not tree[-1]['children']: tree[-1].pop('children') elif data: stack[-1][-1]['data'] = data return tree _regex = re.compile('(?:<([az]+)>|</([az]+)>|([az]+))') def _error_message(text, pos): return text[pos:] def xml_to_tree_fast(text): stack = [] tree = [] pos = 0 while len(text) > pos: error = f'Error at {text[pos:]}' found = _regex.search(text, pos=pos) begin, end = found.span(0) assert begin == pos, _error_message(text, pos) assert found, _error_message(text, pos) pos += len(found[0]) start, stop, data = found.groups() if start: tree.append(dict( tag=start, children=[], )) stack.append(tree) tree = tree[-1]['children'] elif stop: tree = stack.pop() assert tree[-1]['tag'] == stop, _error_message(text, pos) if not tree[-1]['children']: tree[-1].pop('children') elif data: stack[-1][-1]['data'] = data return tree print_tree(xml_to_tree_fast(text))
Résultat:
In [1]: from example import * In [2]: %timeit xml_to_tree_slow(text) 356 µs ± 16.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) In [3]: %timeit xml_to_tree_fast(text) 294 µs ± 6.15 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Fonction ronde
Le message d'aujourd'hui est écrit par orsinium , l'auteur de @itgram_channel.La fonction d'
round
arrondit un nombre à une précision donnée en chiffres décimaux.
>>> round(1.2) 1 >>> round(1.8) 2 >>> round(1.228, 1) 1.2
Vous pouvez également configurer une précision négative:
>>> round(413.77, -1) 410.0 >>> round(413.77, -2) 400.0
round
renvoie la valeur du type de numéro d'entrée:
>>> type(round(2, 1)) <class 'int'> >>> type(round(2.0, 1)) <class 'float'> >>> type(round(Decimal(2), 1)) <class 'decimal.Decimal'> >>> type(round(Fraction(2), 1)) <class 'fractions.Fraction'>
Pour vos propres classes, vous pouvez définir un traitement circulaire avec la méthode
__round__
:
>>> class Number(int): ... def __round__(self, p=-1000): ... return p ... >>> round(Number(2)) -1000 >>> round(Number(2), -2) -2
Les valeurs sont arrondies au multiple de
10 ** (-precision)
le plus proche
10 ** (-precision)
. Par exemple, pour
precision=1
valeur sera arrondie au multiple de 0,1:
round(0.63, 1)
renvoie
0.6
. Si deux multiples sont également proches, l'arrondi est effectué vers le choix pair:
>>> round(0.5) 0 >>> round(1.5) 2
Parfois, l'arrondi des flotteurs peut être un peu surprenant:
>>> round(2.85, 1) 2.9
En effet, la plupart des fractions décimales ne peuvent pas être représentées exactement comme un flottant (https://docs.python.org/3.7/tutorial/floatingpoint.html):
>>> format(2.85, '.64f') '2.8500000000000000888178419700125232338905334472656250000000000000'
Si vous voulez arrondir la moitié, vous pouvez utiliser
decimal.Decimal
.
>>> from decimal import Decimal, ROUND_HALF_UP >>> Decimal(1.5).quantize(0, ROUND_HALF_UP) Decimal('2') >>> Decimal(2.85).quantize(Decimal('1.0'), ROUND_HALF_UP) Decimal('2.9') >>> Decimal(2.84).quantize(Decimal('1.0'), ROUND_HALF_UP) Decimal('2.8')