Jak zorganizować duże programy w języku R?


161

Kiedy podejmuję się projektu R o dowolnej złożoności, moje skrypty szybko stają się długie i mylące.

Jakie praktyki mogę zastosować, aby praca z moim kodem była zawsze przyjemna? Myślę o takich rzeczach jak

  • Umieszczanie funkcji w plikach źródłowych
  • Kiedy coś rozbić do innego pliku źródłowego
  • Co powinno znajdować się w pliku głównym
  • Używanie funkcji jako jednostek organizacyjnych (czy jest to warte zachodu, biorąc pod uwagę, że R utrudnia dostęp do stanu globalnego)
  • Praktyki dotyczące wcięć / łamania linii.
    • Treat (like {?
    • Umieścić rzeczy takie jak)} w 1 lub 2 wierszach?

Zasadniczo, jakie są twoje praktyczne zasady organizowania dużych skryptów R?


12
powinno być wiki społeczności
SilentGhost

Możesz również spojrzeć na ProjectTemplatepakiet.
ctbrown

Odpowiedzi:


71

Standardową odpowiedzią jest użycie pakietów - zobacz podręcznik Writing R Extensions, a także różne samouczki w Internecie.

To daje Ci

  • quasi-automatyczny sposób organizowania kodu według tematu
  • gorąco zachęca do napisania pliku pomocy, skłaniając do przemyślenia interfejsu
  • wiele kontroli poczytalności za pośrednictwem R CMD check
  • szansa na dodanie testów regresji
  • a także środek do przestrzeni nazw.

Samo przejechanie source()kodu działa w przypadku naprawdę krótkich fragmentów. Wszystko inne powinno znajdować się w pakiecie - nawet jeśli nie planujesz go publikować, ponieważ możesz pisać wewnętrzne pakiety dla wewnętrznych repozytoriów.

Jeśli chodzi o część „jak edytować”, podręcznik R Internals zawiera doskonałe standardy kodowania R w rozdziale 6. W przeciwnym razie zwykle używam wartości domyślnych w trybie ESS Emacsa .

Aktualizacja 2008-sierpień-13: David Smith właśnie napisał na blogu o przewodniku Google R. Style Guide .


8
Jeśli rozbudowujesz swoje drzewo źródłowe / analizę „organicznie”, czy nie uważasz, że jest to trudne / uciążliwe? Jeśli zauważysz błąd w swoim kodzie (często występujący podczas eksploracji nowej przestrzeni problemów), musisz (i) naprawić źródło; (ii) ponownie zainstaluj pakiet; (iii) załaduj go ponownie do swojego obszaru roboczego? Czy istnieje sposób wywołania biblioteki (...) w celu ponownego załadowania pakietu, który jest już załadowany (krok iii powyżej)? Nie musisz zabijać swojego obszaru roboczego, restartować R, a następnie ponownie ładować bibliotekę / pakiet, aby sprawdzić, czy wszystko jest w porządku?
Steve Lianoglou

1
Próbuję googlować pod kątem stylu kodowania R.
Hadley

3
@SteveLianoglou Wiem, że to jest dość stare, ale pakiet Devtools Hadley sprawia, że ​​przeładowanie całego kodu jest bardzo łatwe.
Dason

1
Ten post na blogu zawiera (moim zdaniem) naprawdę dobry szybki samouczek, jak zbudować pierwszy pakiet bare bones: hilaryparker.com/2014/04/29/writing-an-r-package-from-scratch
panterasBox

1
Oto działający link do Przewodnika po stylu Google R
Denis Rasulev,

51

Lubię umieszczać różne funkcje we własnych plikach.

Ale nie podoba mi się system pakietów R. Jest raczej trudny w użyciu.

Wolę lekką alternatywę, aby umieścić funkcje pliku w środowisku (co każdy inny język nazywa „przestrzenią nazw”) i dołączyć go. Na przykład utworzyłem grupę funkcji „util” w ten sposób:

util = new.env()

util$bgrep = function [...]

util$timeit = function [...]

while("util" %in% search())
  detach("util")
attach(util)

