Syntaxes avancées (car ramassées) en Python

Il y a mille manières d'exprimer ce qu'on désire en langage naturel, et il en va de même avec un langage de programmation.
Certaines fomulations peuvent être clairement plus laconiques que d'autres, et d'aucuns considèrent que c'est démontrer sa maîtrise du langage que de les maîtriser. Toutefois, si jouer à ce petit jeu de la syntaxe ramassée permet d'économiser sur l'écriture, il ne permet certainement pas d'économiser sur la lecture, du moins tant que celui qui doit lire n'a pas appris à écrire pareillement.
Pour aider dans cet apprentissage, cet article évolutif se propose de recenser les syntaxes ramassées qu'un développeur peut rencontrer en lisant du code en Python (version 3.6.4), à condition qu'elles présentent une véritable utilité.
Pour l'heure, on trouvera des explications suivantes... :
L'article sera enrichi au fil des découvertes réalisées au fil de lectures. Ces mises à jour seront signalées.
Mise à jour du 27/04/2020 : Sur signalement de no_one, correction d'une coquille dans un usage fréquent du slicing.
Mise à jour du 09/04/2020 : Sur recommandation de Tchich, mention à l'utilisation de zip () pour extraire les colonnes d'un tableau.
Mise à jour du 08/03/2018 :
  • rappel de la typologie (et corrections !) ;
  • extraire une plage d'éléments d'une séquence (slicing).

Références

Avant toute chose, quelques références essentielles :
  • La documentation de Python contient un didacticiel qui permet de se familiariser avec bon nombre de syntaxes avancées.
  • Pour aller plus loin, Fluent Python de Luciano Ramalho publié par O'Reilly est considéré à juste titre comme incontournable.

Rappel sur les types

La documentation de Python détaille la hiérarchie des types qu'il faut bien connaître pour maîtriser les syntaxes présentées ici. En particulier, il convient de distinguer :
Séquence Immuable Chaîne (string) 'abc'
Tuple (tuples) ('a', 'b', 'c')
Tableau d'octets (bytes) b'abc'
bytes (3)
Labile Liste (list) ['a', 'b', 'c']
Tableau d'octets (bytes array) bytearray (b'abc')
bytearray (3)
Set Ensemble (set) {'a', 'b', 'c'}
set ('abc')
Ensemble figé (frozen set) frozenset ('abc')
Mapping Dictionnaire (dictionnaire) {'a':'X', 'b':'Y', 'c':'Z'}

Regrouper les arguments formels lors de l'appel à une fonction

La syntaxe ne fait qu'exploiter ce qui est évoqué pour déballer les valeurs d'une séquence :
def f (town, country):
	print (town, 'is in', country)
words = 'Paris', 'France'
f (*words)
Paris is in France

Extraire une plage d'une séquence (slicing)

Le slicing permet d'adresser des éléments via une notation particulière :
[start:end:stride]
L'utilisation des crochets vaut quel que soit le type de la séquence, qui est préservé :
t = ('A', 'B', 'C', 'D', 'E', 'F')
t[1:5:2]	# ('B', 'D')
start, end et stride peuvent être ommis, prenant alors des valeurs par défaut :
[start=0:end=len ():stride=1]
L'élément indexé par end n'est jamais retourné.
Noter qu'un : doit au moins être mentionné pour distinguer le slicing d'une simple indexation :
l = ['A', 'B', 'C', 'D', 'E', 'F']
l[1:5:2]	# ['B', 'D']
l[:5:2]		# ['A', 'C', 'E']
l[1::2]		# ['B', 'D', 'F']
l[::2]		# ['A', 'C', 'E']
l[1::]		# ['B', 'C', 'D', 'E', 'F']
l[:5:]		# ['A', 'B', 'C', 'D', 'E']
l[1:5]		# ['B', 'C', 'D', 'E']	
Il est possible d'utiliser des valeurs négatives pour start et end uniquement, -1 servant à indexer le dernier élément :
l[1:-2]		# ['B', 'C', 'D']
l[-5:-2]	# ['B', 'C', 'D']
l[-2:-5]	# [] car une séquence est toujours lue de gauche à droite
l[-5:-1:2]	# ['B', 'D']
Quelques usages fréquents :
l[:]		# Tous les éléments
l[1:]		# Tous les éléments sauf le premier
l[:-1]		# Tous les éléments sauf le dernier
Parlant de slicing, noter qu'il est aussi possible d'utiliser l'ellipse. C'est un objet dont il n'existe qu'une instance, adressée par .... Toutefois, l'ellipse ne s'utilise pas avec les types de base de Python, mais avec un tableau à plusieurs dimensions de NumPy. Elle permet alors de sauter toutes les dimensions qui ne sont explicitement adressées :
import numpy
a = numpy.array ([['A', 'B', 'C', 'D', 'E'], ['F', 'G', 'H', 'I', 'J']])
print (a[...,0::2])
print (a[:-1,...])
[['A' 'C' 'E']
 ['F' 'H' 'J']]
['F', 'G', 'H', 'I', 'J']

Déballer des valeurs d'une séquence (unpacking)

La syntaxe employée s'appuie sur l'unpacking. Il faut mentionner autant de variables qu'il y a d'éléments dans la séquence... :
towns = ['Paris', 'London', 'Brussels']
first, middle, last = towns
print (first)
print (middle)
print (last)
Paris
['London', 'Brussels', 'Helsinki']
Berlin
...à moins de mentionner une variable (une liste) qui regroupera toutes celles qui ne peuvent être affectées à des variables particulières :
towns = ['Paris', 'London', 'Brussels', 'Helsinki', 'Berlin']
first, *middle, last = towns
print (first)
print (middle)
print (last)
Paris
['London', 'Brussels', 'Helsinki']
Berlin
Noter que dans le cas d'un dictionnaire, ce sont les clés et non les valeurs qui sont ainsi extraites :
towns = ['France':'Paris', 'United Kingdom':'London']
first, last = towns
print (first)
print (last)
France
United Kingdom
Ce type de syntaxe fonctionne non seulement avec les séquences par défaut, mais aussi avec celle produites à l'aide d'un générateur :
def f ():
	values = ['Paris', 'London', 'Brussels', 'Helsinki', 'Berlin']
	for i in range (len (values)):
		yield values[i]
first, *middle, last = f ()
Paris
['London', 'Brussels', 'Helsinki']
Berlin

Extraire des colonnes d'une liste

La syntaxe employée mobilise des compréhensions :
points = [['A', 0], ['B', 10], ['C', 20], ['D', 30], ['E', 40]]
X, Y = [d[0] for d in points], [d[1] for d in points]
print (X)
print (Y)
['A', 'B', 'C', 'D', 'E']
[0, 10, 20, 30, 40]
Noter que l'usage des compréhensions n'est pas recommandé, car elles ne sont pas facilement... compréhensibles ! Ainsi, PyCharm propose par défaut de réécrire automatiquement toute compréhension sous forme d'une boucle.
Noter qu'une syntaxe bien plus ramassée est possible avec un tableau de NumPy, les éléments étant adressés notamment par découpage (slicing) :
import numpy

points = [['A', 0], ['B', 10], ['C', 20], ['D', 30], ['E', 40]]
p = numpy.array (points)
X, Y = p[:, 0], p[:, 1]
Mais un lecteur attentif de ce blog, Tchich, pointe très justement une syntaxe encore plus ramassée, qui s'appuie sur la fonction zip () :
points = [['A', 0], ['B', 10], ['C', 20], ['D', 30], ['E', 40]]
X, Y = zip (*points)
Syntaxes avancées (car ramassées) en Python