Expressions

Expressions multilignes

Une expression peut traverser un saut de ligne lorsqu'elle est entourée de parenthèses. À l'intérieur, les newlines et l'indentation ne terminent pas l'expression :

total = (
    price
    * quantity
    * (1 + tax)
)

valid = (
    x > 0
    and x < 100
    and enabled
)

Sans parenthèses, un newline termine l'expression :

result = 1 + 2 +
    3 + 4  # erreur de parsing

result = (
    1 + 2 +
    3 + 4
)
# ⇒ 10

Les collections et les blocs acceptent déjà les newlines, sans parenthèses supplémentaires.

Appels et lambdas

Un saut de ligne est accepté après (, entre les arguments ou paramètres, et avant ) :

server = create_server(
    "192.168.1.1",
    3000,
    ssl=True,
    workers=4,
)

transform = (
    x,
    y,
    scale=1.0,
) => {
    (x + y) * scale
}

Une virgule finale est facultative. Les arguments positionnels restent placés avant les arguments nommés, quelle que soit leur ligne.

Les parenthèses suspendent la fin de ligne. Le parser attend la fermeture avant de reprendre le temps normal.

Opérateurs

Arithmétique

10 + 5 - 2 * 3
# ⇒ 9

10 / 4
# ⇒ 2.5

10 // 4
# ⇒ 2

-10 // 4
# ⇒ -3

17 % 5
# ⇒ 2

2 ** 10
# ⇒ 1024

/ produit toujours un float, même lorsque la division tombe juste. // effectue une division plancher : elle arrondit vers le bas, pas vers zéro.

L'exponentiation lie plus fortement que l'opérateur unaire :

-2 ** 2
# ⇒ -4

(-2) ** 2
# ⇒ 4

Une base négative élevée à une puissance fractionnaire n'a pas de racine réelle : le résultat est la racine principale, donc un complexe.

(-8) ** (1/3)
# ⇒ (1+1.732050807568877j)

Les entiers ont une précision arbitraire. La VM promeut automatiquement un résultat qui dépasse sa représentation compacte et le redescend lorsque la valeur revient dans sa plage ; ce changement de stockage n'affecte pas le type visible. Les décalages suivent la même règle : 1 << 64 vaut 18446744073709551616, pas 1. Un compte de décalage négatif est un ValueError ; >> sature au-delà de la largeur (0, ou -1 pour un négatif) là où <<, qui doit matérialiser chaque bit déplacé, refuse un compte que rien ne pourrait allouer.

Comparaison, appartenance et identité

5 == 5
5 != 3
3 < 5
1 < x < 10

2 in list(1, 2, 3)
"x" not in dict(a=1, b=2)
"cat" in "catnip"

value is None
value is not None

== compare les valeurs. is compare l'identité : deux noms liés au même objet satisfont a is b, même si le type définit une égalité personnalisée. Les comparaisons peuvent être chaînées ; chaque opérande intermédiaire n'est évalué qu'une fois.

Les structures peuvent surcharger les comparaisons et l'appartenance. Voir Structures.

Logique

ready = loaded and valid
fallback = cached or computed
disabled = not enabled

and et or court-circuitent : la partie droite n'est évaluée que si elle peut changer le résultat. En Catnip, ils retournent un booléen, pas l'un de leurs opérandes. La table complète de truthiness se trouve dans Types de données.

Nil-coalescing

?? retourne son opérande gauche lorsqu'il n'est pas None; sinon, il évalue et retourne la partie droite :

42 ?? 0
# ⇒ 42

None ?? 0
# ⇒ 0

None ?? None ?? 3
# ⇒ 3

False ?? True
# ⇒ False

L'opérateur teste uniquement None, pas la truthiness. Il court-circuite, mais ne capture pas les erreurs produites pendant l'évaluation de la partie gauche : mapping["missing"] ?? default lève d'abord KeyError.

Opérateurs bitwise

0xFF & 0x0F
# ⇒ 15

0xF0 | 0x0F
# ⇒ 255

0xFF ^ 0x0F
# ⇒ 240

~0
# ⇒ -1

Sur les entiers, &, |, ^, <<, >> et ~ opèrent bit par bit. Sinon, ils délèguent au protocole de l'opérande : un tableau NumPy peut les appliquer élément par élément et une structure peut définir op &, op |, op ^ ou op ~.

Priorité

Les parenthèses restent la forme explicite lorsque plusieurs familles se rencontrent. Sans parenthèses, les groupes principaux vont du plus fort au plus faible :

Priorité Opérateurs
Forte **
+x, -x, ~x
*, /, //, %
+, -
<<, >>, puis &, ^, \|
comparaisons, in, is
not, puis and, puis or
Faible ??

** est associative à droite : 2 ** 3 ** 2 vaut 2 ** (3 ** 2). Une comparaison chaînée n'est pas une suite de booléens comparés entre eux : a < b < c évalue b une fois et exige les deux relations.

2 + 3 * 4
# ⇒ 14

(2 + 3) * 4
# ⇒ 20

True or None ?? False
# ⇒ True

Quand la lecture du code dépend de plusieurs lignes du tableau, des parenthèses rendent le contrat local et évitent de faire reposer une modification sur un souvenir de priorité.

Attributs et chaînage

. lit un attribut ou appelle une méthode. Les accès peuvent être chaînés tant que chaque étape fournit l'objet attendu par la suivante :

host = config.database.host
result = client.connect(timeout=5).receive().decode()

Une assignation peut cibler le dernier attribut :

config.database.host = "localhost"

Une erreur à une étape interrompt la chaîne ; Catnip n'ajoute pas de propagation implicite de None.

