In the previous episodes

  • basic stuff, syntax
  • functions (wrote our cool min function) + functional stuff
  • scopes, PEP-8
  • strings, bytes
  • collections
  • classes basics
  • decorators, functools
  • exceptions, context managers
  • iterators

👻 Generators


👇 Generator

>>> def g():
        print("Starting")
        x = 42
        yield x
        x += 1
        yield x
        print("Done")
>>> type(g)
<class 'function'>
>>> gen = g()
>>> type(gen)
<class 'generator'>
>>> next(gen)
Starting    
42
>>> next(gen)
43

after yield it stops and comes back later


unique

>>> def unique(iterable, seen=None):
        seen = set(seen or [])
        for item in iterable:
            if item not in seen:
                seen.add(item)
                yield item
   
>>> xs = [1, 1, 2, 3]
>>> unique(xs)
<generator object unique at 0x1027c5798>
>>> list(unique(xs))
[1, 2, 3]
>>> 1 in unique(xs)
True

map

>>> def map(func, iterable, *rest):
        for args in zip(iterable, *rest):
            yield func(*args)
   
>>> xs = range(5)
>>> map(lambda x: x * x, xs)
<generator object map at 0x103122510>
>>> list(map(lambda x: x * x, xs))
[0, 1, 4, 9, 16]
>>> 9 in map(lambda x: x * x, xs)
True

chain

>>> def chain(*iterables):
        for iterable in iterables:
            for item in iterable:
                yield item
   
>>> xs = range(3)
>>> ys = [42]
>>> chain(xs, ys)
<generator object chain at 0x10311d708>
>>> list(chain(xs, ys))
[0, 1, 2, 3, 42]
>>> 42 in chain(xs, ys)
True

count

>>> def count(start=0):
        while True:
            yield start
            start += 1
   
>>> next(count())
0
>>> counter = count()
>>> next(counter)
0
>>> next(counter)
1

How to reuse: don’t.

>>> def g():
    yield 42
   
>>> gen = g()
>>> list(gen)
[42]
>>> list(gen) # WASTED
[]

or use tee from ìtertools


yield

>>> def g():
        res = yield # entrypoint 1 # ???
        print("Got {!r}".format(res))
        res = yield 42 # entrypoint 2
        print("Got {!r}".format(res))
   
>>> gen = g()
>>> next(gen) # until first yield
>>> next(gen) # until second yield
Got 'None'
42
>>> next(gen) # until the end
Got 'None'
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration

send!

>>> gen = g()
>>> gen.send("foobar")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send [   ] to a just-started generator
>>> gen = g()
>>> gen.send(None) # ≡ next(gen)
>>> gen.send("foobar")
Got 'foobar'
42

throw

>>> def g():
        try:
            yield 42
        except ValueError as e:
            yield e
        finally:
            print("Done")
   
>>> gen = g()
>>> next(gen)
42
>>> gen.throw(ValueError, "something is wrong")
ValueError('something is wrong',)
>>> gen = g()
>>> next(gen)
42
>>> gen.close()
Done

and close()


yield from and return

>>> def f():
        yield 42
        return []
   
>>> def g():
        res = yield from f()
        print("Got {!r}".format(res))
   
>>> gen = g()
>>> next(gen)
42
>>> next(gen, None)
Got []

generators and context managers

import tempfile
import shutil
 
@contextmanager
def tempdir(): # __init__
    outdir = tempfile.mkdtemp() # __enter__
    try:
        yield outdir # ---------
    finally:
        shutil.rmtree(outdir) # __exit__
 
with tempdir() as path:
    print(path)

useful example?


Generators wrap-up

  • generator is a function which use yield and yield from
  • generators are everywhere
  • generators can be used as iterators, coroutines, easy context managers and others!

🔨 itertools


islice

