Idiomatikus Python

Raymond Hettinger: 

Transforming Code into Beautiful, Idiomatic Python

Python alapgondolat

A kódot többször olvassuk mint írjuk.

  • jól olvasható
     
  • könnyen érthető
     
  • azt csinálja amit állít hogy csinál (helyes)
     
  • rugalmas, könnyen módosítható
     
  • egyszerű
     
  • rövid ☺
     
  • öndokumentáló

Mitől jó kód a jó kód?

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Python filozófia

Python előnyei

Jól olvasható

  • szignifikáns white space
     
  • nincs kapcsos zárójel:


     
  • Beszédes függvénynevek
    pl. startswith(), endswith(), zfill()
>>> from __future__ import braces
  File "<ipython-input-2-2aebb3fc8ecf>", line 1
    from __future__ import braces
SyntaxError: not a chance

Könnyen érthető

  • nyelvi konstruktorok
    pl.
import json
for line in file:
    print(line)

Egyszerű

class Zoo:
    ...

Könnyen módosítható

def count_banana():
    return 5
class Zoo:
    @staticmethod
    def count_banana():
        return 5

Öndokumentáló

class PokerRange:
    def to_html(self):
        """Returns a 13x13 HTML table representing the hand range."""
"""
    flask
    ~~~~~
    A microframework based on Werkzeug.  It's extensively documented
    and follows best practice patterns.
    :copyright: (c) 2015 by Armin Ronacher.
    :license: BSD, see LICENSE for more details.
"""

__init__.py:

Használjuk ki ezeket az előnyöket!

Iteráció

  • Sokkal egyszerűbb mint más nyelvekben.

Python vs más nyelvek

iterable = ['Jóska', 'Pista', 'Sári', 'Mariska']
for val in iterable:
    print(val)
var iterable = ['Jóska', 'Pista', 'Sári', 'Mariska'];
for (var i = 0; i < iterable.length; i++) {
    console.log(iterable[i]);
}

JavaScript

Python

const char *iterable[4];
iterable[0] = "Jóska";
iterable[1] = "Pista";
iterable[2] = "Sári";
iterable[3] = "Mariska";

int i;
for (i = 0; i < 4; i++) {
    printf ("%s\n", iterable[i]);
}

C

for i in [0, 1, 2, 3, 4, 5]:
    print(i**2)
for i in range(6):
    print(i**2)


# python 2:
for i in xrange(6):
    print(i**2)

Egyszerű számsor bejárása

colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)):
    print(colors[i])
for color in colors:
    print(color)

Lista bejárása

colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)-1, -1, -1):
    print(colors[i])
for color in reversed(colors):
    print(color)

Bejárás visszafelé

i = 0
for color in colors:
    print(i, '-->', color)
    i += 1
for i, color in enumerate(colors):
    print(i, '-->', color)
colors = ['red', 'green', 'blue', 'yellow']

for i in range(len(colors)):
    print(i, '-->', colors[i])

Iterálás indexekkel

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue', 'yellow']

n = min(len(names), len(colors))
for i in range(n):
    print(names[i], '-->', colors[i])

Két collection iterálása egyszerre

for name, color in zip(names, colors):
    print(name, '-->', color)


# python 2:
for name, color in izip(names, colors):
    print(name, '-->', color)
colors = ['red', 'green', 'blue', 'yellow']

for color in sorted(colors):
    print(color)
for color in sorted(colors, reverse=True):
    print(color)

Iterálás sorrendben

# python 2.4+
print(sorted(colors, key=len)
colors = ['red', 'green', 'blue', 'yellow']

# python <2.4
def compare_length(c1, c2):
    if len(c1) < len(c2): return -1
    if len(c1) > len(c2): return 1
    return 0

print sorted(colors, cmp=compare_length)

Iterálás egyedi sorrendben

blocks = []

while True:
    block = f.read(32)
    if block == '':
        break
    blocks.append(block)
def read32():
    return f.read(32)

blocks = []
for block in iter(read32, ''):
    blocks.append(block)
from functools import partial

blocks = []
for block in iter(partial(f.read, 32), ''):
    blocks.append(block)

Függvény hívása bizonyos pontig (sentinel value)

def find(seq, target):
    found = False
    for i, value in enumerate(seq):
        if value == target:
            found = True
            break
    if not found:
        return -1
    return i
def find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break
    else:  # finished the body
        return -1
    return i
def find(seq, target):
    i = -1
    for i, value in enumerate(seq):
        if value == target:
            break
    return i

Több kilépési pont
explicit megkülönböztetése

Dictionary

  • A dictionary használatának elsajátítása
    alapvető Python skill
     
  • Eszköz kapcsolatok, számlálók és csoportok kifejezésére

Dictionary kulcsok bejárása

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
    print(k)
for k in list(d):
    if k.startswith('r'):
        del d[k]

# Python 2:
for k in d.keys():
    if k.startswith('r'):
        del d[k]
d = {k: d[k] for k in d if not k.startswith('r')}

Dictionary kulcsok és értékek bejárása

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}

