E un impozit pe care îl plătești în orice limbaj fiindcă nu cunoști biblioteca standard suficient de bine. În Python apare cel mai des în două locuri: oameni care scriu douăzeci de linii de jonglat cu dicționare, când Counter sau defaultdict ar fi rezolvat în două, și oameni care scriu metode __init__ de patru linii pe care @dataclass le-ar fi generat pentru ei.
Lecția asta e despre structurile mici de date. Niciuna nu e palpitantă. Toate sunt timp pe care nu-l mai pierzi cu boilerplate.
Counter: numararea de lucruri hashable
from collections import Counter
words: list[str] = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts: Counter[str] = Counter(words)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
counts.most_common(2) # [('apple', 3), ('banana', 2)]
counts["apple"] # 3
counts["mango"] # 0 — never raises KeyError
Counter e o subclasă de dict care pune implicit 0 pentru cheile care lipsesc. Îi dai orice iterabil de elemente hashable și primești înapoi frecvența fiecăruia. Metoda most_common(n) e feature-ul ucigaș: sortat, descrescător, primele n. Niciodată nu mai scrii sorted(items, key=lambda x: -x[1])[:n].
Exemplu real: să afli ce IP-uri lovesc cel mai des endpoint-ul de erori:
from collections import Counter
from pathlib import Path
errors: Counter[str] = Counter()
for line in Path("access.log").read_text().splitlines():
if " 500 " in line:
ip: str = line.split()[0]
errors[ip] += 1
print(errors.most_common(10))
Counter-ele se compun și cu operatori aritmetici:
a: Counter[str] = Counter("aabbc")
b: Counter[str] = Counter("abbcc")
a + b # Counter({'b': 4, 'c': 3, 'a': 3})
a - b # Counter({'a': 1}) — only positive counts kept
a & b # Counter({'a': 1, 'b': 2, 'c': 1}) — min (intersection)
a | b # Counter({'a': 2, 'b': 2, 'c': 2}) — max (union)
Semantică de multiset. Util pentru lucruri de genul „câte din fiecare token apar în ambele documente”.
defaultdict: chei auto-initializate
from collections import defaultdict
groups: defaultdict[str, list[int]] = defaultdict(list)
for user_id, tag in [(1, "a"), (2, "a"), (1, "b"), (3, "a")]:
groups[tag].append(user_id)
# defaultdict(<class 'list'>, {'a': [1, 2, 3], 'b': [1]})
defaultdict(factory) creează valoarea apelând factory() prima dată când accesezi o cheie lipsă. Cu list primești o listă goală gata pentru .append(). Pattern-ul „grupează lucrurile astea după o cheie” e atât de comun, încât ăsta e de departe cel mai folosit membru al modulului collections.
Alte factory-uri:
defaultdict(int), auto-zero, ca un Counter primitivdefaultdict(set), set gol pentru grupare deduplicatădefaultdict(dict), dicționare imbricate; cheia o dată, primești un dict, bagi lucruri în eldefaultdict(lambda: "unknown"), orice callable care întoarce valoarea implicită
Alternativa fără defaultdict:
groups: dict[str, list[int]] = {}
for user_id, tag in pairs:
if tag not in groups:
groups[tag] = []
groups[tag].append(user_id)
Sau cu setdefault:
groups.setdefault(tag, []).append(user_id)
Ambele funcționează. defaultdict e mai curat odată ce corpul buclei tale are mai mult decât un singur append.
Notă despre asistența AI. Asistenții moderni de cod AI sunt de încredere în recunoașterea pattern-urilor „grupez lucruri” sau „număr lucruri” și sugerează
defaultdict(list)sauCounterodată ce văd bucla. Merită prompt-ul. Capcana inversă e mai comună: asistenții tind să supraproducă@dataclasspentru orice date structurate, inclusiv în cazuri unde un dict simplu ar fi fost mai potrivit. Dacă te trezești cu cinci dataclass-uri care țin câte trei câmpuri și nu primesc niciodată metode, întreabă-te dacă undict[str, Foo]ar fi fost mai onest.
deque: lista pe care o vrei cand faci append si pop
from collections import deque
queue: deque[int] = deque([1, 2, 3])
queue.append(4) # right side: [1, 2, 3, 4]
queue.appendleft(0) # left side: [0, 1, 2, 3, 4]
queue.pop() # 4, removed from right
queue.popleft() # 0, removed from left
Un list are append și pop rapide la dreapta și lente la stânga, fiindcă scoaterea din față mută toate celelalte elemente. Un deque (pronunțat „dec”) are operații O(1) la ambele capete.
Folosește-l când:
- Ai nevoie de o coadă FIFO (
append+popleft). - Ai nevoie de o stivă LIFO, deși un list merge bine pentru asta.
- Ai nevoie de o fereastră glisantă cu lungime maximă fixă:
recent: deque[float] = deque(maxlen=100)
for value in stream:
recent.append(value) # oldest auto-evicted when len > 100
if len(recent) == 100:
moving_avg: float = sum(recent) / 100
maxlen e feature-ul subapreciat. Deque-ul aruncă în liniște cea mai veche intrare când ar depăși limita. Perfect pentru medii mobile, log-uri rulante, „ultimele N evenimente”.
Dacă te trezești făcând lst.pop(0) sau lst.insert(0, x) pe o listă lungă, treci la un deque.
namedtuple: un tuplu cu nume de campuri
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p: Point = Point(3, 4)
p.x # 3
p.y # 4
p[0] # 3 — also works as a tuple
x, y = p # tuple unpacking still works
E un tuplu: imutabil, hashable, comparabil, despachetabil, dar cu acces pe atribute denumite. namedtuple a fost răspunsul timp de ani la „vreau un struct mic, read-only”.
În Python modern (3.7+), răspunsul e de obicei @dataclass(frozen=True) în loc, decât dacă ai nevoie strict de comportamentul de tip tuplu:
- Despachetare după poziție
- Comparație
==după valoarea de tuplu - Lucru cu cod care așteaptă o secvență (un rând de bază de date, un API bazat pe despachetare)
Pentru orice altceva, dataclass e mai flexibil. Există și typing.NamedTuple pentru aceeași idee, cu hint-urile de tip integrate în sintaxa de clasă:
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
Comportament identic la runtime cu collections.namedtuple, mai plăcut de scris.
OrderedDict: in mare parte redundant din 3.7
Înainte de Python 3.7, dict nu garanta ordinea de inserare. OrderedDict da. Din 3.7, dict-ul obișnuit păstrează ordinea de inserare ca garanție de limbaj. Așa că OrderedDict e în mare parte istoric.
Mai are două feature-uri unice:
move_to_end(key)șimove_to_end(key, last=False)pentru repoziționarea intrărilor.- Comparația
==ține cont de ordine.OrderedDict([("a", 1), ("b", 2)]) != OrderedDict([("b", 2), ("a", 1)]), dar cu dict-uri obișnuite ar fi egale.
Pentru 99% din cod: folosește dict. OrderedDict există când ai nevoie de cele două feature-uri.
@dataclass: struct-ul modern
Ăsta e mare. @dataclass a fost adăugat în Python 3.7 (PEP 557) și înlocuiește o porție uriașă de boilerplate.
Înainte:
class Order:
def __init__(self, id: int, customer: str, total: float, paid: bool = False) -> None:
self.id = id
self.customer = customer
self.total = total
self.paid = paid
def __repr__(self) -> str:
return f"Order(id={self.id!r}, customer={self.customer!r}, total={self.total!r}, paid={self.paid!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Order):
return NotImplemented
return (self.id, self.customer, self.total, self.paid) == \
(other.id, other.customer, other.total, other.paid)
După:
from dataclasses import dataclass
@dataclass
class Order:
id: int
customer: str
total: float
paid: bool = False
Decoratorul inspectează adnotările clasei și generează automat __init__, __repr__ și __eq__. Scrii ce e de fapt unic în clasă: câmpurile, tipurile lor, valorile implicite, și sari peste rest.
o = Order(id=1, customer="Marco", total=49.99)
print(o) # Order(id=1, customer='Marco', total=49.99, paid=False)
o.paid = True
o == Order(1, "Marco", 49.99, True) # True
Variantele care merita stiute
frozen=True, imutabil, hashable. Folosește pentru obiecte de valoare, chei de dicționar, orice n-ar trebui să mute.
@dataclass(frozen=True)
class Coord:
x: float
y: float
c = Coord(1.0, 2.0)
c.x = 5.0 # FrozenInstanceError
points: set[Coord] = {Coord(1, 2), Coord(3, 4)} # works because frozen is hashable
slots=True (3.10+), generează __slots__, salvând memorie și accelerând ușor accesul la atribute. Util când ai milioane de instanțe. Compromisul e că nu poți atribui dinamic atribute în afara câmpurilor declarate.
@dataclass(slots=True)
class Tick:
timestamp: float
price: float
volume: int
kw_only=True (3.10+), forțează toate câmpurile să fie keyword-only la construcție. Bun când o clasă are multe câmpuri și construcția pozițională ar fi ilizibilă.
@dataclass(kw_only=True)
class HttpRequest:
url: str
method: str = "GET"
headers: dict[str, str] | None = None
timeout: float = 30.0
# Must call as: HttpRequest(url="...", method="POST")
Poți marca și câmpuri individuale ca kw-only cu sentinela KW_ONLY, vezi documentația când ai nevoie.
Valori implicite mutabile: singura capcana
Nu face asta:
@dataclass
class Shopping:
items: list[str] = [] # ValueError on class definition
Python prinde asta pentru tine, fiindcă fiecare instanță ar împărți aceeași listă. Folosește field(default_factory=list):
from dataclasses import dataclass, field
@dataclass
class Shopping:
items: list[str] = field(default_factory=list)
notes: dict[str, str] = field(default_factory=dict)
Factory-ul se apelează din nou pentru fiecare instanță nouă.
Pydantic vs dataclass
Întrebare comună. Ambele te lasă să declari câmpuri cu tipuri. Diferența:
@dataclassnu face validare. Dacă spuiid: int, primești orice îți pasează apelantul. Dă-i"5"și primești un string cu un type hint care minte.- Pydantic validează și convertește. Pasează
"5"la un model Pydantic care așteaptăintși primești5. Pasează"hello"și primești unValidationError.
Folosește Pydantic când:
- Parsezi input neîncrezibil (HTTP body, JSON, fișier de configurare).
- Ești la o graniță de API și vrei garanții.
- Vrei generare de schemă JSON, serializare, validatori pe câmpuri.
Folosește @dataclass când:
- Modelezi stare internă, iar codul tău e singurul producător.
- Nu vrei o dependență terță.
- Nu ai nevoie de validare; type hint-urile sunt documentație aici, nu enforcement.
Într-un serviciu tipic: Pydantic pentru modelele de request și response, dataclass pentru tot ce e intern. attrs e biblioteca terță mai veche care le-a inspirat pe ambele, încă excelentă, încă folosită pe scară largă, dar dacă pornești de la zero azi alegerea e @dataclass (stdlib) sau Pydantic (validare).
Exemplu real: model de raspuns API
from dataclasses import dataclass, field
from datetime import datetime, timezone
from collections import Counter, defaultdict
@dataclass(frozen=True, slots=True)
class LogEntry:
timestamp: datetime
level: str
service: str
message: str
@dataclass
class LogSummary:
period_start: datetime
period_end: datetime
total: int = 0
by_level: Counter[str] = field(default_factory=Counter)
by_service: defaultdict[str, list[str]] = field(
default_factory=lambda: defaultdict(list)
)
def summarize(entries: list[LogEntry]) -> LogSummary:
if not entries:
now: datetime = datetime.now(tz=timezone.utc)
return LogSummary(period_start=now, period_end=now)
summary = LogSummary(
period_start=min(e.timestamp for e in entries),
period_end=max(e.timestamp for e in entries),
total=len(entries),
)
for e in entries:
summary.by_level[e.level] += 1
if e.level == "ERROR":
summary.by_service[e.service].append(e.message)
return summary
LogEntry e frozen și slotted, fiindcă sunt milioane de instanțe și nu se schimbă. LogSummary e mutabil, fiindcă îl construim treptat. Counter pentru frecvențele de nivel. defaultdict(list) pentru gruparea mesajelor de eroare după serviciu. Type hint-uri peste tot, fără boilerplate, structura datelor e vizibilă dintr-o ochire.
Ăsta e scopul: mai puțin cod, mai mult sens pe linie.
Asta încheie Modulul 2, Stăpânirea bibliotecii standard. Modulul 3 reia cu iteratori, generatoare și kit-ul itertools, care face codul Python să se simtă mai puțin ca o secvență de bucle for și mai mult ca un pipeline.
Referințe: collections — Container datatypes, dataclasses — Data Classes, typing.NamedTuple, PEP 557 — Data Classes. Consultat 2026-05-01.