Rozdział 14 wyjaśnił, po co w pipeline jest akurat Python. Ten dodatek pokazuje, jak wygląda kod, który się z niego naprawdę pisze — te same kilkanaście operacji i metod, które wracają w prawie każdym narzędziu, niezależnie od studia.
pathlib, subprocess, argparse, json, dataclasses, logging, comprehensions, contextlib.logging zamiast print, i łapanie konkretnych wyjątków zamiast gołego except.Pomocnik pokazał budowanie ścieżki przez os.path.join — to wciąż działający, częsty w starszym kodzie styl. We współczesnym kodzie narzędziowym dominuje jednak pathlib.Path: ścieżka staje się obiektem z operatorem / zamiast sklejania stringów, i z metodami, które same wiedzą, jak się zachować na Windows i na Linuksie naraz.
from pathlib import Path publish_dir = Path(show) / task / "publish" publish_dir.mkdir(parents=True, exist_ok=True) latest = publish_dir / "shot0100_v004.json" preview = latest.with_suffix(".jpg") # shot0100_v004.jpg print(latest.exists(), latest.parent.name)
Narzędzie rzadko operuje na jednym pliku — częściej na wszystkich plikach danego typu w całym showie. Path.rglob() przeszukuje drzewo katalogów rekurencyjnie i leniwie: nie wczytuje całej listy do pamięci naraz, tylko zwraca kolejne dopasowania jedno po drugim, co ma znaczenie, gdy show ma setki tysięcy plików.
from pathlib import Path show_root = Path("/shows/demo") for texture in show_root.rglob("*.tex"): print(f"znaleziono: {texture.relative_to(show_root)}")
Nazwy plików w pipeline zwykle kodują metadane w samej nazwie — numer shota, numer wersji. Wyrażenie regularne z nazwanymi grupami wyciąga je naraz, jedną operacją, zamiast ręcznego cięcia stringa po znaku podkreślenia i licząc, że format nigdy się nie zmieni.
import re pattern = re.compile(r"shot_(?P<shot>\d+)_v(?P<version>\d+)\.\w+$") match = pattern.match("shot_010_v003.mb") if match: shot = match.group("shot") # "010" version = int(match.group("version")) # 3
Rozdział 14 postawił tezę: Python nie zastępuje C++, tylko go spina. W kodzie narzędzia widać to dosłownie — większość linii nie liczy niczego samodzielnie, tylko woła inny program i sprawdza, czy się udało. subprocess.run() to standardowy sposób: przechwycić wyjście, sprawdzić kod wyjścia, nigdy nie zakładać, że zewnętrzny program się nie wywalił.
import subprocess result = subprocess.run( ["ffmpeg", "-i", str(src), "-vframes", "1", str(thumbnail)], capture_output=True, text=True, ) if result.returncode != 0: raise RuntimeError(f"ffmpeg nie powiódł się: {result.stderr}")
Samodzielne skrypty farmowe (Rozdział 19) uruchamiają się poza jakimkolwiek DCC, więc potrzebują prawdziwego interfejsu linii poleceń — nie wartości wpisanych na sztywno na górze pliku. argparse daje nazwane flagi, wartości domyślne i, praktycznie za darmo, tryb --dry-run do bezpiecznego testowania.
import argparse parser = argparse.ArgumentParser(description="Konwersja tekstur showu wsadowo") parser.add_argument("show") parser.add_argument("--dry-run", action="store_true", help="tylko pokaż, co by się stało") args = parser.parse_args() if args.dry_run: print(f"[dry-run] skonwertowałbym tekstury dla {args.show}")
Sesja DCC i samodzielny proces na farmie łączy ten sam kontekst — show, shot — ale nie przez argumenty, tylko przez zmienne środowiskowe, ustawione raz przy budowie rozwiązanego środowiska (Dodatek I). os.environ.get() z wartością domyślną to obronny, standardowy sposób ich odczytu.
import os show = os.environ.get("SHOW") shot = os.environ.get("SHOT", "unknown") if show is None: raise EnvironmentError("SHOW nie jest ustawione — uruchom w rozwiązanym kontekście")
Dict z kluczami-stringami jest wszędzie w kodzie narzędziowym, ale ma cichą wadę: literówka w kluczu nie wywali błędu od razu, tylko gdzieś dalej, jako KeyError w zupełnie innym miejscu kodu. @dataclass daje strukturze nazwę i kształt — pola, typy, czytelny repr — przy minimalnym narzucie w stosunku do zwykłej klasy.
from dataclasses import dataclass @dataclass class Shot: show: str number: str frame_range: tuple[int, int] @property def name(self) -> str: return f"{self.show}_{self.number}" s = Shot("DEMO", "010", (1001, 1150)) print(s.name, s) # DEMO_010, Shot(show='DEMO', number='010', ...)
Zapytanie do API takiego jak ShotGrid/Flow (Rozdział 8) zwraca listę dictów. Comprehension to idiomatyczny sposób ich filtrowania i przekształcania — jedna linia zamiast pętli for z ręcznym append.
# entities = wynik zapytania do ShotGrid/Flow, lista dictów approved_shots = [e["code"] for e in entities if e["sg_status_list"] == "apr"] by_code = {e["code"]: e for e in entities}
Configi w pipeline zwykle mają warstwy — wartości domyślne dla całego showu, nadpisane per shot. Złożenie dwóch dictów jedną linią ({**defaults, **overrides}) to prostszy odpowiednik ręcznego kopiowania klucz po kluczu.
import json with open(config_path) as f: overrides = json.load(f) defaults = {"fps": 24, "resolution": [1920, 1080]} config = {**defaults, **overrides} # overrides wygrywają przy konflikcie
print() nie ma poziomów, nie da się go wyłączyć ani przekierować do pliku loga na farmie bez przepisywania kodu. logging rozwiązuje wszystkie trzy naraz — i, przy okazji, log.exception() zapisuje pełny traceback, nie tylko treść komunikatu.
import logging log = logging.getLogger(__name__) def publish(shot): log.info("publikuję %s", shot.name) try: ... except OSError: log.exception("publish nie powiódł się dla %s", shot.name) raise
Goły except: łapie dosłownie wszystko — literówkę w nazwie zmiennej tak samo jak brak pliku — i chowa błąd zamiast go ujawnić. Rozdział 15 mówił o czytelnym komunikacie zamiast surowego tracebacka; własna klasa wyjątku plus łapanie konkretnego typu to sposób, żeby to zrobić w praktyce.
class PublishError(Exception): """Coś poszło nie tak podczas publikowania wersji.""" def publish(shot): try: _write_version(shot) except FileNotFoundError as e: raise PublishError(f"brak folderu publish dla {shot.name}") from e
Ostatni powracający wzorzec: „tymczasowo zmień coś, zawsze przywróć — nawet jeśli coś po drodze wybuchnie". Blok with gwarantuje, że kod sprzątający uruchomi się także przy wyjątku; contextlib.contextmanager to najlżejszy sposób, żeby taki menedżer kontekstu napisać samemu, bez pełnej klasy z __enter__/__exit__.
from contextlib import contextmanager @contextmanager def temp_namespace(ns): previous = cmds.namespaceInfo(currentNamespace=True) cmds.namespace(set=ns) try: yield finally: cmds.namespace(set=previous) # wraca nawet po wyjątku with temp_namespace("REF_charA"): cmds.createNode("transform")