Language Features II¶
Properties¶
Properties are useful when you want to disguise a function as a variable. This allows cleaner abstractions over the internal representation. In the following example, we use properties to provide access to cartesian and polar coordinates of a point in two-dimensional space. To the user, it looks like Point2DMutable
is storing x
, y
, r
and phi
all at once. However, Point2DMutable
only stores the cartesian representation since storing both representation would be redundant. Note that the conversion taking place in the background can lead to numerical instability in some situations.
import math
class Point2DMutable:
__slots__ = '_x', '_y'
# no defaults!
def __init__(self, x, y):
# no redundant state
self._x = x
self._y = y
def __str__(self):
return f"{self._x}, {self._y}"
def __repr__(self):
return f"Point2DMutable({self._x}, {self._y})"
def __eq__(self, other):
return (self._x, self._y) == (other._x, other._y)
def __ne__(self, other):
return not self.__eq__(other)
def dist(self, p):
return math.hypot(self._x - p._x, self._y - p._y)
@staticmethod
def from_polar(r, phi):
point = Point2DMutable(0, 0)
point._set_from_polar(r, phi)
return point
def _set_from_polar(self, r, phi):
self._x = math.cos(phi) * r
self._y = math.sin(phi) * r
@property
def x(self):
return self._x
@x.setter
def x(self, x):
self._x = x
@property
def y(self):
return self._y
@y.setter
def y(self, y):
self._y = y
@property
def r(self):
return math.hypot(self._x, self._y)
@r.setter
def r(self, r):
self._set_from_polar(r, self.phi)
@property
def phi(self):
return math.atan2(self._y, self._x)
@phi.setter
def phi(self, phi):
self._set_from_polar(self.r, phi)
p = Point2DMutable(2, 3)
p.x
2
p.r *= 2
p.x
4.000000000000001
p.phi = 0
p
Point2DMutable(7.21110255092798, 0.0)
Assignment LF5¶
Getter ersetzen Ersetzen Sie die Getter in
MyFraction
durch Properties.
KNN Schreiben Sie eine Funktion, die eine Liste von Punkten, einen Punkt p und eine Zahl k nimmt und aus der Liste von Punkten k Punkte heraussucht, die die kleinste Distanz zu p haben.
import heapq
def k_nearest_neighbors(point, k, candidates):
return heapq.nsmallest(k, candidates, key=point.dist)
points = [
Point2DMutable(1, 2),
Point2DMutable(2, 3),
Point2DMutable(5, 7),
Point2DMutable(7, 5),
Point2DMutable(3, 10),
Point2DMutable(12, -2),
Point2DMutable(2, 15),
]
k_nearest_neighbors(Point2DMutable(0, 0), 4, points)
[Point2DMutable(1, 2),
Point2DMutable(2, 3),
Point2DMutable(5, 7),
Point2DMutable(7, 5)]
Context Managers¶
https://docs.python.org/3/library/stdtypes.html#typecontextmanager
The primary use of Context Managers is to properly dispose of resources or handle exceptions automatically. open
returns a Context Manager that automatically closes the file when the with
block ends.
Context Managers can be used as implicit parameters or as a local modification of module-global state:
import myfraction
frac = myfraction.MyFraction()
with myfraction.string_style(myfraction.DECIMAL):
print(frac)
Be careful when using global state with multithreading!
import math
import functools
# representational constants
# see context managers chapter for details
FRACTIONAL, DECIMAL = range(2)
# this is a module-level global variable
# see context managers chapter for details
_STRING_REPRESENTATION = FRACTIONAL
# see context managers chapter for details
class string_style:
def __init__(self, style):
self._new_style = style
self._old_style = None
def __enter__(self):
global _STRING_REPRESENTATION
self._old_style = _STRING_REPRESENTATION
_STRING_REPRESENTATION = self._new_style
return self
def __exit__(self, tp, value, traceback):
global _STRING_REPRESENTATION
_STRING_REPRESENTATION = self._old_style
# re-raise exceptions
return False
# see decorators chapter for details on total_ordering
@functools.total_ordering
class MyFraction:
# default parameters help with conversion from integers
def __init__(self, num=0, denom=1):
# prevent illegal instances from being created
# use assert denom != 0 only if you trust the user
if denom == 0:
# do not print error messages
# print("ERROR")
# always raise exceptions
raise ValueError("Denominator cannot be zero.")
self._num = num
self._denom = denom
# if you forget this, equality test will not work consistently
self._cancel()
# we will improve this using properties later on
def get_num(self):
return self._num
def get_denom(self):
return self._denom
# __eq__ is required for equality checks
# for simplicity, this requires "other" to be a MyFraction object
# Python's built-in Fraction supports comparisons with ints and floats, too
def __eq__(self, other):
# delegate to tuple equality instead of bothering with
# return self._num == other._num and ...
return (self._num, self._denom) == (other._num, other._denom)
# delegate to __eq__ for consistency
def __ne__(self, other):
return not self.__eq__(other)
# without aggressive cancelling,
# MyFraction(1, 2) is not equal to MyFraction(2, 4)
# cancelling can only be guaranteed through encapsulation
def _cancel(self):
gcd = math.gcd(self._num, self._denom)
self._num //= gcd
self._denom //= gcd
def __lt__(self, other):
return self._num * other._denom < other._num * self._denom
# <= > >= generated by functools
# do not modify self or other, always create a new object!
def __add__(self, other):
new_num = self._num * other._denom + other._num * self._denom
new_denom = self._denom * other._denom
return MyFraction(new_num, new_denom)
# subtract and unary minus -> assignment
def __neg__(self):
return MyFraction(-self._num, self._denom)
def __sub__(self, other):
return self + (-other)
def __mul__(self, other):
return MyFraction(
self._num * other._num,
self._denom * other._denom
)
# divide, invert -> assignment
# also implement __radd__, ... if your addition supports types other than MyFraction
# without this, MyFraction cannot be used in sets and dicts
# this only makes sense for immutable values (see example below)
def __hash__(self):
return hash((self._num, self._denom))
# the built-in print function uses "str" internally
def __str__(self):
# alternative implementations
#return str(self._num) + "/" + str(self._denom)
#return "{}/{}".format(self._num, self._denom)
#return f"{self._num}/{self._denom}"
# allow switching between fractional and decimal representation
return f"{self._num}/{self._denom}" if _STRING_REPRESENTATION == FRACTIONAL else f"{float(self)}"
# jupyter notebook uses "repr" for printing values
def __repr__(self):
# remember the repr contract
return f"MyFraction({self._num!r}, {self._denom!r})"
# this is required for
# if MyFraction(): ...
def __bool__(self):
# you can use implicit conversion
# return bool(self._num)
# but prefer explicit when possible
return self._num != 0
def __float__(self):
# remember: floats can overflow
try:
return self._num / self._denom
except OverflowError as oe:
# provide a nice error message on top of the stack trace
raise ValueError("This fraction cannot be represented as a float") from oe
@staticmethod
def from_float(flt):
num, denom = flt.as_integer_ratio()
return MyFraction(num, denom)
@staticmethod
def from_str(s):
split = s.split("/")
if len(split) == 1:
return MyFraction(int(split[0]))
elif len(split) == 2:
return MyFraction(int(split[0]), int(split[1]))
else:
raise ValueError("Illegal fraction format")
def with_num(self, new_num):
return MyFraction(new_num, self._denom)
def with_denom(self, new_denom):
return MyFraction(self._num, new_denom)
frac = MyFraction(1, 3)
print(frac)
with string_style(DECIMAL):
print(frac)
print(frac)
1/3
0.3333333333333333
1/3
Decorators¶
Decorators can be used to modify a function or class after it has been created. Note that some language features such as @classmethod
and @staticmethod
are also using decorator syntax.
import functools
def print_args_and_result(func):
# see functools section
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"{func.__name__} arguments: {args}")
print(f"{func.__name__} keyword arguments: {kwargs}")
print(f"{func.__name__} result: {result}")
return result
return wrapper
# see list comprehensions chapter
@print_args_and_result
def cross_powers(bases, exponents):
return [b ** e for b in bases for e in exponents]
@print_args_and_result
def cross_powers_with_nonnegative_exponent(bases, exponents):
nonnegative_exponents = [e for e in exponents if e >= 0]
return cross_powers(bases, nonnegative_exponents)
cross_powers_with_nonnegative_exponent([2, 3, 5], [-2, -1, 4, 7])
cross_powers arguments: ([2, 3, 5], [4, 7])
cross_powers keyword arguments: {}
cross_powers result: [16, 128, 81, 2187, 625, 78125]
cross_powers_with_nonnegative_exponent arguments: ([2, 3, 5], [-2, -1, 4, 7])
cross_powers_with_nonnegative_exponent keyword arguments: {}
cross_powers_with_nonnegative_exponent result: [16, 128, 81, 2187, 625, 78125]
[16, 128, 81, 2187, 625, 78125]
import functools
import io
import sys
class PrintIndenter:
def __init__(self, indent_size=4):
self._indent_size = indent_size
self._current_depth = 0
def _pad(self, string):
padding = " " * self._current_depth
return padding + string.replace("\n", "\n" + padding)
def print_(self, *items):
buf = io.StringIO()
# print to buffer
printed = print(*items, file=buf, end='')
# pad buffer
result = self._pad(buf.getvalue())
print(result)
def with_indent(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
self._current_depth += self._indent_size
result = func(*args, **kwargs)
self._current_depth -= self._indent_size
return result
return wrapper
indenter = PrintIndenter(2)
# this is what decorators look like without the fancy @ syntax
# fib = indenter.with_indent(fib)
@indenter.with_indent
def fib(n):
indenter.print_(n)
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(5)
5
4
3
2
1
0
1
2
1
0
3
2
1
0
1
5
Aufgabe LF6 (WIP)¶
Veränderbarer Bruch Schreiben Sie
MyFraction
so um, dass Brüche veränderbar sind. Passen Sie die Propertiesnum
unddenom
so an, dass diese auch schreibbar sind. Überschreiben Sie außerdem die In-Place-Operatoren, z.B.__iadd__
. Vergessen Sie nicht, die Brüche immer gekürzt zu halten.