L'appel de méthode lie automatiquement l'instance à son premier paramètre self. Lire le même attribut sans parenthèses retourne la méthode liée, qui peut ensuite être stockée ou passée en callback :

send = client.send
send(message)

Attributs d'introspection

Les attributs qui mènent d'une valeur au graphe des types ou à une portée ne se lisent ni ne s'écrivent depuis Catnip. La forme est refusée à la lecture du fichier, avec sa position :

[].__class__     # SyntaxError: special attribute '__class__' is not accessible from Catnip

La liste : __bases__, __builtins__, __class__, __closure__, __code__, __dict__, __func__, __getattribute__, __globals__, __import__, __init__, __loader__, __module__, __mro__, __new__, __objclass__, __reduce__, __reduce_ex__, __self__, __spec__, __subclasses__, __wrapped__.

La raison est ce qu'un objet Python donne quand on le remonte : [].__class__.__bases__[0].__subclasses__() part d'un littéral de liste et atteint toute classe chargée dans l'interpréteur, puis __init__.__globals__["__builtins__"] atteint open, exec et __import__. La chaîne commence dans le langage, donc aucun choix de globals ne la ferme — seul le refus des maillons la coupe.

Un attribut qui s'écrit avec quatre soulignements n'avait pas prévu d'être lu par toi.

Pour lire un nom de type, typeof(x) remplace x.__class__.__name__.

Ces noms ne se déclarent pas non plus — champ ou méthode de structure, méthode de trait, membre d'énumération, variante d'union ou champ de variante. Le membre serait construit sans jamais pouvoir être relu :

struct P { __class__ }     # SyntaxError: cannot declare '__class__': the name would not be readable back

Les autres noms encadrés de soulignements restent accessibles, parce qu'ils désignent des protocoles et non un chemin de navigation : x.__len__(), col.__eq__(other), shape.__geo_interface__. Le runtime, lui, continue d'appeler les siens pour son compte — with invoque __enter__ et __exit__, une boucle for invoque __iter__, la troncature de vérité invoque __bool__ — et ces appels-là ne passent pas par la forme écrite.

Indexation

object[index] transmet l'index au protocole du type :

numbers = list(10, 20, 30, 40)
numbers[0]
# ⇒ 10

numbers[-1]
# ⇒ 40

person = dict(name="Alice", age=30)
person["name"]
# ⇒ "Alice"

"catnip"[2]
# ⇒ "t"

L'index peut être une expression. Un index négatif compte depuis la fin pour les séquences qui suivent la convention Python.

Une assignation indexée utilise le protocole symétrique du type :

numbers[1] = 99
person["age"] = 31

Elle modifie l'objet indexé ; elle ne crée pas une nouvelle liaison pour numbers ou person. Les chaînes et tuples refusent cette opération parce qu'ils sont immuables.

Slicing

Un slice utilise [start:stop:step]. Chaque composant est facultatif ; start est inclus et stop exclu :

values = list(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

values[2:6]
# ⇒ [2, 3, 4, 5]

values[:3]
# ⇒ [0, 1, 2]

values[7:]
# ⇒ [7, 8, 9]

values[:]
# ⇒ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

step choisit l'intervalle entre deux éléments :

values[::2]
# ⇒ [0, 2, 4, 6, 8]

values[1:8:2]
# ⇒ [1, 3, 5, 7]

Les bornes négatives comptent depuis la fin. Un pas négatif inverse le sens du parcours :

values[-3:]
# ⇒ [7, 8, 9]

values[::-1]
# ⇒ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

"kayak" == "kayak"[::-1]
# ⇒ True

Une borne hors limites est ajustée et peut produire une séquence vide. Un pas égal à zéro lève ValueError.

values[20:30]
# ⇒ []

values[::0]  # ValueError

Le slicing fonctionne sur les listes, chaînes et objets hôtes qui acceptent un objet slice. Il préserve le type de séquence selon le protocole de cet objet.

Notation membre .[...]

La notation .[start:stop:step] exprime le même slice sous forme de membre. Elle permet de poursuivre directement une expression ou un broadcast :

(list(1, 2, 3) + list(4, 5, 6)).[1:5]
# ⇒ [2, 3, 4, 5]

list(10, 20, 30, 40).[:3].[* 2]
# ⇒ [20, 40, 60]

data.[:] et data[:] ont la même sémantique. Le point ne change ni les bornes ni la copie ; il rend la chaîne d'opérations explicite.

Accès imbriqué

Deux paires de crochets effectuent deux accès successifs :

matrix = list(
    list(1, 2, 3),
    list(4, 5, 6),
)

matrix[1][2]
# ⇒ 6

matrix[0:2][1]
# ⇒ [4, 5, 6]

Le premier résultat doit donc lui-même être indexable.

Indexation multiple

Plusieurs positions séparées par des virgules construisent une seule clé tuple. object[a, b] équivaut à object[(a, b)] et appelle une fois __getitem__((a, b)). C'est la forme utilisée par NumPy et les objets N-dimensionnels :

numpy = import("numpy")
array = numpy.array(list(
    list(1, 2, 3),
    list(4, 5, 6),
))

array[1, 2]
# ⇒ 6

array[0:2, 1]
# ⇒ [2, 5]

array[1, 2] = 99

Chaque position accepte une expression ou un slice. Un seul index n'est jamais enveloppé dans un tuple : object[index] reste un accès simple.

object[a, b] et object[a][b] ne sont donc pas interchangeables. La première forme transmet (a, b) en un appel ; la seconde transmet a, puis b au résultat.

Dans les crochets d'accès, la virgule décrit une coordonnée. Deux crochets successifs décrivent un trajet.

Voir aussi