for k in d:
    print(k, '-->', d[k])
for k, v in d.items():
    print(k, '-->', v)

# Python 2:
for k, v in d.iteritems():
    print k, '-->', v

Dictionary létrehozása
default értékkel

errors = dict.fromkeys(['sid', 'wsid', 'base64'], 0)
# {'errors': 0, 'wsid': 0, 'base64: 0}

Dictionary létrehozása párokból

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']


d = dict(zip(names, colors))
# {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}


# Python 2:
d = dict(izip(names, colors))

Számlálás dictionary-vel

colors = ['red', 'green', 'red', 'blue', 'green', 'red']

d = {}
for color in colors:
    if color not in d:
        d[color] = 0
    d[color] += 1
# {'blue': 1, 'green': 2, 'red': 3}
d = {}
for color in colors:
    d[color] = d.get(color, 0) + 1
from collections import defaultdict

d = defaultdict(int)
for color in colors:
    d[color] += 1

Csoportosítás dictionary-vel

names = ['raymond', 'rachel', 'matthew', 'roger', 
         'betty', 'melissa', 'judith', 'charlie']

d = {}
for name in names:
    key = len(name)
    if key not in d:
        d[key] = []
    d[key].append(name)

# {5: ['roger', 'betty'], 6: ['rachel', 'judith'],
#  7: ['raymond', 'matthew', 'melissa', 'charlie']}
d = {}
for name in names:
    key = len(name)
    d.setdefault(key, []).append(name)
from collections import defaultdict

d = defaultdict(list)
for name in names:
    key = len(names)
    d[key].append(name)

Véletlenszeű elem atomic kinyerése dictionary-ből

names = {'matthew': 'blue', 'rachel': 'green', 
         'raymond': 'red'}

while d:
    key, value = d.popitem()
    print(key, '-->', value)

Több dictionary összekapcsolása

import os
import argparse

defaults = {'color': 'red', 'user': 'guest'}
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args([])
command_line_args = {k: v for k, v in 
                     vars(namespace).items() if v}

d = defaults.copy()
d.update(os.environ)
d.update(command_line_args)
import os
from collections import ChainMap


d = ChainMap(command_line_args, os.environ, defaults)

Tuple, packing, unpacking

  • változók egyidejű változtatása
     
  • kiküszöböl bizonyos hibalehetőségeket
     
  • magasabb szinten való gondolkodást tesz lehetővé

Felsorolás kibontása

p = 'Raymond', 'Hettinger', 0x30, 'python@example.com'

first_name = p[0]
last_name = p[1]
age = p[2]
email = p[3]
first_name, last_name, age, email = p

Több változó változtatása egyszerre

def fibonacci(n):
    x = 0
    y = 1
    for i in range(n):
        print(x)
        t = y
        y = x + y
        x = t
def fibonacci(n):
    x, y = 0, 1
    for i in range(n):
        print(x)
        x, y = y, x+y

List, dictionary, set comprehensions

Egyszerű list létrehozása

numbers = []
for number in range(10):
    numbers.append(number)
numbers = [number for number in range(10)]
numbers = range(10)

dict, set comprehensions

parity = {x: x**2 for x in (2, 4, 6)}
# {2: 4, 4: 16, 6: 36}
even_numbers = {num for num in bigdata if num % 2 == 0}
results = []
for i in range(10):
    s = i ** 2
    resul.append(s)
print(sum(result))
print(sum([i ** 2 for i in range(10)]))

# Python 2:
print sum([i ** 2 for i in xrange(10)])

Számok összeadása

print(sum(i ** 2 for i in range(10)))

# Python 2:
print sum(i ** 2 for i in xrange(10))

Dekorátorok

Dekorátor használata
"adminisztratív logika" szétválasztására