To wszystko jest w pliku util.R . Kiedy je pozyskujesz, otrzymujesz środowisko „util”, dzięki czemu możesz dzwonić util$bgrep()i tak dalej; ale co więcej, attach()wezwanie sprawia, że ​​jest to sprawiedliwe bgrep()i takie działa bezpośrednio. Gdybyś nie umieścił wszystkich tych funkcji w ich własnym środowisku, zanieczyszczałyby przestrzeń nazw najwyższego poziomu interpretera (tę, która się ls()wyświetla).

Próbowałem zasymulować system Pythona, w którym każdy plik jest modułem. Byłoby lepiej, ale to wydaje się w porządku.


Dzięki, Brendan. To jest bardzo przydatne. O co chodzi z pętlą while? Co jest nie tak z if (! ("Util"% in% search ())) attach (util)
Dan Goldstein,

2
więc możesz wielokrotnie używać source („util.R”), jeśli chcesz go modyfikować i tak dalej.
Brendan OConnor

naprawdę nie potrzebujesz pętli while - wszystko czego potrzebujesz to odłącz (util). Nie pamiętam, czy wyświetla błąd, czy nie, jeśli nie jest jeszcze załadowany, ale jest to najbezpieczniejsze i działa. sugestie mile widziane.
Brendan OConnor

1
Tworzenie środowisk specyficznych dla funkcji i dołączanie ich to droga dla mnie. Innym sposobem, aby osiągnąć to samo w inny sposób (z większą modułowość) jest do stosowania sys.source: MyEnv <- attach(NULL, name=s_env); sys.source(file, MyEnv). Deklaruję nawet (w swoim własnym środowisku!) Podczas uruchamiania funkcję, sys.source2która wyszuka, jeśli środowisko o tej samej nazwie już tu jest i podaje je zamiast tworzyć nowe. To sprawia, że ​​dodawanie osobistych funkcji jest szybkie, łatwe i trochę zorganizowane :-)
Antoine Lizée

5
Właśnie zobaczyłem to dzisiaj i dotyczy to alternatywnego pakietu: github.com/klmr/modules
ctbrown

34

Może się to wydawać trochę oczywiste, zwłaszcza jeśli jesteś programistą, ale oto jak myślę o logicznych i fizycznych jednostkach kodu.

Nie wiem, czy tak jest w Twoim przypadku, ale kiedy pracuję w R, rzadko zaczynam od dużego złożonego programu. Zwykle zaczynam od jednego skryptu i rozdzielam kod na logicznie rozdzielone jednostki, często używając funkcji. Kod do manipulacji danymi i wizualizacji jest umieszczany w swoich własnych funkcjach itp. I takie funkcje są zgrupowane w jednej sekcji pliku (manipulacja danymi na górze, potem wizualizacja, itd.). Ostatecznie chcesz pomyśleć o tym, jak ułatwić sobie utrzymanie skryptu i obniżyć odsetek błędów.

Jak drobnoziarnisty / gruboziarnisty sprawisz, że Twoje funkcje będą się różnić i istnieją różne ogólne zasady: np. 15 linii kodu lub „funkcja powinna być odpowiedzialna za wykonanie jednego zadania, które jest identyfikowane przez jej nazwę”, itp. Twój przebieg będzie różny . Ponieważ R nie obsługuje wywołania przez referencję, zwykle jestem bardzo zróżnicowany, jeśli chodzi o zbyt drobnoziarniste działanie moich funkcji, gdy wymaga to przekazywania ramek danych lub podobnych struktur. Ale może to być nadmierna rekompensata za niektóre głupie błędy w wydajności, kiedy zaczynałem z R.

Kiedy wyodrębnić jednostki logiczne do ich własnych jednostek fizycznych (takich jak pliki źródłowe i większe grupy, takie jak pakiety)? Mam dwie sprawy. Po pierwsze, jeśli plik staje się zbyt duży i przewijanie między logicznie niepowiązanymi jednostkami jest irytujące. Po drugie, jeśli mam funkcje, które mogą być ponownie wykorzystane przez inne programy. Zwykle zaczynam od umieszczenia jakiejś zgrupowanej jednostki, powiedzmy funkcji manipulacji danymi, w oddzielnym pliku. Mogę następnie pobrać ten plik z dowolnego innego skryptu.

