Biblioteka kształtów – programowanie funkcyjne¶
W tym ćwiczeniu przepiszesz bibliotekę kształtów SVG zgodnie z paradygmatem programowania funkcyjnego. W przeciwieństwie do poprzedniego zadania:
nie używamy klas
kształty są reprezentowane jako dane (słowniki)
renderowanie odbywa się przez funkcje
Efektem końcowym ma być poprawne wygenerowanie wykresu funkcji sinus i cosinus
za pomocą dostarczonego programu plot.py.
Reprezentacja kształtu¶
Każdy element SVG jest reprezentowany jako słownik zawierający m.in.:
tag– typ elementu (np.circle,line)id– identyfikatorinne atrybuty (np.
cx,cy)opcjonalnie
style
Dla przykładu poniżej podano implementację funkcji tworzącej element <circle>:
1def Circle(shape_id, cx, cy, r, style=None):
2 return {
3 "tag": "circle",
4 "id": shape_id,
5 "cx": cx,
6 "cy": cy,
7 "r": r,
8 "style": style,
9 }
Renderowanie elementów¶
Każdy typ elementu ma odpowiadającą funkcję renderującą. Dla przykładu element <circle> narysujemy:
1def render_circle(obj):
2 return f'<circle id="{obj["id"]}"{render_style(obj["style"])} cx="{obj["cx"]}" cy="{obj["cy"]}" r="{obj["r"]}" />'
Zwróć uwagę, że funkcja:
przyjmuje słownik (dane)
zwraca tekst SVG
nie modyfikuje stanu
Funkcja render_circle() pobiera styl elementu, zapisany w słowniku pod kluczem "style"
i wywołuje dla niego render_style().
Funkcja główna renderująca¶
Funkcja render_element() wybiera odpowiednią funkcję renderującą
na podstawie pola tag, korzystając z dyspozytora:
1def render_element(obj):
2 try:
3 return _RENDERERS[obj["tag"]](obj)
4 except KeyError:
5 raise ValueError(f'Unknown SVG tag: {obj["tag"]}') from None
Zadanie¶
Uzupełnij plik svg_shapes_func.py tak, aby program plot.py działał poprawnie.
W szczególności:
Zdefiniuj brakujące funkcje konstruktorów kształtów:
Line()Rect()Group()Square()
Każda z nich powinna zwracać słownik opisujący element SVG.
2. Zaimplementuj Style, która przyjmuje wymagane argumenty:
fill, stroke oraz stroke_width a zwraca odpowiedni słownik. Dodatkowo,
napisz render_style().
Zaimplementuj brakujące funkcje renderujące:
_render_line()_render_rect()_render_group()
Uzupełnij słownik dyspozytora:
_RENDERERS = { ... }
który mapuje
tag→ funkcja renderująca
Dodatkowe elementy funkcyjne¶
Zwróć uwagę na elementy programowania funkcyjnego użyte w zadaniu:
funkcje jako argumenty (
make_points(..., math.sin, ...))brak mutowalnego stanu
transformacje danych (
with_style())list comprehensions zamiast pętli z
.add()
Funkcja with_style() (już zaimplementowana):
1def with_style(style, element):
2 return {**element, "style": style}
Ostatecznie, obraz tworzony jest funkcją svg_drawing:
1def render_svg(width, height, elements, file_obj=None):
2 inner = "\n".join(render_element(el) for el in elements)
3 txt = (
4 f'<svg width="{width}" height="{height}" '
5 f'xmlns="http://www.w3.org/2000/svg">\n'
6 f'{inner}\n'
7 f'</svg>'
8 )
9
10 if file_obj is None:
11 print(txt)
12 elif isinstance(file_obj, str):
13 with open(file_obj, "w", encoding="utf-8") as f:
14 f.write(txt)
15 else:
16 file_obj.write(txt)
Efekt końcowy / ocena ćwiczenia¶
Poniżej znajduje się kompletny program plot.py, który korzysta z Twojej biblioteki.
Twoim celem jest uzyskanie poprawnego obrazu SVG.
1import math
2from svg_shapes_func import *
3from itertools import chain
4
5
6# Coordinate transforms as lambdas
7to_data_x = lambda screen_x: (screen_x - 10) / 100
8to_data_y = lambda screen_y: (150 - screen_y) / 100
9to_screen_x = lambda data_x: 10 + 100 * data_x
10to_screen_y = lambda data_y: 150 - 100 * data_y
11
12
13# Returns iterable to markers rather than markers themselves
14def make_points(sample_points, fun, marker, prefix, size):
15 def make_one(i):
16 return marker(
17 f"{prefix}{i}",
18 to_screen_x(to_data_x(i + 10)),
19 to_screen_y(fun(to_data_x(i + 10))),
20 size,
21 )
22 return map(make_one, sample_points)
23
24
25# Styles
26border_style = Style(stroke="black", fill="white", stroke_width="3")
27grid_style = Style(stroke="gray", stroke_width="0.5")
28sin_style = Style(fill="#a6cee3", stroke_width=1.5, stroke="#1f78b4")
29cos_style = Style(fill="#fdbf6f", stroke_width=1.5, stroke="#ff7f00")
30
31# Border
32border = Rect("border", 10, 10, 628, 280, style=border_style)
33
34# Iterable to vertical grid lines
35vertical_grid = map(lambda x:
36 Line(f"lx{x}",to_screen_x(x * math.pi / 4),10, to_screen_x(x * math.pi / 4),290),
37 range(1, 8)
38 )
39
40# Iterable to horizontal grid lines
41horizontal_grid = map(lambda y:
42 Line(f"ly{y}", 10, to_screen_y(y), 638, to_screen_y(y)),
43 [1.0, 0.75, 0.5, 0.25, 0.0, -0.25, -0.5, -0.75, -1.0]
44 )
45
46# a grid holds a chained iterables over grid lines
47grid_group = Group("grid", chain(vertical_grid, horizontal_grid), style=grid_style)
48
49# Sample points iterates over approximately every pi/10
50sample_points = range(0, 628, 31)
51
52# Marker sets created by a higher-order function
53sin_points = make_points(sample_points, math.sin, Circle, "s", 5)
54cos_points = make_points(sample_points, math.cos, Square, "c", 8)
55
56# Apply style functionally
57sin_group = with_style(sin_style, Group("sin", sin_points))
58cos_group = with_style(cos_style, Group("cos", cos_points))
59
60# Final drawing
61render_svg(640, 300, [border, grid_group, sin_group, cos_group], "pltf.svg")
Po poprawnej implementacji uruchomienie plot.py powinno wygenerować poprawny wykres funkcji
sinus i cosinus w formacie SVG.