Cours Python · Module 3: Programmation orientée objet
Type hints - classification précise
Dans cette leçon10
Bon retour ! Darwin ici, avec une leçon sur la précision et la documentation du code.
En biologie, une classification précise est essentielle - « grand animal » n'est pas la même chose que « Panthera leo (lion d'Afrique), mâle, adulte, 190kg ». En Python, les type hints (annotations de type) permettent la même précision dans le code !
1# Imprécis - qu'est-ce que c'est ?
2def count_animals(animals):
3 return len(animals)
4
5# Précis - tout est clair !
6def count_animals(animals: list[str]) -> int:
7 """Compte les animaux dans la liste"""
8 return len(animals)Que sont les type hints ?
Les type hints sont des annotations de type en Python - des informations supplémentaires sur les types des variables, des paramètres de fonction et des valeurs de retour.
IMPORTANT : les type hints sont optionnels et n'affectent PAS l'exécution du programme ! Python les ignore à l'exécution. Ils servent à :
- Documentation - expliquer les intentions du programmeur
- IDE - de meilleures suggestions et l'autocomplétion
- Outils - mypy, pyright peuvent détecter les erreurs de type
- Lisibilité - un code plus facile à comprendre
1# Sans type hints - il faut deviner
2def calculate_density(count, area):
3 return count / area
4
5# Avec type hints - tout est clair !
6def calculate_density(count: int, area: float) -> float:
7 """Calcule la densité de population (individus/km²)"""
8 return count / areaType hints de base
Variables
Tu écris l'annotation d'une variable après deux-points, entre le nom et le signe =. Pour les conteneurs, tu peux aussi indiquer le type des éléments entre crochets :
1# Types de base
2name: str = "Lion"
3age: int = 5
4weight: float = 190.5
5is_dangerous: bool = True
6
7# Types de conteneurs seuls (sans type des éléments)
8animals: list = ["Lion", "Éléphant"]
9habitat_map: dict = {"Lion": "savane"}
10
11# Python 3.9+ - encore mieux avec le type des éléments
12animals: list[str] = ["Lion", "Éléphant", "Girafe"]
13populations: dict[str, int] = {"Lion": 500, "Éléphant": 300}
14coordinates: tuple[float, float] = (51.5, -0.1)
15unique_species: set[str] = {"Lion", "Éléphant"}Fonctions - paramètres et type de retour
Dans une fonction, tu écris le type de chaque paramètre après deux-points et le type du résultat après la flèche ->, avant le deux-points final :
1def greet(name: str) -> str:
2 """Prend un str, renvoie un str"""
3 return f"Bonjour, {name} !"
4
5def add_numbers(a: int, b: int) -> int:
6 """Prend deux int, renvoie un int"""
7 return a + b
8
9def calculate_average(numbers: list[float]) -> float:
10 """Prend une liste de float, renvoie un float"""
11 return sum(numbers) / len(numbers)
12
13def print_info(message: str) -> None:
14 """None signifie « ne renvoie rien »"""
15 print(message)
16 # pas de return ou return NoneTypes du module typing
Optional - peut être None
Optional[str] indique que la fonction renvoie du texte ou None. C'est utile pour une recherche qui peut ne rien trouver :
1from typing import Optional
2
3def find_species(name: str) -> Optional[str]:
4 """
5 Renvoie une espèce ou None si non trouvée
6
7 Optional[str] = str | None
8 """
9 database = {"Lion": "Panthera leo", "Éléphant": "Loxodonta africana"}
10 return database.get(name) # Peut renvoyer un str ou None
11
12result = find_species("Lion") # Optional[str]
13if result is not None:
14 print(f"Trouvé : {result}")Union - l'un de plusieurs types
Union énumère plusieurs types autorisés. L'identifiant d'un animal peut être un nombre ou du texte :
1from typing import Union
2
3def process_id(animal_id: Union[int, str]) -> str:
4 """
5 Accepte int OU str
6
7 Python 3.10+ : int | str
8 """
9 return f"ID : {animal_id}"
10
11process_id(123) # OK - int
12process_id("LEO-45") # OK - strPython 3.10+ : tu peux utiliser | à la place de Union !
1def process_id(animal_id: int | str) -> str:
2 """Syntaxe plus récente - Python 3.10+"""
3 return f"ID : {animal_id}"
4
5# Optional peut aussi s'écrire plus court
6def find_species(name: str) -> str | None:
7 """str | None = Optional[str]"""
8 passList, Dict, Tuple, Set - avec des types spécifiques
Les conteneurs intégrés prennent le type de leurs éléments entre crochets. Un tuple peut avoir un nombre fixe de positions ou un nombre quelconque, noté ... :
1# Python 3.9+ - utilise les minuscules !
2def process_animals(names: list[str]) -> dict[str, int]:
3 """Liste de chaînes → dictionnaire chaîne:int"""
4 return {name: len(name) for name in names}
5
6# Tuple - nombre fixe d'éléments avec des types précisés
7def get_coordinates() -> tuple[float, float]:
8 """Renvoie (latitude, longitude)"""
9 return (51.5074, -0.1278)
10
11# Tuple - nombre variable d'éléments du même type
12def get_observations() -> tuple[int, ...]:
13 """Tuple avec un nombre quelconque d'int"""
14 return (12, 45, 23, 67, 89)
15
16# Set
17def get_unique_habitats() -> set[str]:
18 """Ensemble d'habitats uniques"""
19 return {"savane", "jungle", "montagnes"}Any - n'importe quel type
Any signifie que la valeur peut être de n'importe quel type, donc les outils arrêtent de la vérifier :
1from typing import Any
2
3def log_data(data: Any) -> None:
4 """Accepte n'importe quoi - à utiliser avec parcimonie !"""
5 print(f"Journalisation : {data}")
6
7log_data(123) # OK
8log_data("texte") # OK
9log_data([1, 2, 3]) # OK
10log_data({"key": "val"}) # OKRemarque : n'utilise Any que lorsque tu acceptes vraiment n'importe quel type ! C'est comme renoncer aux type hints.
Type hints pour les classes
Dans une classe, tu annotes les attributs de classe, les paramètres de __init__ et des méthodes, ainsi que les attributs d'instance. Un nom de classe que Python ne connaît pas encore s'écrit entre guillemets, par ex. 'Species' :
1class Species:
2 """Species avec type hints"""
3
4 # Attributs de classe
5 kingdom: str = "Animalia"
6 all_species: list['Species'] = [] # Forward reference
7
8 def __init__(
9 self,
10 scientific_name: str,
11 common_name: str,
12 population: int,
13 endangered: bool = False
14 ) -> None:
15 """Constructeur avec type hints"""
16 self.scientific_name: str = scientific_name
17 self.common_name: str = common_name
18 self.population: int = population
19 self.endangered: bool = endangered
20 self.observations: list[dict[str, Any]] = []
21
22 Species.all_species.append(self)
23
24 def add_observation(
25 self,
26 location: str,
27 count: int,
28 date: str | None = None
29 ) -> None:
30 """Ajoute une observation - type de retour None"""
31 obs: dict[str, str | int] = {
32 "location": location,
33 "count": count
34 }
35 if date:
36 obs["date"] = date
37 self.observations.append(obs)
38
39 def get_population_trend(self) -> float:
40 """Renvoie un float"""
41 if len(self.observations) < 2:
42 return 0.0
43
44 first = self.observations[0]["count"]
45 last = self.observations[-1]["count"]
46 return (last - first) / first * 100
47
48 @classmethod
49 def get_endangered_species(cls) -> list['Species']:
50 """Renvoie une liste de Species - forward reference !"""
51 return [s for s in cls.all_species if s.endangered]
52
53 @staticmethod
54 def is_valid_population(pop: int) -> bool:
55 """Renvoie un bool"""
56 return pop >= 0Type hints avancés
Callable - les fonctions comme types
Quand un paramètre est une fonction, Callable[[Species], bool] décrit ce que cette fonction reçoit et ce qu'elle renvoie :
1from typing import Callable
2
3def apply_filter(
4 species_list: list[Species],
5 filter_func: Callable[[Species], bool]
6) -> list[Species]:
7 """
8 Filtre une liste par une fonction
9
10 Callable[[Species], bool] = fonction prenant un Species, renvoyant un bool
11 """
12 return [s for s in species_list if filter_func(s)]
13
14# Utilisation
15def is_endangered(species: Species) -> bool:
16 return species.endangered
17
18endangered = apply_filter(Species.all_species, is_endangered)TypeAlias - alias pour les types complexes
Tu peux nommer une seule fois un type long et réutiliser ce nom à plusieurs endroits. Un tel alias se crée avec l'annotation TypeAlias :
1from typing import TypeAlias
2
3# Alias pour la lisibilité
4PopulationData: TypeAlias = dict[str, list[int]]
5Coordinates: TypeAlias = tuple[float, float]
6SpeciesDict: TypeAlias = dict[str, Species]
7
8def analyze_populations(data: PopulationData) -> float:
9 """Le type PopulationData est plus lisible que dict[str, list[int]]"""
10 all_counts: list[int] = []
11 for counts in data.values():
12 all_counts.extend(counts)
13 return sum(all_counts) / len(all_counts)
14
15def get_location(name: str) -> Coordinates:
16 """Coordinates indique clairement ce que l'on renvoie"""
17 locations: dict[str, Coordinates] = {
18 "Serengeti": (-2.3333, 34.8333),
19 "Masai Mara": (-1.5, 35.1667)
20 }
21 return locations.get(name, (0.0, 0.0))Generic - classes génériques
Une classe générique reçoit le type de ses éléments en paramètre, comme list[str]. La même pile peut contenir des chaînes ou des nombres, et l'IDE sait quand même ce que tu vas en retirer :
1from typing import Generic, TypeVar
2
3T = TypeVar('T') # Type générique
4
5class Stack(Generic[T]):
6 """Pile générique - peut stocker n'importe quel type"""
7
8 def __init__(self) -> None:
9 self._items: list[T] = []
10
11 def push(self, item: T) -> None:
12 """Ajoute un élément de type T"""
13 self._items.append(item)
14
15 def pop(self) -> T:
16 """Retire et renvoie un élément de type T"""
17 return self._items.pop()
18
19 def is_empty(self) -> bool:
20 return len(self._items) == 0
21
22# Utilisation
23string_stack: Stack[str] = Stack[str]()
24string_stack.push("Lion")
25string_stack.push("Éléphant")
26animal: str = string_stack.pop() # L'IDE sait que c'est un str !
27
28int_stack: Stack[int] = Stack[int]()
29int_stack.push(123)
30int_stack.push(456)
31number: int = int_stack.pop() # L'IDE sait que c'est un int !Exemple Safari - système complet avec type hints
Maintenant tout ensemble : des alias de types, des enums pour le statut de conservation et l'habitat, une classe annotée dans chaque méthode et des fonctions d'aide qui l'utilisent.
1from typing import Optional, TypeAlias
2from datetime import date
3from enum import Enum
4
5# === ALIAS DE TYPE ===
6
7SpeciesID: TypeAlias = str
8Coordinates: TypeAlias = tuple[float, float]
9ObservationData: TypeAlias = dict[str, str | int | date]
10
11# === ENUMS ===
12
13class ConservationStatus(Enum):
14 """Statut de conservation d'une espèce"""
15 EXTINCT = "extinct"
16 EXTINCT_IN_WILD = "extinct_in_wild"
17 CRITICALLY_ENDANGERED = "critically_endangered"
18 ENDANGERED = "endangered"
19 VULNERABLE = "vulnerable"
20 NEAR_THREATENED = "near_threatened"
21 LEAST_CONCERN = "least_concern"
22
23class Habitat(Enum):
24 """Type d'habitat"""
25 SAVANNA = "savanna"
26 JUNGLE = "jungle"
27 MOUNTAINS = "mountains"
28 DESERT = "desert"
29 WETLANDS = "wetlands"
30
31# === CLASSE PRINCIPALE ===
32
33class Species:
34 """
35 Classe Species avec des type hints complets
36
37 Une classification précise comme en biologie !
38 """
39
40 # Attribut de classe
41 _registry: dict[SpeciesID, 'Species'] = {}
42
43 def __init__(
44 self,
45 scientific_name: str,
46 common_name: str,
47 population: int,
48 habitat: Habitat,
49 status: ConservationStatus,
50 dangerous: bool = False
51 ) -> None:
52 """
53 Initialise l'espèce
54
55 Args:
56 scientific_name: Nom scientifique (par ex. "Panthera leo")
57 common_name: Nom courant (par ex. "Lion")
58 population: Nombre d'individus
59 habitat: Type d'habitat (enum)
60 status: Statut de conservation (enum)
61 dangerous: Dangereuse pour l'humain ou non
62 """
63 self.id: SpeciesID = scientific_name
64 self.scientific_name: str = scientific_name
65 self.common_name: str = common_name
66 self.population: int = population
67 self.habitat: Habitat = habitat
68 self.status: ConservationStatus = status
69 self.dangerous: bool = dangerous
70
71 # Liste des observations
72 self.observations: list[ObservationData] = []
73
74 # Emplacements
75 self.locations: set[Coordinates] = set()
76
77 # Enregistrement
78 Species._registry[self.id] = self
79
80 def add_observation(
81 self,
82 location: str,
83 coordinates: Coordinates,
84 count: int,
85 observation_date: date | None = None
86 ) -> None:
87 """
88 Ajoute une observation d'espèce
89
90 Args:
91 location: Nom de l'emplacement
92 coordinates: (latitude, longitude)
93 count: Nombre d'individus observés
94 observation_date: Date de l'observation (optionnelle)
95 """
96 obs: ObservationData = {
97 "location": location,
98 "count": count,
99 "date": observation_date or date.today()
100 }
101 self.observations.append(obs)
102 self.locations.add(coordinates)
103
104 def get_total_observed(self) -> int:
105 """Renvoie le nombre total d'individus observés"""
106 return sum(
107 int(obs["count"])
108 for obs in self.observations
109 )
110
111 def get_observation_locations(self) -> list[str]:
112 """Renvoie la liste des emplacements d'observation uniques"""
113 locations: set[str] = {
114 str(obs["location"])
115 for obs in self.observations
116 }
117 return sorted(locations)
118
119 def is_threatened(self) -> bool:
120 """Vérifie si l'espèce est menacée"""
121 threatened_statuses: set[ConservationStatus] = {
122 ConservationStatus.CRITICALLY_ENDANGERED,
123 ConservationStatus.ENDANGERED,
124 ConservationStatus.VULNERABLE
125 }
126 return self.status in threatened_statuses
127
128 def get_risk_assessment(self) -> dict[str, str | int | bool]:
129 """
130 Évaluation du risque pour l'expédition
131
132 Returns:
133 Dictionnaire avec l'évaluation du risque
134 """
135 risk_level: int = 0
136
137 if self.dangerous:
138 risk_level += 5
139
140 if self.population < 100:
141 risk_level += 3 # Rare - difficile à trouver
142
143 if self.habitat == Habitat.JUNGLE:
144 risk_level += 2 # Terrain difficile
145
146 return {
147 "species": self.common_name,
148 "risk_level": min(10, risk_level),
149 "dangerous": self.dangerous,
150 "rare": self.population < 100,
151 "recommendation": "Extrême prudence" if risk_level >= 7 else "Protocole normal"
152 }
153
154 @classmethod
155 def get_by_id(cls, species_id: SpeciesID) -> Optional['Species']:
156 """
157 Trouve une espèce par son ID
158
159 Returns:
160 Species ou None si non trouvée
161 """
162 return cls._registry.get(species_id)
163
164 @classmethod
165 def get_by_habitat(cls, habitat: Habitat) -> list['Species']:
166 """Renvoie toutes les espèces d'un habitat donné"""
167 return [
168 species for species in cls._registry.values()
169 if species.habitat == habitat
170 ]
171
172 @classmethod
173 def get_endangered(cls) -> list['Species']:
174 """Renvoie toutes les espèces menacées"""
175 return [
176 species for species in cls._registry.values()
177 if species.is_threatened()
178 ]
179
180 @staticmethod
181 def calculate_biodiversity_index(
182 species_list: list['Species']
183 ) -> float:
184 """
185 Calcule l'indice de biodiversité de Simpson
186
187 Args:
188 species_list: Liste des espèces à analyser
189
190 Returns:
191 Indice (0.0 - 1.0, plus élevé = plus grande diversité)
192 """
193 if not species_list:
194 return 0.0
195
196 total: int = sum(s.population for s in species_list)
197 if total == 0:
198 return 0.0
199
200 sum_squares: float = sum(
201 (s.population / total) ** 2
202 for s in species_list
203 )
204
205 return 1.0 - sum_squares
206
207 def __str__(self) -> str:
208 return f"{self.common_name} ({self.population} individus)"
209
210 def __repr__(self) -> str:
211 return (f"Species(scientific_name='{self.scientific_name}', "
212 f"population={self.population}, "
213 f"habitat={self.habitat.value})")
214
215# === FONCTIONS UTILITAIRES ===
216
217def generate_report(species: Species) -> str:
218 """
219 Génère un rapport d'espèce
220
221 Args:
222 species: Espèce sur laquelle faire le rapport
223
224 Returns:
225 Rapport textuel formaté
226 """
227 threatened: str = "OUI" if species.is_threatened() else "Non"
228 risk: dict[str, str | int | bool] = species.get_risk_assessment()
229
230 report: str = f"""
231╔══════════════════════════════════════════════════╗
232 {species.common_name.upper()}
233╠══════════════════════════════════════════════════╣
234 Nom scientifique : {species.scientific_name}
235 Population : {species.population} individus
236 Habitat : {species.habitat.value}
237 Statut : {species.status.value}
238 Menacée : {threatened}
239 Dangereuse : {"OUI" if species.dangerous else "Non"}
240 Niveau de risque : {risk['risk_level']}/10
241 Observations : {len(species.observations)}
242 Emplacements : {', '.join(species.get_observation_locations()) or 'Aucun'}
243╚══════════════════════════════════════════════════╝
244 """.strip()
245
246 return report
247
248def filter_by_population(
249 species_list: list[Species],
250 min_pop: int,
251 max_pop: int | None = None
252) -> list[Species]:
253 """
254 Filtre les espèces par population
255
256 Args:
257 species_list: Liste des espèces
258 min_pop: Population minimale
259 max_pop: Population maximale (optionnelle)
260
261 Returns:
262 Liste filtrée
263 """
264 filtered: list[Species] = [
265 s for s in species_list
266 if s.population >= min_pop
267 ]
268
269 if max_pop is not None:
270 filtered = [s for s in filtered if s.population <= max_pop]
271
272 return filtered
273
274# === DÉMONSTRATION ===
275
276print("=== CATALOGUE D'ESPÈCES AVEC TYPE HINTS ===\n")
277
278# Création des espèces
279lion = Species(
280 scientific_name="Panthera leo",
281 common_name="Lion",
282 population=120,
283 habitat=Habitat.SAVANNA,
284 status=ConservationStatus.VULNERABLE,
285 dangerous=True
286)
287
288rhino = Species(
289 scientific_name="Diceros bicornis",
290 common_name="Rhinocéros noir",
291 population=45,
292 habitat=Habitat.SAVANNA,
293 status=ConservationStatus.CRITICALLY_ENDANGERED,
294 dangerous=True
295)
296
297elephant = Species(
298 scientific_name="Loxodonta africana",
299 common_name="Éléphant d'Afrique",
300 population=450,
301 habitat=Habitat.SAVANNA,
302 status=ConservationStatus.ENDANGERED,
303 dangerous=False
304)
305
306# Ajout d'observations
307lion.add_observation("Serengeti", (-2.3333, 34.8333), 12)
308lion.add_observation("Masai Mara", (-1.5, 35.1667), 8)
309rhino.add_observation("Ngorongoro", (-3.1792, 35.5500), 3)
310elephant.add_observation("Amboseli", (-2.6527, 37.2606), 35)
311
312# Rapports
313print(generate_report(lion))
314print()
315print(generate_report(rhino))
316
317# Filtrage
318print("\n=== ESPÈCES MENACÉES ===")
319endangered: list[Species] = Species.get_endangered()
320for species in endangered:
321 print(f" {species.common_name} : {species.status.value}")
322
323# Filtre par population
324print("\n=== ESPÈCES AVEC POPULATION < 100 ===")
325rare: list[Species] = filter_by_population(
326 list(Species._registry.values()),
327 min_pop=0,
328 max_pop=99
329)
330for species in rare:
331 print(f" - {species.common_name} : {species.population} individus")
332
333# Indice de biodiversité
334biodiversity: float = Species.calculate_biodiversity_index(
335 list(Species._registry.values())
336)
337print(f"\nIndice de biodiversité : {biodiversity:.3f}")Outils de vérification des types
mypy - vérification statique des types
L'outil mypy lit les annotations et vérifie les types avant que tu lances le programme. Tu l'installes avec pip et tu lui indiques le fichier à vérifier :
1# Installation
2pip install mypy
3
4# Vérifier un fichier
5mypy script.pyPython exécute ce code sans erreur, mais mypy le bloque :
1def add(a: int, b: int) -> int:
2 return a + b
3
4result: str = add(5, 10) # mypy : Incompatible types in assignmentLa fonction renvoie int, mais la variable est déclarée en str. Python ne vérifie pas les annotations pendant l'exécution, donc seul mypy ou ton éditeur repérera cette erreur.
Vérification des types dans les IDE
Les IDE modernes (VS Code, PyCharm) vérifient automatiquement les type hints :
- Autocomplétion - suggestions basées sur les types
- Détection d'erreurs - détection des erreurs de type
- Refactorisation - des changements plus sûrs
- Documentation - aperçu rapide de la signature
Bonnes pratiques des type hints
1. Ajoute toujours des type hints aux API publiques
Les méthodes utilisées par le reste du programme sont un contrat avec les autres programmeurs. Les annotations disent tout de suite ce que ces méthodes attendent et ce qu'elles renvoient :
1# Bon - API publique avec type hints
2class Species:
3 def add_observation(self, location: str, count: int) -> None:
4 pass
5
6# Mauvais - API publique sans types
7class Species:
8 def add_observation(self, location, count):
9 pass2. Utilise des types spécifiques plutôt que Any
Any n'apprend rien au lecteur et ne vérifie rien. Si tu sais quelles données arrivent, écris-le dans l'annotation :
1# Mauvais - trop générique
2def process_data(data: Any) -> Any:
3 return data
4
5# Bon - types spécifiques
6def process_data(data: dict[str, int]) -> list[int]:
7 return list(data.values())3. Utilise Optional pour les valeurs None
Si une fonction peut renvoyer None, indique-le dans l'annotation. Ainsi, quiconque l'appelle sait qu'il doit vérifier le résultat :
1# Bon - clairement précisé
2def find_species(name: str) -> Species | None:
3 pass
4
5# Trompeur - renvoie-t-elle None ?
6def find_species(name: str) -> Species:
7 pass4. Documente les types complexes avec TypeAlias
Quand le même type complexe apparaît à plusieurs endroits, donne-lui un nom. La signature de la fonction devient plus courte et son sens saute aux yeux :
1# Lisible
2ObservationData: TypeAlias = dict[str, str | int | date]
3
4def add_observation(data: ObservationData) -> None:
5 pass
6
7# Illisible
8def add_observation(data: dict[str, str | int | date]) -> None:
9 passRésumé
Dans cette leçon, tu as appris :
- Ce que sont les type hints et pourquoi les utiliser
- Les annotations de base : int, str, float, bool, list, dict
- Optional et Union (et la syntaxe
|en Python 3.10+) - Les type hints pour les fonctions (paramètres et types de retour)
- Les type hints pour les classes et les méthodes
- Les techniques avancées : Callable, TypeAlias, Generic
- Les Enums pour des valeurs sûres
- Des outils comme mypy
- Les bonnes pratiques des type hints
Point de contrôle
Avant de passer à la suite :
- Tu comprends la différence entre les type hints et le comportement à l'exécution
- Tu sais ajouter des type hints aux fonctions
- Tu connais Optional et Union (et
|) - Tu sais utiliser list[T], dict[K, V]
- Tu comprends TypeAlias et Generic
Analogie Safari : les type hints sont comme une classification biologique précise - au lieu de « grand félin » on dit « Panthera leo, mâle, 5 ans, 190kg » - tout est clair !
Dans la prochaine leçon, Darwin t'apprendra les décorateurs - un outil puissant pour modifier les comportements des fonctions et des classes !
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. En Python, les type hints sont :
2. Comment annote-t-on le type de retour d'une fonction ?
Tâches pratiques dans le jeu
- Tri vertical
Range les éléments typiques d'une définition de classe :
- Éditeur de code
Crée une classe Animal avec des type hints : name: str, age: int, weight: float
- Tri horizontal
Range l'en-tête d'une méthode speak qui renvoie du texte (type hint str) :