Jeśli zamierzasz wdrożyć swoje funkcje, musisz zacząć myśleć o pakietach. Nie wdrażam kodu R w środowisku produkcyjnym ani do ponownego wykorzystania przez innych z różnych powodów (pokrótce: kultura organizacji preferuje inne języki, obawy o wydajność, GPL itp.). Ponadto mam tendencję do ciągłego udoskonalania i dodawania do moich kolekcji plików źródłowych i wolałbym nie zajmować się pakietami, gdy wprowadzam zmianę. Więc powinieneś sprawdzić inne odpowiedzi dotyczące pakietu, takie jak Dirk, aby uzyskać więcej informacji na ten temat.

Wreszcie, myślę, że twoje pytanie niekoniecznie jest skierowane do R. Naprawdę poleciłbym przeczytanie Code Complete autorstwa Steve'a McConnella, który zawiera wiele mądrości na temat takich zagadnień i ogólnie praktyk kodowania.


3
Bardzo pomocny komentarz, dziękuję. Jestem programistą, ale dobrze jest sprawdzić u innych. Kiedy mówisz: „Ponieważ R nie obsługuje funkcji wywołania przez referencję, zwykle uważam, że moje funkcje nie są zbyt precyzyjne”, słyszę cię. Jestem przyzwyczajony do pisania funkcji takich jak ReadData (); CleanData (); Analizować dane(); GraphData (); a R sprawia, że ​​jest to uciążliwe. Przychodzi mi do głowy myśl, że muszę używać „źródła” tak, jak używam funkcji w innych językach.
Dan Goldstein

2
Masz rację, Dan. Używam w ten sposób „źródła” do zadań związanych z przygotowaniem zestawu danych, więc mogę po prostu użyć przygotowanej ramki data.frame w innych skryptach, w których przeprowadzana jest rzeczywista analiza. Nigdy nie byłem pewien, czy to dobra praktyka, ponieważ wydaje się dziwne w porównaniu z innymi językami - bardziej jak skrypty powłoki. Dobrze jest porównać notatki. :)
ars

Miła odpowiedź i komentarze. Uważam, że jest to szczególnie denerwujące w R, ponieważ często pracujesz z danymi w określonym formacie, dla których naprawdę trudno jest napisać funkcje wielokrotnego użytku. Więc czy piszesz ładny funkcjonalny kod, nawet jeśli wiesz, że nigdy nie zostanie on ponownie użyty, czy po prostu piszesz szybki i nieprzyjemny kod proceduralny, aby uzyskać odrobinę dodatkowej wydajności przy dużych obiektach? Trochę dylematu ... R byłby absolutnie niesamowity z wezwaniem przez referencję ...
nigt101

Cóż, można to zrobić, w pewnym sensie , ale nie ma wzrostu wydajności ...
nic 101

19

Zgadzam się z radą Dirka! IMHO, organizowanie programów od prostych skryptów do udokumentowanych pakietów jest, dla programowania w języku R, jak przejście z programu Word na TeX / LaTeX do pisania. Polecam przyjrzeć się bardzo przydatnemu Tworzenie pakietów języka R: samouczek autorstwa Friedricha Leischa.


6
Pakiety wyglądają atrakcyjnie. Martwiłem się jednak, że mogą być przesadą. Nie piszę kodu ogólnego przeznaczenia. Większość tego, co robię, to testowanie tej hipotezy, testowanie tej hipotezy, tworzenie wykresów, dostosowywanie parametrów wykresu, tworzenie wykresów, przekształcanie danych, tworzenie wykresów. Robię rzeczy, które po zakończeniu prawdopodobnie nigdy nie zostaną ponownie uruchomione.
Dan Goldstein,

1
W takim przypadku powinieneś rzucić okiem na Sweave. Łączy kod R z LaTeX. Więc macie razem analizę i źródło raportu.
Thierry

15

Moja zwięzła odpowiedź:

  1. Starannie pisz swoje funkcje, identyfikując wystarczająco ogólne wyjścia i wejścia;
  2. Ogranicz użycie zmiennych globalnych;
  3. Użyj obiektów S3 i, w stosownych przypadkach, obiektów S4;
  4. Umieść funkcje w pakietach, zwłaszcza gdy Twoje funkcje wywołują C / Fortran.