def web_lookup(url, saved({}):
    if url in saved:
        return saved[url]
    page = urllib.urlopen(url).read()
    saved[url] = page
    return page
from functools import lru_cache

@lru_cache
def web_lookup(url):
    return urllib.urlopen(url).read()

Dekorátor használata
"adminisztratív logika" szétválasztására

from functools import wraps

def lru_cache(func):
    saved = {}
    @wraps(func)
    def newfunc(*args):
        if args in saved:
            return newfunc(*args)
        result = func(*args)
        saved[args] = result
        return result
    return newfunc

Command line példa

@main.command(help='Download ELB logs from S3 bucket. '
                   'If minute is passed, it will be used, otherwise only hour '
                   'will be included in prefix.')
@click.argument('wsid', type=ct.WSID)
@click.argument('year', type=ct.YEAR, default='')
@click.argument('month', type=ct.MONTH, default='')
@click.argument('day', type=ct.DAY, default='')
@click.argument('hour', type=ct.HOUR, default='')
@click.argument('minute', type=ct.MINUTE, default='')
@click.pass_context
def download(ctx, wsid, year, month, day, hour, minute):
    """Download ELB logs from S3 bucket."""

Context managerek

Olvasás fileból

with open('big_data.txt') as f:
    data = f.read()
f = open('big_data.txt')
try:
    data = f.read()
finally:
    f.close()

Multi-thread lock

import threading

lock = threading.Lock()

lock.acquire()
try:
    print('Critical section 1')
    print('Critical section 2')
finally:
    lock.release()
import threading

lock = threading.Lock()

with lock:
    print('Critical section 1')
    print('Critical section 2')

Ideiglenes környezet szétválasztása 1.

from decimal import Decimal, getcontext, setcontext

old_context = getcontext().copy()
getcontext().prec = 50
print(Decimal(335) / Decimal(113))
setcontext(old_context)
from decimal import Decimal, Context, localcontext

with localcontext(Context(prec=50)):
    print(Decimal(355) / Decimal(113))

Ideiglenes környezet szétválasztása 2.

with open('pow_help.txt', 'w') as f:
    oldstdout = sys.stdout
    sys.stdout = f
    try:
        help(pow)
    finally:
        sys.stdout = oldstdout
from contextlib import redirect_stdout

with open('pow_help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

Kivételek

  • "Olcsó", vagyis gyors
     
  • Jól látható
     
  • Beszédes stacktrace (főleg Python 3)
     
  • "Natív", (pl. StopIteration)

Hiányzó file probléma

try:
    with open('bigdata.txt') as f:
        bigdata = f.read().strip()
except FileNotFoundError:
    print("File is missing, loading default values")
if os.path.exists('bigdata.txt'):
    with open('bigdata.txt') as f:
        bigdata = f.read().strip()

Gyakori hiba:
minden kivétel elkapása

try:
    do_something()
except:
    pass
try:
    do_something()
except Exception:
    pass
try:
    do_something()
except Exception as e:
    pass

Megoldás:
a kivételek explicit felsorolása

try:
    do_something()
except ValueError as e:
    handle_exception(e)
try:
    read_file()
except FileNotFoundError as e:
    handle_exception(e)

Olvashatóbb kód

  • pozícionális argumentek és indexek

    vs
     
  • kulcsszavakat és nevek

Függvényhívás
keyword argumentekkel

twitter_search('@obama', retweets=False, 
               numtweets=20, popular=True)
twitter_search('@obama', False, 20, True)

named tuple

doctest.testmod()
TestResults(failed=0, attempted=4)
doctest.testmod()
(0, 4)
from collections import namedtuple

TestResults = namedtuple('TestResult', ['failed', 'attempted'])

Teranry conditional operator

number = count if count % 2 != 0 else count - 1
name = user.name() if user is not None else 'Guest'
from collections import Counter

colors = ['red', 'green', 'red', 'blue', 'green', 'red', 'red']
colorcount = Counter(colors)
colorcount.most_common()
# [('red', 4), ('green', 2), ('blue', 1)]

Egy jobb számláló

with open('/some/file', 'r') as f:
    line_count = Counter(f)
for text in lines:
  if re.search(‘[a-zA-Z]\=’, text):
     some_action(text)
  elif re.search(‘[a-zA-Z]\s\=’, text):
     some_other_action(text)
  else:
     some_default_action()

Funckionális eszköz: partial

def is_grouped_together(text):
  return re.search("[a-zA-Z]\s\=", text)

def is_spaced_apart(text):
  return re.search(“[a-zA-Z]\s\=”, text) 

def and_so_on(text):
  return re.search(“pattern_188364625", text)

... 

for text in lines:
  if is_grouped_together(text):
    some_action(text)
  elif is_spaced_apart(text):
    some_other_action(text)
  else:
    some_default_action()
def my_search_method():
  is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
  is_grouped_together = partial(re.search, '[a-zA-Z]\=')
  ...
  
  for text in lines:
    if is_grouped_together(text):
      some_action(text)
    elif is_spaced_apart(text):
      some_other_action(text)
    else:
      some_default_action()

Funckionális eszköz: partial

Hatékonyság

  • Ne mozgassunk/másoljunk adatot
    feleslegesen
    pl. dict.copy(), deepcopy, ezek drága műveletek

Stringek összefűzése

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

s = names[0]
for name in names[1:]:
    s += ', ' + name
print(s)
print(', '.join(names))

Beszúrás felsorolás elejére

names = ['raymond', 'rachel', 'matthew', 'roger',
         'betty', 'melissa', 'judith', 'charlie']

del names[0]
names.pop(0)
names.insert(0, 'mark')
from collections import deque

names = deque(['raymond', 'rachel', 'matthew', 'roger',
               'betty', 'melissa', 'judith', 'charlie'])


del names[0]
names.popleft()
names.appendleft()

e nyi

==

Idiomatikus Python

By Kiss György

Idiomatikus Python

  • 306