-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path04_functions_classes_errors.py
More file actions
98 lines (75 loc) · 3.42 KB
/
Copy path04_functions_classes_errors.py
File metadata and controls
98 lines (75 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#!/usr/bin/env python3
"""
Writing functions (default args, *args/**kwargs), a minimal class, and
exception handling — the Python equivalent of r/05_functions_and_stats.R's
function-writing section, plus error handling that R's tryCatch() also
covers but comes up constantly enough in Python to deserve its own file.
Run: python3 python/04_functions_classes_errors.py
"""
from dataclasses import dataclass, field
def summarize_depth(depths, threshold=20, drop_na=False):
"""Default arguments (threshold=20) let callers omit anything they don't
need to customize."""
if drop_na:
depths = [d for d in depths if d is not None]
valid = [d for d in depths if d is not None]
return {
"mean": sum(valid) / len(valid) if valid else float("nan"),
"n_below_threshold": sum(1 for d in valid if d < threshold),
}
print("summarize_depth():", summarize_depth([30, 45, 12, None, 18], drop_na=True))
def build_read_group(sample_id, **kwargs):
"""**kwargs collects any number of extra named arguments into a dict —
useful when a function needs to accept optional, open-ended metadata."""
fields = {"ID": sample_id, "SM": sample_id, "PL": "ILLUMINA", **kwargs}
return "\\t".join(f"{k}:{v}" for k, v in fields.items())
print("\nbuild_read_group() with extra LB kwarg:")
print(" ", build_read_group("sampleA", LB="lib1"))
def total_depth(*samples):
"""*args collects any number of positional arguments into a tuple —
the caller doesn't need to pass a list explicitly."""
return sum(samples)
print("\ntotal_depth(10, 20, 30):", total_depth(10, 20, 30))
# --- A minimal class ------------------------------------------------
# @dataclass auto-generates __init__, __repr__, and __eq__ from the
# annotated fields below, instead of writing that boilerplate by hand.
@dataclass
class Variant:
chrom: str
pos: int
ref: str
alt: str
depth: int = 0
tags: list = field(default_factory=list) # mutable defaults need default_factory, not `[]`
@property
def is_snp(self) -> bool:
return len(self.ref) == 1 and len(self.alt) == 1
def __str__(self) -> str:
kind = "SNP" if self.is_snp else "INDEL"
return f"{self.chrom}:{self.pos} {self.ref}>{self.alt} ({kind}, depth={self.depth})"
v1 = Variant("chr20", 12345, "A", "T", depth=35)
v2 = Variant("chr20", 12350, "A", "AT", depth=40, tags=["low_complexity"])
print("\nDataclass instances:")
print(" ", v1)
print(" ", v2)
print(" repr():", v1) # auto-generated __repr__ from @dataclass
# --- Exception handling -----------------------------------------------
def parse_depth_field(info_field: str) -> int:
"""Deliberately narrow except clauses (catch ValueError, not bare
`except:`) so a real bug elsewhere doesn't get silently swallowed."""
try:
dp_str = info_field.split("DP=")[1].split(";")[0]
return int(dp_str)
except (IndexError, ValueError) as exc:
raise ValueError(f"Could not parse DP from {info_field!r}") from exc
print("\nparse_depth_field('DP=45;AF=0.5'):", parse_depth_field("DP=45;AF=0.5"))
try:
parse_depth_field("no depth here")
except ValueError as exc:
print("Caught expected error:", exc)
# `finally` always runs, whether or not an exception occurred — typically
# used for cleanup (closing a file/connection) that must happen either way.
try:
result = 10 / 2
finally:
print("\n`finally` block ran (cleanup goes here) — result:", result)