Uważam, że R jest coraz częściej używany w produkcji, więc zapotrzebowanie na kod wielokrotnego użytku jest większe niż wcześniej. Uważam, że tłumacz jest znacznie bardziej wytrzymały niż wcześniej. Nie ma wątpliwości, że R jest 100-300x wolniejsze niż C, ale zwykle wąskie gardło koncentruje się wokół kilku wierszy kodu, które można przekazać do C / C ++. Myślę, że byłoby błędem delegowanie mocnych stron języka R w manipulacji danymi i analizie statystycznej na inny język. W takich przypadkach spadek wydajności jest niski, a w każdym razie wart oszczędności wysiłku programistycznego. Gdyby liczył się sam czas wykonania, wszyscy pisalibyśmy w asemblerze.


11

Chciałem dowiedzieć się, jak pisać pakiety, ale nie zainwestowałem czasu. Dla każdego z moich mini-projektów przechowuję wszystkie funkcje niskiego poziomu w folderze o nazwie „functions /” i umieszczam je w oddzielnej przestrzeni nazw, którą jawnie tworzę.

Poniższe wiersze kodu utworzą środowisko o nazwie „myfuncs” w ścieżce wyszukiwania, jeśli jeszcze nie istnieje (za pomocą dołączania), i zapełnią je funkcjami zawartymi w plikach .r w moim katalogu „functions /” (używając sys.source). Zwykle umieszczam te wiersze na górze mojego głównego skryptu przeznaczonego dla „interfejsu użytkownika”, z którego wywoływane są funkcje wysokiego poziomu (wywołujące funkcje niskiego poziomu).

if( length(grep("^myfuncs$",search()))==0 )
  attach("myfuncs",pos=2)
for( f in list.files("functions","\\.r$",full=TRUE) )
  sys.source(f,pos.to.env(grep("^myfuncs$",search())))

Kiedy wprowadzasz zmiany, zawsze możesz ponownie źródłować je z tymi samymi wierszami lub użyć czegoś podobnego

evalq(f <- function(x) x * 2, pos.to.env(grep("^myfuncs$",search())))

do oceny dodatków / modyfikacji w utworzonym środowisku.

Wiem, że to niezdarne, ale pozwala uniknąć zbyt formalnego podchodzenia do tego (ale jeśli będziesz miał okazję, zachęcam do korzystania z systemu pakietów - mam nadzieję, że będę migrować w ten sposób w przyszłości).

Jeśli chodzi o konwencje kodowania, to jedyna rzecz, jaką widziałem w odniesieniu do estetyki (lubię je i luźno podążam, ale nie używam zbyt wielu nawiasów klamrowych w R):

http://www1.maths.lth.se/help/R/RCC/

Istnieją inne „konwencje” dotyczące używania [, drop = FALSE] i <- jako operator przypisania sugerowany w różnych prezentacjach (zwykle w przemówieniu) w useR! konferencje, ale nie sądzę, aby żadna z nich była ścisła (chociaż [, drop = FALSE] jest przydatne w przypadku programów, w których nie jesteś pewien oczekiwanego wkładu).


6

Policz mnie jako osobę opowiadającą się za pakietami. Przyznam się, że jestem dość kiepski w pisaniu stron podręcznika i winiet, dopóki nie będę musiał (tj. Zostanę wydany), ale jest to naprawdę wygodny sposób na pakowanie kodu źródłowego. Dodatkowo, jeśli poważnie myślisz o utrzymaniu kodu, wszystkie punkty, które porusza Dirk, wchodzą w grę plya.


4

Również się zgadzam. Aby rozpocząć, użyj funkcji package.skeleton (). Nawet jeśli uważasz, że Twój kod może już nigdy nie zostać uruchomiony, może to zmotywować Cię do stworzenia bardziej ogólnego kodu, który pozwoli Ci zaoszczędzić czas później.

Jeśli chodzi o dostęp do globalnego środowiska, jest to łatwe z operatorem << -, chociaż jest to odradzane.


3

