Skip to content

Commit bfb7754

Browse files
committed
Add intro lesson about argparse
1 parent bab2894 commit bfb7754

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed

lessons/intro/argparse/index.md

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
argparse
2+
========
3+
4+
Knihovna `argparse` je ve standardní knihovně a slouží k vytváření rozhraní pro příkazovou řádku
5+
(angl. *command line interface*, CLI).
6+
7+
`argparse` toho neumí tolik jako [`click`]( {{ lesson_url('intro/click')}} ), ale pro spoustu
8+
případů bohatě poslouží a každý s Pythonem ji má k dispozici bez nutnosti další instalace.
9+
10+
## Argumenty příkazové řádky
11+
12+
`argparse` není ve standardní knihovně jediným nástrojem na zpracování argumentů příkazové řádky.
13+
Mezi jeho hlavní „konkurenty“ patří: [os.environ], [optparse] — předchůdce `argparse` — a [getopt].
14+
15+
[os.environ]: https://docs.python.org/3/library/os.html#os.environ
16+
[optparse]: https://docs.python.org/3/library/optparse.html
17+
[getopt]: https://docs.python.org/3/library/getopt.html
18+
19+
A pro to úplně nejzákladnější zpracování argumentů nám stačí i modul `sys` a seznam `argv`:
20+
21+
```python
22+
import sys
23+
24+
for index, argument in enumerate(sys.argv):
25+
print(f"Argument číslo {index} je: {argument}")
26+
```
27+
28+
```console
29+
$ python3 test_sys.py ahoj "ja jsem" argument
30+
Argument číslo 0 je: test_sys.py
31+
Argument číslo 1 je: ahoj
32+
Argument číslo 2 je: ja jsem
33+
Argument číslo 3 je: argument
34+
```
35+
36+
`sys.argv` se hodí, pokud potřebujete získat z příkazové řádky třeba jen jeden argument
37+
a program nepotřebuje nápovědu. Na složitější případy už je lepší použít `argparse`.
38+
39+
`argparse` nemá tak striktně definovanou strukturu jako `click`, takže se dá použít
40+
více různými způsoby. Ten úplně nejzákladnější sestává z následujících kroků:
41+
42+
1. Vytvoříme si instanci třídy `ArgumentParser`.
43+
2. Parseru přidáváme argumenty, které bude umět zpracovat.
44+
3. Necháme parser pracovat s tím, co nám přišlo z příkazové řádky.
45+
46+
```python
47+
import argparse
48+
49+
def hello(name):
50+
print(f"Hello {name}!")
51+
52+
if __name__ == '__main__':
53+
# Vytvoříme prázdný parser
54+
parser = argparse.ArgumentParser(description='Sample app')
55+
# Naučíme ho zpracovávat první argument
56+
parser.add_argument("-n", "--name", action="store", dest="name", default="world")
57+
# Necháme jej zpracovat sys.argv
58+
arguments = parser.parse_args()
59+
# Spustíme funkci s argumentem z příkazové řádky
60+
hello(arguments.name)
61+
```
62+
63+
Volání `parser.parse_args()` bez dalších informací zpracovává obsah seznamu `sys.argv`,
64+
ale je i možné při testování zadat seznam argumentů ručně.
65+
66+
Pro každý argument je možné specifikovat celou řadu vlastností. V předchozím příkladu jsou první
67+
dvě možná jména pro nový argument, následuje akce (`store` znamená ulož hodnotu), `dest` určuje,
68+
jak se bude jmenovat atribut, kde bude uložená hodnota k dispozici a `default` nastavuje výchozí
69+
hodnotu pro případ, kdy argument při spuštění skriptu vynecháme.
70+
71+
```console
72+
$ python args.py
73+
Hello world!
74+
$ python args.py -n PyLadies
75+
Hello PyLadies!
76+
$ python args.py --name PyLadies
77+
Hello PyLadies!
78+
$ python args.py --help
79+
usage: args.py [-h] [-n NAME]
80+
81+
Sample app
82+
83+
optional arguments:
84+
-h, --help show this help message and exit
85+
-n NAME, --name NAME
86+
```
87+
88+
Přepínač `--help` pro nápovědu získal náš program automaticky.
89+
90+
## Přepínače
91+
92+
Přepínače jsou v podstatě argumenty bez hodnoty a slouží k zapnutí nebo vypnutí
93+
některé z vlastností programu. Například `--help` je také přepínač, který zobrazí nápovědu
94+
a program ukončí, aniž by cokoli udělal.
95+
96+
Pro přepínače se používají akce `store_true` a `store_false`. `store_true` uloží
97+
do proměnné `True`, pokud bude přepínač zadán na příkazové řádce, a naopak `store_false`
98+
uloží `False` a hodí se tedy spíše pro vypnutí některé z vlastností.
99+
100+
```python
101+
import argparse
102+
103+
def hello(name, upper, ex_mark):
104+
string = f"Hello {name}"
105+
if upper:
106+
string = string.upper()
107+
if ex_mark:
108+
string += "!"
109+
print(string)
110+
111+
if __name__ == '__main__':
112+
parser = argparse.ArgumentParser(description='Sample app')
113+
114+
parser.add_argument("-n", "--name", action="store", dest="name", default="world")
115+
parser.add_argument("-u", "--uppercase", action="store_true", dest="upper")
116+
parser.add_argument("-E", "--no-exclamation-mark", action="store_false", dest="ex_mark")
117+
118+
arguments = parser.parse_args()
119+
120+
hello(arguments.name, arguments.upper, arguments.ex_mark)
121+
```
122+
123+
Protože `store_true` má výchozí hodnotu `False` a opačně, definice argumentů výše
124+
nám na chování programu bez jejich použití nic nezmění.
125+
126+
```console
127+
$ python args.py -n PyLadies
128+
Hello PyLadies!
129+
```
130+
131+
Použijeme-li je, ovlivní nám podobu výstupu:
132+
133+
```console
134+
$ python args.py -n PyLadies --upper
135+
HELLO PYLADIES!
136+
$ python args.py -n PyLadies --upper -E
137+
HELLO PYLADIES
138+
$ python args.py -E
139+
Hello world
140+
```
141+
142+
Výhodou parseru je, že můžeme argumenty použít v libovolném pořadí:
143+
144+
```console
145+
$ python args.py -E --upper --name Ostrava
146+
HELLO OSTRAVA
147+
```
148+
149+
## Poziční argumenty
150+
151+
Stejně jako u funkcí mohou být i v příkazové řádce argumenty nepojmenované.
152+
Používají se ve dvou případech: pro povinné parametry a pro parametry, kterých
153+
může být zadán libovolný počet.
154+
Na všechno ostatní radši použijte přepínače.
155+
156+
Například příkaz `cd` potřebuje jeden argument: jméno adresáře,
157+
do kterého má přepnout.
158+
Jeho rozhraní by mohlo vypadat takto:
159+
160+
```python
161+
import argparse
162+
163+
def cd(dir):
164+
print(f"Changing to directory to {dir}!")
165+
166+
if __name__ == '__main__':
167+
parser = argparse.ArgumentParser(description='Sample app')
168+
parser.add_argument("directory")
169+
arguments = parser.parse_args()
170+
171+
cd(arguments.directory)
172+
```
173+
174+
```console
175+
$ python args.py
176+
usage: args.py [-h] directory
177+
args.py: error: the following arguments are required: directory
178+
$ python args.py new_folder
179+
Changing to directory new_folder
180+
$ python args.py --help
181+
usage: args.py [-h] directory
182+
183+
Sample app
184+
185+
positional arguments:
186+
directory
187+
188+
optional arguments:
189+
-h, --help show this help message and exit
190+
```
191+
192+
Argument `directory` je poziční a povinný. Spuštění bez něj skončí chybou.
193+
Za povšimnutí stojí, že nepovinné přepínače z minulých příkladů se v nápovědě
194+
vždy zobrazovaly v hranatých závorkách.
195+
196+
Proměnný počet argumentů se zadává pomocí `nargs`. Možné hodnoty jsou:
197+
198+
* `N` — přesný počet vyjádřený číslem
199+
* `?` — žádný nebo jeden argument
200+
* `*` — libovolné množství argumentů včetně 0
201+
* `+` — libovolné množství, ale minimálně jeden argument
202+
203+
Můžeme například nechat náš program pozdravit hned několikrát, ale minimálně jednou.
204+
205+
```python
206+
import argparse
207+
208+
def hello(names, upper, ex_mark):
209+
for name in names:
210+
string = f"Hello {name}"
211+
if upper:
212+
string = string.upper()
213+
if ex_mark:
214+
string += "!"
215+
print(string)
216+
217+
218+
if __name__ == '__main__':
219+
parser = argparse.ArgumentParser(description='Sample app')
220+
221+
parser.add_argument("names", nargs="+")
222+
parser.add_argument("-u", "--uppercase", action="store_true", dest="upper")
223+
parser.add_argument("-E", "--no-exclamation-mark", action="store_false", dest="ex_mark")
224+
225+
arguments = parser.parse_args()
226+
227+
hello(arguments.names, arguments.upper, arguments.ex_mark)
228+
```
229+
230+
Takový program už nebude možné spustit bez alespoň jednoho nepojmenovaného argumentu
231+
a bude možné jich zadat hned několik. Tato skutečnost se zrcadlí i v nápovědě.
232+
233+
```console
234+
$ python args.py
235+
usage: args.py [-h] [-u] [-E] names [names ...]
236+
args.py: error: the following arguments are required: names
237+
238+
$ python args.py --help
239+
usage: args.py [-h] [-u] [-E] names [names ...]
240+
241+
Sample app
242+
243+
positional arguments:
244+
names
245+
246+
optional arguments:
247+
-h, --help show this help message and exit
248+
-u, --uppercase
249+
-E, --no-exclamation-mark
250+
251+
$ python args.py PyLadies Ostrava Pythonistas
252+
Hello PyLadies!
253+
Hello Ostrava!
254+
Hello Pythonistas!
255+
```
256+
257+
## Typy a validace
258+
259+
Prozatím všechny argumenty z příkazové řádky dostal náš program jako řetězce.
260+
Toto chování lze jednoduše změnit nastavením `type`.
261+
262+
Například u jednoduchého dělení s dvěma povinnými argumenty se nám více
263+
hodí mít oba jako desetinná čísla namísto řetězců.
264+
265+
```python
266+
import argparse
267+
268+
def deleni(a, b):
269+
print(a / b)
270+
271+
272+
if __name__ == '__main__':
273+
parser = argparse.ArgumentParser(description='Sample app')
274+
parser.add_argument("a", type=float)
275+
parser.add_argument("b", type=float)
276+
arguments = parser.parse_args()
277+
278+
deleni(arguments.a, arguments.b)
279+
```
280+
281+
```console
282+
$ python deleni.py 10 5
283+
2.0
284+
285+
$ python deleni.py 10 5.5
286+
1.8181818181818181
287+
```
288+
289+
`type` bere jakoukoli funkci, kterou pak řetězec s hodnotou argumentu prožene
290+
a uloží si výsledek. Takže nejen `int`, `bool` a `float`, ale i libovolná vlastní funkce
291+
zde může sloužit nejen pro změnu typu argumentu ale třeba také pro jeho validaci nebo libovolné
292+
další úpravy.
293+
294+
```python
295+
import argparse
296+
297+
def validate_email(address):
298+
if "@" not in address:
299+
raise argparse.ArgumentTypeError("Missing @ in email address")
300+
if not address.endswith("@python.cz"):
301+
raise argparse.ArgumentTypeError("Recipient address not from our domain")
302+
return address
303+
304+
def send_email(address):
305+
print(f"Sending email to {address}")
306+
307+
if __name__ == '__main__':
308+
parser = argparse.ArgumentParser(description='Sample app')
309+
parser.add_argument("address", type=validate_email)
310+
arguments = parser.parse_args()
311+
312+
send_email(arguments.address)
313+
```
314+
315+
```console
316+
$ python email.py tomas
317+
usage: email.py [-h] address
318+
email.py: error: argument address: Missing @ in email address
319+
320+
$ python email.py [email protected]
321+
usage: email.py [-h] address
322+
email.py: error: argument address: Recipient address not from out domain
323+
324+
$ python email.py [email protected]
325+
Sending email to [email protected]
326+
```
327+
328+
Jako bonus způsobí vyvolání výjimky `ArgumentTypeError` automatické zobrazení nápovědy.
329+
330+
## Soubory
331+
332+
Speciálním typem argumentu jsou cesty k souborům. Je samozřejmě možné
333+
je zpracovávat jako řetězce, ale není to nejlepší nápad a je to zbytečně pracné.
334+
335+
`argparse` má pro tyto účely speciální `FileType`, který nám soubor také rovnou otevře.
336+
337+
```python
338+
import argparse
339+
340+
def row_count(file):
341+
n = len(file.read().splitlines())
342+
print(f"Row count: {n}")
343+
344+
345+
if __name__ == '__main__':
346+
parser = argparse.ArgumentParser(description='Sample app')
347+
parser.add_argument("file", type=argparse.FileType(mode="r"))
348+
arguments = parser.parse_args()
349+
350+
row_count(arguments.file)
351+
```
352+
353+
```console
354+
$ python soubor.py
355+
usage: soubor.py [-h] file
356+
soubor.py: error: the following arguments are required: file
357+
358+
$ python soubor.py deleni.py
359+
Row count: 13
360+
361+
$ python soubor.py args.py
362+
Row count: 12
363+
364+
$ python soubor.py neexistujici
365+
usage: soubor.py [-h] file
366+
soubor.py: error: argument file: can't open 'neexistujici': [Errno 2] No such file or directory: 'neexistujici'
367+
```
368+
369+
## A další
370+
371+
Nejedná se o vyčerpávající popis možností modulu `argparse`, ale spíše ukázku.
372+
Vše ostatní je jako obvykle dostupné v [dokumentaci].
373+
374+
Jak je vidět, i se standardní knihovnou si v mnoha případech vystačíme.
375+
O důvod více dobře zvážit, zda jsou externí závislosti opravdu potřeba.
376+
377+
[dokumentaci]: https://docs.python.org/3/library/argparse.html

lessons/intro/argparse/info.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
title: argparse – Rozhraní pro příkazovou řádku
2+
style: md
3+
attribution: Pro kurz pokročilého Pythonu napsal Lumír Balhar 2020.
4+
license: cc-by-sa-40

0 commit comments

Comments
 (0)