| name | python-bio-decorators |
| description | Write @decorators (functools.wraps, @lru_cache, factories) to time, validate, and memoize bio functions. Use for pipeline timing/logging, DNA/protein alphabet checks, caching codon/alignment calls, or decorator stacking. |
| tool_type | python |
| primary_tool | functools |
Decorators for Bioinformatics
@decorator above def f(): is exactly f = decorator(f) at definition time.
When to Use
- Adding timing/logging around a pipeline step (e.g.
@timer on a parsing or alignment function) without editing its body.
- Validating that a function's input is a legal DNA/RNA/protein alphabet before running expensive logic.
- Caching pure functions with repeated inputs — codon-to-amino-acid lookups, recursive alignment scoring, k-mer counting.
- Building reusable "specialized function" closures, e.g. a motif counter factory (
make_motif_counter("CG")).
- Explaining or debugging why a decorated function lost its
__name__/docstring, or why decorator stacking order changed behavior.
Version Compatibility
Pure standard library — functools and closures work unchanged on Python ≥3.8 (examples use f-strings and functools.wraps, both stable since 3.6+). No third-party dependency required.
Prerequisites
- Comfort with Python functions as first-class objects (functions passed as arguments, returned from functions).
*args, **kwargs syntax for variadic wrappers.
- Related:
python-bio-functions (higher-order functions, closures basics), python-bio-error-handling (raising/catching inside wrappers), python-bio-context-managers (the sibling resource-management pattern).
Core Pattern: Basic Decorator
Goal: measure and log how long a bioinformatics function takes to run, without modifying its body.
Approach: wrap the function in a closure that times the call and forwards *args, **kwargs; use functools.wraps to preserve identity.
import functools
import time
def timer(func):
"""Decorator: measure and print execution time of a function."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[timer] {func.__name__}: {elapsed:.4f}s")
return result
return wrapper
@timer
def gc_content(seq: str) -> float:
"""Calculate GC content of a DNA sequence."""
seq = seq.upper()
return (seq.count('G') + seq.count('C')) / len(seq) * 100
gc_content("ATGCGATCGATCGTAGC")
Decorator Factory (with arguments)
Goal: validate that a sequence only contains legal characters for its type (DNA vs. protein) before the wrapped function runs.
Approach: a decorator that itself takes arguments needs three nested levels — factory(args) → decorator(func) → wrapper(*args); the factory closes over valid_chars.
def validate_sequence(valid_chars: str, seq_type: str = "DNA"):
"""Decorator factory: validate that the first argument is a valid sequence."""
valid_set = set(valid_chars.upper())
def decorator(func):
@functools.wraps(func)
def wrapper(seq, *args, **kwargs):
invalid = set(seq.upper()) - valid_set
if invalid:
raise ValueError(
f"Invalid {seq_type} characters {invalid} in input to {func.__name__}()"
)
return func(seq, *args, **kwargs)
return wrapper
return decorator
@validate_sequence('ATGC', seq_type='DNA')
def complement(seq: str) -> str:
"""Return the DNA complement."""
comp_map = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
return ''.join(comp_map[b] for b in seq.upper())
complement("ATGCGATC")
complement()
Memoization
Goal: avoid recomputing expensive pure functions (codon lookup, recursive alignment) for repeated inputs.
Approach: prefer the stdlib functools.lru_cache; write a manual cache only when you need custom eviction or a .clear() hook exposed on the function.
from functools import lru_cache
@lru_cache(maxsize=None)
def translate_codon(codon: str) -> str:
"""Translate a single codon to its amino acid (cached)."""
table = {
'TTT':'F','TTC':'F','TTA':'L','TTG':'L','TCT':'S','TCC':'S','TCA':'S','TCG':'S',
'TAT':'Y','TAC':'Y','TAA':'*','TAG':'*','TGT':'C','TGC':'C','TGA':'*','TGG':'W',
'CTT':'L','CTC':'L','CTA':'L','CTG':'L','CCT':'P','CCC':'P','CCA':'P','CCG':,
:,:,:,:,:,:,:,:,
:,:,:,:,:,:,:,:,
:,:,:,:,:,:,:,:,
:,:,:,:,:,:,:,:,
:,:,:,:,:,:,:,:,
}
table.get(codon.upper(), )
() -> :
protein = []
i (, (dna) - , ):
aa = translate_codon(dna[i:i + ])
aa == :
protein.append(aa)
.join(protein)
translate_sequence()
translate_codon.cache_info()
For recursive functions (e.g. a naive Needleman-Wunsch scorer), a manual dict-backed memoizer works the same way but exposes .cache and .clear_cache directly on the wrapper — useful when args aren't hashable via lru_cache alone or you need to inspect hit count mid-run.
Stacking Decorators
Applied bottom-up (closest to the function runs first): @A @B def f is A(B(f)).
@timer
@validate_sequence('ATGC')
def analyze(seq: str) -> dict:
"""Full analysis of a DNA sequence."""
seq = seq.upper()
return {'length': len(seq), 'gc': (seq.count('G') + seq.count('C')) / len(seq) * 100}
analyze("ATGCGATCGATCGATCGATCG")
analyze("ATGXYZ")
Closure Pattern
Goal: build a family of specialized functions (e.g. one motif counter per motif) without repeating code.
Approach: an inner function defined inside an outer one keeps a live reference to the outer function's local variables even after the outer call returns.
def make_motif_counter(motif: str):
"""Return a function that counts a specific motif in any sequence."""
motif = motif.upper()
def counter(sequence: str) -> int:
return sequence.upper().count(motif)
return counter
count_cpg = make_motif_counter("CG")
count_cpg("GCGCGCATCG")
Pitfalls
- Always use
functools.wraps: without it, func.__name__ becomes 'wrapper', breaking logging, help(), and stack traces.
- Decorator factories need 3 levels:
@validate_sequence('ATGC') requires factory → decorator → wrapper; a 2-level decorator receives the argument as the function, causing a confusing TypeError.
- Stacking order matters:
@A @B def f = A(B(f)); validation should be inner (runs first, fails fast), timing outer (measures total including validation).
lru_cache requires hashable arguments: lists, dicts, and numpy arrays cannot be cached; convert to tuple or bytes before passing.
lru_cache holds strong references: cached results are never GC'd until the cache is cleared; set a bounded maxsize and call .cache_clear() on long-running processes handling many distinct sequences.
See Also
python-bio-functions — higher-order functions and closures without the decorator syntax sugar.
python-bio-error-handling — designing the exceptions raised inside a validation wrapper.
python-bio-context-managers — the with-statement analog for setup/teardown around code.
python-bio-classes — combining decorators (@property, @staticmethod) with class-based sequence objects.