>>> from itertools import islice
>>> xs = range(10)
>>> list(islice(xs, 3)) # ≡ xs[:3]
[0, 1, 2]
>>> list(islice(xs, 3, None)) # ≡ xs[3:]
[3, 4, 5, 6, 7, 8, 9]
>>> list(islice(xs, 3, 8, 2)) # ≡ xs[3:8:2]
[3, 5, 7]

count, cycle, repeat

>>> def take(n, iterable):
        return list(islice(iterable, n))
   
>>> list(take(range(10), 3))
[0, 1 2]
>>> from itertools import count, cycle, repeat
>>> take(3, count(0, 5))
[0, 5, 10]
>>> take(3, cycle([1, 2, 3]))
[1, 2, 3]
>>> take(3, repeat(42))
[42, 42, 42]
>>> take(3, repeat(42, 2)) # 2 is number of repeats here
[42, 42]

chain

>>> from itertools import chain
>>> take(5, chain(range(2), range(5, 10)))
[0, 1, 5, 6, 7]
>>> it = (range(x, x ** x) for x in range(2, 4))
>>> take(5, chain.from_iterable(it))
[2, 3, 3, 4, 5]

chain.from_iterable(it) vs chain(*it)?


tee

>>> from itertools import tee
>>> it = range(3)
>>> a, b, c = tee(it, 3)
>>> list(a), list(b), list(c)
([0, 1, 2], [0, 1, 2], [0, 1, 2])

don’t use it after copying


combinatorics!

>>> list(itertools.product("AB", repeat=2))
[('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')]
>>> list(itertools.product("AB", repeat=3))
[('A', 'A', 'A'), ('A', 'A', 'B'), ('A', 'B', 'A'),    ]
>>> list(itertools.permutations("AB"))
[('A', 'B'), ('B', 'A')]
>>> from itertools import combinations, \n    combinations_with_replacement
>>> list(combinations("ABC", 2))
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(combinations_with_replacement("ABC", 2))
[('A', 'A'), ('A', 'B'), ('A', 'C'),
('B', 'B'), ('B', 'C'), ('C', 'C')]

itertools wrap-up

  • useful
  • combinatorics,
  • islice,
  • count, cycle, repeat
  • tee

➡️ Modules, imports and other stuff


aYylScG.jpeg

http://www.dabeaz.com/modulepackage/


1️⃣ Modules


module.py

"""I'm a module."""
 
some_variable = "variable"
 
 
def foo():
    return 42

with tempfile

>>> import useful
>>> dir(module)
[   , '__cached__', '__doc__', '__file__', '__name__',
'foo', 'some_variable']

python ./module.py

def test():
    assert boo() == 4
 
 
if __name__ == "__main__":
    print("Running tests     ")
    test()
    print("OK")

import

>>> import module # исполняет модуль сверху вниз
>>> module
<module 'module' from './module.py'>
>>> import module as alias
>>> alias
<module 'module' from './module.py'>
>>> from useful import foo as boo, some_variable
>>> foo()
42
>>> some_variable
'variable'

sys.path

>>> import sys
>>> sys.path
['', '/usr/local/lib/python3.8',    ]

Modules wrap-up

  • module is a *.py file from which you cam import from
  • import from, import as
  • byte code is executed from up to down
  • sort imports
from collections import OrderedDict
from itertools import islice
import os
import sys

2️⃣ Packages


package package

package
├── __init__.py # !
├── bar.py
└── foo.py
>>> import package
>>> package
<module 'package' from './package/__init__.py'>
>>> package.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'foo'
>>> package.bar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'bar'
>>> from package import bar
>>> bar
<module 'package.bar' from './package/bar.py'>

absolute / relative import

import package.foo
from package import bar
from . import foo, bar  # don't, personal opinion :)

executable package

# package/__init__.py
print("package.__init__")
 
# package/__main__.py
print("It works!")

Now we can run it!

$ python -m package
package.__init__ # ?
It works!

Packages wrap-up

  • package is a way to group Python code
  • every directory with __init__.py makes a package
  • use absolute imports
  • adding __main__.py allows you to execute your package

🏁 Any questions?