Ponieważ nie nauczyłem się jeszcze pisać pakietów, zawsze organizowałem je poprzez pozyskiwanie skryptów podrzędnych. Jest podobny do lekcji pisania, ale nie jest tak zaangażowany. Nie jest to programowo eleganckie, ale uważam, że gromadzę analizy w czasie. Kiedy już mam dużą działającą sekcję, często przenoszę ją do innego skryptu i po prostu ją umieszczam, ponieważ będzie ona używać obiektów obszaru roboczego. Być może muszę zaimportować dane z kilku źródeł, posortować je wszystkie i znaleźć skrzyżowania. Mógłbym umieścić tę sekcję w dodatkowym skrypcie. Jeśli jednak chcesz rozpowszechniać swoją „aplikację” dla innych osób lub używa ona interaktywnych danych wejściowych, pakiet jest prawdopodobnie dobrą trasą. Jako badacz rzadko muszę rozpowszechniać mój kod analizy, ale CZĘSTO muszę go ulepszać lub poprawiać.


Użyłem tej metody, ale od tego czasu zdałem sobie sprawę, że funkcje i pakiety są lepsze niż źródło („next_script.R”). Pisałem o tym tutaj: stackoverflow.com/questions/25273166/…
Arthur Yip

1

Szukałem również świętego Graala odpowiedniego przepływu pracy do złożenia dużego projektu R. Znalazłem w zeszłym roku ten pakiet o nazwie rsuite i na pewno tego właśnie szukałem. Ten pakiet języka R został specjalnie opracowany do wdrażania dużych projektów języka R, ale odkryłem, że może być używany do projektów o mniejszym, średnim i dużym rozmiarze. Dam linki do rzeczywistych przykładów światowych w minutę (poniżej), ale najpierw chcę wyjaśnić nowy paradygmat budowania projektów R z rsuite.

Uwaga. Nie jestem twórcą ani programistą rsuite.

  1. Z RStudio źle robiliśmy projekty; celem nie powinno być stworzenie projektu lub pakietu, ale szerszy zakres. Zamiast tego tworzysz super-projekt lub projekt główny, który zawiera standardowe projekty języka R i pakiety języka R we wszystkich możliwych kombinacjach.

  2. Posiadając super-projekt R nie potrzebujesz już Uniksa makedo zarządzania niższymi poziomami projektów R poniżej; używasz skryptów R na górze. Pokażę ci. Tworząc główny projekt oprogramowania rsuite, otrzymujesz następującą strukturę folderów:

wprowadź opis obrazu tutaj

  1. Folder Rto miejsce, w którym umieszczasz skrypty zarządzania projektami, te, które zostaną zastąpione make.

  2. Folder packagesto folder, w którym rsuiteznajdują się wszystkie pakiety składające się na super-projekt. Możesz także skopiować i wkleić pakiet, który nie jest dostępny z Internetu, a rsuite również go zbuduje.

  3. folder deployment, gdzie rsuitebędzie zapisywać wszystkie pliki binarne pakiety, które zostały wskazane w pakietach DESCRIPTIONplików. Zatem to sprawia, że ​​projekcja jest całkowicie odtwarzalna w czasie.

  4. rsuitezawiera klienta dla wszystkich systemów operacyjnych. Przetestowałem je wszystkie. Ale możesz także zainstalować go jako addindla RStudio.

  5. rsuiteumożliwia także tworzenie izolowanej condainstalacji w swoim własnym folderze conda. To nie jest środowisko, ale fizyczna instalacja Pythona pochodząca z programu Anaconda na twoim komputerze. Działa to razem z R SystemRequirements, z których możesz zainstalować wszystkie pakiety Pythona, które chcesz, z dowolnego kanału Conda.

  6. Możesz także tworzyć lokalne repozytoria, aby pobierać pakiety R, gdy jesteś w trybie offline lub chcesz zbudować całość szybciej.

  7. Jeśli chcesz, możesz również skompilować projekt R jako plik zip i udostępnić go współpracownikom. Będzie działać, pod warunkiem, że Twoi koledzy mają zainstalowaną tę samą wersję R.

  8. Inną opcją jest zbudowanie kontenera całego projektu w systemie Ubuntu, Debian lub CentOS. Dlatego zamiast udostępniać plik zip z kompilacją projektu, udostępniasz cały Dockerkontener z projektem gotowym do uruchomienia.

Dużo eksperymentowałem, rsuiteszukając pełnej odtwarzalności i unikając zależności pakietów, które instaluje się w środowisku globalnym. Jest to błędne, ponieważ gdy tylko zainstalujesz aktualizację pakietu, projekt najczęściej przestaje działać, szczególnie te pakiety z bardzo określonymi wywołaniami funkcji z określonymi parametrami.

