""" Cycler ======
Cycling through combinations of values, producing dictionaries.
You can add cyclers::
from cycler import cycler cc = (cycler(color=list('rgb')) + cycler(linestyle=['-', '--', '-.'])) for d in cc: print(d)
Results in::
{'color': 'r', 'linestyle': '-'} {'color': 'g', 'linestyle': '--'} {'color': 'b', 'linestyle': '-.'}
You can multiply cyclers::
from cycler import cycler cc = (cycler(color=list('rgb')) * cycler(linestyle=['-', '--', '-.'])) for d in cc: print(d)
Results in::
{'color': 'r', 'linestyle': '-'} {'color': 'r', 'linestyle': '--'} {'color': 'r', 'linestyle': '-.'} {'color': 'g', 'linestyle': '-'} {'color': 'g', 'linestyle': '--'} {'color': 'g', 'linestyle': '-.'} {'color': 'b', 'linestyle': '-'} {'color': 'b', 'linestyle': '--'} {'color': 'b', 'linestyle': '-.'} """
unicode_literals)
""" Helper function to compose cycler keys
Parameters ---------- left, right : iterable of dictionaries or None The cyclers to be composed Returns ------- keys : set The keys in the composition of the two cyclers """ raise ValueError("Can not compose overlapping cycles")
""" Composable cycles
This class has compositions methods:
``+`` for 'inner' products (zip)
``+=`` in-place ``+``
``*`` for outer products (itertools.product) and integer multiplication
``*=`` in-place ``*``
and supports basic slicing via ``[]``
Parameters ---------- left : Cycler or None The 'left' cycler
right : Cycler or None The 'right' cycler
op : func or None Function which composes the 'left' and 'right' cyclers.
""" return cycle(self)
"""Semi-private init
Do not use this directly, use `cycler` function instead. """ self._left = Cycler(left._left, left._right, left._op) # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors self._left = [copy.copy(v) for v in left] else:
self._right = Cycler(right._left, right._right, right._op) # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors self._right = [copy.copy(v) for v in right] else:
def keys(self): """ The keys this Cycler knows about """
""" Change a key in this cycler to a new name. Modification is performed in-place.
Does nothing if the old key is the same as the new key. Raises a ValueError if the new key is already a key. Raises a KeyError if the old key isn't a key.
""" if new in self._keys: raise ValueError("Can't replace %s with %s, %s is already a key" % (old, new, new)) if old not in self._keys: raise KeyError("Can't replace %s with %s, %s is not a key" % (old, new, old))
self._keys.remove(old) self._keys.add(new)
if self._right is not None and old in self._right.keys: self._right.change_key(old, new)
# self._left should always be non-None # if self._keys is non-empty. elif isinstance(self._left, Cycler): self._left.change_key(old, new) else: # It should be completely safe at this point to # assume that the old key can be found in each # iteration. self._left = [{new: entry[old]} for entry in self._left]
""" Compose the 'left' and 'right' components of this cycle with the proper operation (zip or product as of now) """ for a, b in self._op(self._left, self._right): out = dict() out.update(a) out.update(b) yield out
def _from_iter(cls, label, itr): """ Class method to create 'base' Cycler objects that do not have a 'right' or 'op' and for which the 'left' object is not another Cycler.
Parameters ---------- label : str The property key.
itr : iterable Finite length iterable of the property values.
Returns ------- cycler : Cycler New 'base' `Cycler` """
# TODO : maybe add numpy style fancy slicing if isinstance(key, slice): trans = self.by_key() return reduce(add, (_cycler(k, v[key]) for k, v in six.iteritems(trans))) else: raise ValueError("Can only use slices with Cycler.__getitem__")
return self._compose()
""" Pair-wise combine two equal length cycles (zip)
Parameters ---------- other : Cycler The second Cycler """ if len(self) != len(other): raise ValueError("Can only add equal length cycles, " "not {0} and {1}".format(len(self), len(other))) return Cycler(self, other, zip)
""" Outer product of two cycles (`itertools.product`) or integer multiplication.
Parameters ---------- other : Cycler or int The second Cycler or integer """ if isinstance(other, Cycler): return Cycler(self, other, product) elif isinstance(other, int): trans = self.by_key() return reduce(add, (_cycler(k, v*other) for k, v in six.iteritems(trans))) else: return NotImplemented
return self * other
op_dict = {zip: min, product: mul} if self._right is None: return len(self._left) l_len = len(self._left) r_len = len(self._right) return op_dict[self._op](l_len, r_len)
""" In-place pair-wise combine two equal length cycles (zip)
Parameters ---------- other : Cycler The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot += with a non-Cycler object") # True shallow copy of self is fine since this is in-place old_self = copy.copy(self) self._keys = _process_keys(old_self, other) self._left = old_self self._op = zip self._right = Cycler(other._left, other._right, other._op) return self
""" In-place outer product of two cycles (`itertools.product`)
Parameters ---------- other : Cycler The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot *= with a non-Cycler object") # True shallow copy of self is fine since this is in-place old_self = copy.copy(self) self._keys = _process_keys(old_self, other) self._left = old_self self._op = product self._right = Cycler(other._left, other._right, other._op) return self
""" Check equality """ if len(self) != len(other): return False if self.keys ^ other.keys: return False
return all(a == b for a, b in zip(self, other))
def __repr__(self): op_map = {zip: '+', product: '*'} if self._right is None: lab = self.keys.pop() itr = list(v[lab] for v in self) return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) else: op = op_map.get(self._op, '?') msg = "({left!r} {op} {right!r})" return msg.format(left=self._left, op=op, right=self._right)
# an table showing the value of each key through a full cycle output = "<table>" sorted_keys = sorted(self.keys, key=repr) for key in sorted_keys: output += "<th>{key!r}</th>".format(key=key) for d in iter(self): output += "<tr>" for k in sorted_keys: output += "<td>{val!r}</td>".format(val=d[k]) output += "</tr>" output += "</table>" return output
"""Values by key
This returns the transposed values of the cycler. Iterating over a `Cycler` yields dicts with a single value for each key, this method returns a `dict` of `list` which are the values for the given key.
The returned value can be used to create an equivalent `Cycler` using only `+`.
Returns ------- transpose : dict dict of lists of the values for each key. """
# TODO : sort out if this is a bottle neck, if there is a better way # and if we care.
# change this to dict comprehension when drop 2.6
# for back compatibility
"""Simplify the Cycler
Returned as a composition using only sums (no multiplications)
Returns ------- simple : Cycler An equivalent cycler using only summation""" # TODO: sort out if it is worth the effort to make sure this is # balanced. Currently it is is # (((a + b) + c) + d) vs # ((a + b) + (c + d)) # I would believe that there is some performance implications
trans = self.by_key() return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans)))
"""Concatenate this cycler and an other.
The keys must match exactly.
This returns a single Cycler which is equivalent to `itertools.chain(self, other)`
Examples --------
>>> num = cycler('a', range(3)) >>> let = cycler('a', 'abc') >>> num.concat(let) cycler('a', [0, 1, 2, 'a', 'b', 'c'])
Parameters ---------- other : `Cycler` The `Cycler` to concatenate to this one.
Returns ------- ret : `Cycler` The concatenated `Cycler` """ return concat(self, other)
"""Concatenate two cyclers.
The keys must match exactly.
This returns a single Cycler which is equivalent to `itertools.chain(left, right)`
Examples --------
>>> num = cycler('a', range(3)) >>> let = cycler('a', 'abc') >>> num.concat(let) cycler('a', [0, 1, 2, 'a', 'b', 'c'])
Parameters ---------- left, right : `Cycler` The two `Cycler` instances to concatenate
Returns ------- ret : `Cycler` The concatenated `Cycler` """ if left.keys != right.keys: msg = '\n\t'.join(["Keys do not match:", "Intersection: {both!r}", "Disjoint: {just_one!r}"]).format( both=left.keys & right.keys, just_one=left.keys ^ right.keys)
raise ValueError(msg)
_l = left.by_key() _r = right.by_key() return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys))
""" Create a new `Cycler` object from a single positional argument, a pair of positional arguments, or the combination of keyword arguments.
cycler(arg) cycler(label1=itr1[, label2=iter2[, ...]]) cycler(label, itr)
Form 1 simply copies a given `Cycler` object.
Form 2 composes a `Cycler` as an inner product of the pairs of keyword arguments. In other words, all of the iterables are cycled simultaneously, as if through zip().
Form 3 creates a `Cycler` from a label and an iterable. This is useful for when the label cannot be a keyword argument (e.g., an integer or a name that has a space in it).
Parameters ---------- arg : Cycler Copy constructor for Cycler (does a shallow copy of iterables).
label : name The property key. In the 2-arg form of the function, the label can be any hashable object. In the keyword argument form of the function, it must be a valid python identifier.
itr : iterable Finite length iterable of the property values. Can be a single-property `Cycler` that would be like a key change, but as a shallow copy.
Returns ------- cycler : Cycler New `Cycler` for the given property
""" raise TypeError("cyl() can only accept positional OR keyword " "arguments -- not both.")
if not isinstance(args[0], Cycler): raise TypeError("If only one positional argument given, it must " " be a Cycler instance.") return Cycler(args[0]) elif len(args) > 2: raise TypeError("Only a single Cycler can be accepted as the lone " "positional argument. Use keyword arguments instead.")
if kwargs: return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs)))
raise TypeError("Must have at least a positional OR keyword arguments")
""" Create a new `Cycler` object from a property name and iterable of values.
Parameters ---------- label : hashable The property key.
itr : iterable Finite length iterable of the property values.
Returns ------- cycler : Cycler New `Cycler` for the given property """ keys = itr.keys if len(keys) != 1: msg = "Can not create Cycler from a multi-property Cycler" raise ValueError(msg)
lab = keys.pop() # Doesn't need to be a new list because # _from_iter() will be creating that new list anyway. itr = (v[lab] for v in itr)
|