Pierwszą rzeczą, którą zacząłem eksperymentować, były bookdownebooki. Nigdy nie miałem tyle szczęścia, że ​​udało mi się przetrwać próbę czasu dłuższą niż sześć miesięcy. Tak więc przekonwertowałem oryginalny projekt bookdown na zgodny z rsuiteramami. Teraz nie muszę się martwić aktualizacją mojego globalnego środowiska R, ponieważ projekt ma własny zestaw pakietów w deploymentfolderze.

Następną rzeczą, jaką zrobiłem, było tworzenie projektów uczenia maszynowego, ale rsuiteprzeszkadzało. Główny, aranżacyjny projekt u góry, a wszystkie podprojekty i pakiety mają być pod kontrolą głównego. Naprawdę zmienia sposób programowania w R, zwiększając produktywność.

Potem zacząłem pracować w nowej paczce o nazwie rTorch. Było to możliwe w dużej mierze dzięki rsuite; pozwala myśleć i osiągać sukcesy.

Jedna rada. Nauka rsuitenie jest łatwa. Ponieważ przedstawia nowy sposób tworzenia projektów R, wydaje się trudne. Nie przejmuj się pierwszymi próbami, kontynuuj wspinaczkę po zboczu, aż się uda. Wymaga zaawansowanej wiedzy o systemie operacyjnym i systemie plików.

Spodziewam się, że pewnego dnia RStudiobędziemy mogli generować projekty aranżacyjne, takie jak rsuitez menu. Byłoby świetnie.

Spinki do mankietów:

Repozytorium RSuite GitHUb

r4ds bookdown

keras i błyszczący tutorial

moderndive-book-rsuite

interpretable_ml-rsuite

IntroMachineLearningWithR-rsuite

clark-intro_ml-rsuite

hyndman-bookdown-rsuite

statystyczne_rethinking-rsuite

fread-benchmarks-rsuite

dataviz-rsuite

samouczek-segmentacji-handlu-h2o

telco-customer-churn-tutorial

sclerotinia_rsuite


-7

R jest OK do użytku interaktywnego i małych skryptów, ale nie użyłbym go do dużego programu. Używałbym głównego języka do większości programowania i opakowałbym go w interfejs R.


1
Istnieją naprawdę duże pakiety (tj. Programy). Czy poważnie sugerujesz, że powinny zostać przepisane w jakimś innym języku? Czemu???
Eduardo Leoni

4
Jedną z kwestii jest wydajność. Często przepisałem kod R na kod C ++ i uczyniłem go 100x szybszym. Innym jest wsparcie narzędziowe. R nie ma nic porównywalnego do IDE, takich jak Eclipse czy Visual Studio. Wreszcie, jeśli program jest bardzo duży, prawdopodobnie wykonuje zadania niestatystyczne, do których R nie jest dobrze przystosowany.
John D. Cook

2
Dostępna jest wtyczka (Stat-ET), która pozwala Eclipse na interakcję z R. Zgadzam się, że C ++ może działać znacznie szybciej niż R. Ale ile czasu potrzeba na przekodowanie rzeczy R do C ++? O ile nie możesz często ponownie używać kodu, korzyść z szybszego kodu nie jest wiele warta w porównaniu z wysiłkiem związanym z przekodowaniem go w C ++.
Thierry

2
Tak, istnieje kompromis (produktywność a wydajność). A w przypadku czysto analizy danych / prac statystycznych R często wygrywa. Ale jeśli chodzi o pisanie innych zadań, np. GUI, WWW itp., Nie jestem pewien, czy tak jest. Często tworzymy prototypy i pracujemy w R, ale wdrażamy kod produkcyjny w Pythonie / C ++. Z tym drugim otrzymujesz wydajność i bardzo dojrzałe biblioteki / struktury wielokrotnego użytku do różnych zadań. Ale to jest płynna sytuacja, a ekosystem R. stale się rozwija.
ars

Używanie Rcpppakietu, w tym kodu C ++ w programach języka R, staje się dość proste. Zatem przepisanie pewnych fragmentów kodu R może zostać łatwo zintegrowane z R. Ponadto wraz z pojawieniem się RStudio wprowadzono IDE dla języka R, choć może nie tak potężne jak Visual Studio.
Paul Hiemstra,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.