From 2d69585c5a3a0f8684dfc1b6ecea47def61ed880 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Mon, 11 Mar 2019 21:08:05 +0300 Subject: [PATCH 01/14] add Point class in Shape module --- ch06_objects/Shape.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 ch06_objects/Shape.py diff --git a/ch06_objects/Shape.py b/ch06_objects/Shape.py new file mode 100644 index 0000000..d66a17d --- /dev/null +++ b/ch06_objects/Shape.py @@ -0,0 +1,19 @@ +import math + + +class Point: + def __init__(self, x=0, y=0): + self.x = x + self.y = y + + def distance_from_origin(self): + return math.hypot(self.x, self.y) + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + def __repr__(self): + return "Point({0.x!r}, {0.y!r})".format(self) + + def __str__(self): + return "({0.x!r}, {0.y!r})".format(self) From 7c3842ca07a2f1d9bf5fe04e45fdb22269764df0 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Thu, 14 Mar 2019 18:48:06 +0300 Subject: [PATCH 02/14] add Circle class to Shape module --- ch06_objects/Shape.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/ch06_objects/Shape.py b/ch06_objects/Shape.py index d66a17d..fee0114 100644 --- a/ch06_objects/Shape.py +++ b/ch06_objects/Shape.py @@ -17,3 +17,55 @@ def __repr__(self): def __str__(self): return "({0.x!r}, {0.y!r})".format(self) + + +class Circle(Point): + def __init__(self, radius, x=0, y=0): + super().__init__(x, y) + self.radius = radius + + @property + def radius(self): + """The circle's radius + + >>> circle = Circle(-2) + Traceback (most recent call last): + ... + AssertionError: radius must be nonzero and non-negative + >>> circle = Circle(4) + >>> circle.radius = -1 + Traceback (most recent call last): + ... + AssertionError: radius must be nonzero and non-negative + >>> circle.radius = 6 + """ + return self.__radius + + @radius.setter + def radius(self, radius): + assert radius > 0, "radius must be nonzero and non-negative" + self.__radius = radius + + def edge_distance_from_origin(self): + return abs(self.distance_from_origin() - self.radius) + + def area(self): + return math.pi * (self.radius ** 2) + + def circumference(self): + return 2 * math.pi * self.radius + + def __eq__(self, other): + return self.radius == other.radius and super().__eq__(other) + + def __repr__(self): + return "Circle({0.radius!r}, {0.x!r}, {0.y!r})".format(self) + + def __str__(self): + return repr(self) + + +if __name__ == '__main__': + import doctest + + doctest.testmod() From df8e09fc8bfe7c4b189f8402e731f156936e73a5 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Thu, 14 Mar 2019 19:06:52 +0300 Subject: [PATCH 03/14] add FuzzyBool class --- ch06_objects/FuzzyBool.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ch06_objects/FuzzyBool.py diff --git a/ch06_objects/FuzzyBool.py b/ch06_objects/FuzzyBool.py new file mode 100644 index 0000000..926c3d4 --- /dev/null +++ b/ch06_objects/FuzzyBool.py @@ -0,0 +1,34 @@ +class FuzzyBool: + def __init__(self, value=0.0): + self.__value = value if 0.0 <= value <= 1.0 else 0.0 + + def __eq__(self, other): + return self.__value == other.value + + def __str__(self): + """ + >>> f = FuzzyBool(0.5) + >>> str(f) + '0.5' + """ + return str(self.__value) + + def __repr__(self): + """ + >>> f = FuzzyBool(0.5) + >>> repr(f) + 'FuzzyBool(0.5)' + """ + return "FuzzyBool({0!r})".format(self.__value) + + def __hash__(self): + return hash(id(self)) + + def __format__(self, format_spec): + return super().__format__(format_spec) + + +if __name__ == '__main__': + import doctest + + doctest.testmod() From 22444814c00a9b2c9d07c6f8a46d89099745b7c4 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Thu, 21 Mar 2019 20:10:47 +0300 Subject: [PATCH 04/14] implement special functions * casting to bool, int, float * bitwise operations * fix repr function --- ch06_objects/FuzzyBool.py | 74 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/ch06_objects/FuzzyBool.py b/ch06_objects/FuzzyBool.py index 926c3d4..75c6c2f 100644 --- a/ch06_objects/FuzzyBool.py +++ b/ch06_objects/FuzzyBool.py @@ -19,7 +19,35 @@ def __repr__(self): >>> repr(f) 'FuzzyBool(0.5)' """ - return "FuzzyBool({0!r})".format(self.__value) + return ("{0}({1})".format(self.__class__.__name__, + self.__value)) + + def __bool__(self): + """ + >>> bool(FuzzyBool(.5)) + False + >>> bool(FuzzyBool(.75)) + True + """ + return self.__value > 0.5 + + def __int__(self): + """ + >>> int(FuzzyBool(.5)) + 0 + >>> int(FuzzyBool(.75)) + 1 + """ + return round(self.__value) + + def __float__(self): + """ + >>> float(FuzzyBool(.5)) + 0.5 + >>> float(FuzzyBool(.75)) + 0.75 + """ + return self.__value def __hash__(self): return hash(id(self)) @@ -27,6 +55,50 @@ def __hash__(self): def __format__(self, format_spec): return super().__format__(format_spec) + def __invert__(self): + """ + >>> f = ~FuzzyBool(0.3) + >>> repr(f) + 'FuzzyBool(0.7)' + """ + return FuzzyBool(1.0 - self.__value) + + def __and__(self, other): + """ + >>> f = FuzzyBool(0.3) & FuzzyBool(0.7) + >>> repr(f) + 'FuzzyBool(0.3)' + """ + return FuzzyBool(min(self.__value, other.__value)) + + def __iand__(self, other): + """ + >>> f = FuzzyBool(0.7) + >>> f &= FuzzyBool(0.3) + >>> repr(f) + 'FuzzyBool(0.3)' + """ + self.__value = min(self.__value, other.__value) + return self + + def __or__(self, other): + """ + >>> f = FuzzyBool(0.3) | FuzzyBool(0.7) + >>> repr(f) + 'FuzzyBool(0.7)' + """ + return FuzzyBool(max(self.__value, other.__value)) + + def __ior__(self, other): + """ + >>> f = FuzzyBool(0.3) + >>> f |= FuzzyBool(0.7) + >>> repr(f) + 'FuzzyBool(0.7)' + """ + self.__value = max(self.__value, other.__value) + return self + if __name__ == '__main__': import doctest From 7bb6cc8b1a69ac5b8a91987ecac3a82f79694762 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Thu, 4 Apr 2019 19:35:06 +0300 Subject: [PATCH 05/14] add format method --- ch06_objects/FuzzyBool.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ch06_objects/FuzzyBool.py b/ch06_objects/FuzzyBool.py index 75c6c2f..1965aad 100644 --- a/ch06_objects/FuzzyBool.py +++ b/ch06_objects/FuzzyBool.py @@ -22,6 +22,18 @@ def __repr__(self): return ("{0}({1})".format(self.__class__.__name__, self.__value)) + def __format__(self, format_spec): + """ + >>> f = FuzzyBool(0.509) + >>> "{0:f}".format(f) + '0.509' + >>> "{0:.2f}".format(f) + '0.51' + >>> "{0:.1f}".format(f) + '0.5' + """ + return self.__value.__format__(format_spec) + def __bool__(self): """ >>> bool(FuzzyBool(.5)) From 5863d81644a1249f7ca0fae3fb492e32d56b6bf2 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Thu, 4 Apr 2019 19:42:29 +0300 Subject: [PATCH 06/14] add conjunction and disjunction methods --- ch06_objects/FuzzyBool.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ch06_objects/FuzzyBool.py b/ch06_objects/FuzzyBool.py index 1965aad..4357b82 100644 --- a/ch06_objects/FuzzyBool.py +++ b/ch06_objects/FuzzyBool.py @@ -111,6 +111,24 @@ def __ior__(self, other): self.__value = max(self.__value, other.__value) return self + @staticmethod + def conjunction(*fuzzies): + """ + >>> f1, f2, f3 = FuzzyBool(0.75), FuzzyBool(0.5), FuzzyBool(0.9) + >>> str(FuzzyBool.conjunction(f1, f2, f3)) + '0.5' + """ + return FuzzyBool(min([float(x) for x in fuzzies])) + + @staticmethod + def disjunction(*fuzzies): + """ + >>> f1, f2, f3 = FuzzyBool(0.75), FuzzyBool(0.5), FuzzyBool(0.9) + >>> str(FuzzyBool.disjunction(f1, f2, f3)) + '0.9' + """ + return FuzzyBool(max([float(x) for x in fuzzies])) + if __name__ == '__main__': import doctest From f0c304f6503b3759ab8bc4511d6d6a7efcecbcf9 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Fri, 5 Apr 2019 18:51:46 +0300 Subject: [PATCH 07/14] add FuzzyBoolAlt.py * implement FuzzyBool by subclassing float * use meta programming tricks for unimplementing methods --- ch06_objects/FuzzyBoolAlt.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ch06_objects/FuzzyBoolAlt.py diff --git a/ch06_objects/FuzzyBoolAlt.py b/ch06_objects/FuzzyBoolAlt.py new file mode 100644 index 0000000..429114b --- /dev/null +++ b/ch06_objects/FuzzyBoolAlt.py @@ -0,0 +1,57 @@ +class FuzzyBool(float): + def __new__(cls, value=0.0): + return super().__new__(cls, + value if 0.0 <= value <= 1.0 else 0.0) + + def __invert__(self): + return FuzzyBool(1.0 - float(self)) + + def __and__(self, other): + return FuzzyBool(min(other)) + + def __or__(self, other): + return FuzzyBool(max(other)) + + def __repr__(self): + return '{0}({1})'.format(self.__class__.__name__, + super().__repr__()) + + def __bool__(self): + return self > 0.5 + + def __int__(self): + return round(self) + + for name, operator in (("__neg__", "-"), + ("__index__", "index()")): + message = ( + "bad operand type for unary {0}: '{{self}}'".format(operator)) + exec("def {0}(self): raise TypeError(\"{1}\".format(" + "self=self.__class__.__name__))".format(name, message)) + + for name, operator in (("__xor__", "^"), ("__ixor__", "^="), + ("__add__", "+"), ("__iadd__", "+="), + ("__radd__", "+"), + ("__sub__", "-"), ("__isub__", "-="), + ("__rsub__", "-"), + ("__mul__", "*"), ("__imul__", "*="), + ("__rmul__", "*"), + ("__pow__", "**"), ("__ipow__", "**="), + ("__rpow__", "**"), ("__floordiv__", "//"), + ("__ifloordiv__", "//="), ("__rfloordiv__", "//"), + ("__truediv__", "/"), ("__itruediv__", "/="), + ("__rtruediv__", "/"), ("__divmod__", "divmod()"), + ("__rdivmod__", "divmod()"), ("__mod__", "%"), + ("__imod__", "%="), ("__rmod__", "%"), + ("__lshift__", "<<"), ("__ilshift__", "<<="), + ("__rlshift__", "<<"), ("__rshift__", ">>"), + ("__irshift__", ">>="), ("__rrshift__", ">>")): + message = ("unsupported operand type(s) for {0}: " + "'{{self}}'{{join}} {{args}}".format(operator)) + exec("def {0}(self, *args):\n" + " types = [\"'\" + arg.__class__.__name__ + \"'\" " "" + "for arg in args]\n" + " raise TypeError(\"{1}\".format(" + "self=self.__class__.__name__, " + "join=(\" and\" if len(args) == 1 else \",\")," "" + "args=\", \".join(types)))".format(name, message)) From ec68c419e71f72201362cca009a2b830d1d14702 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Sat, 13 Apr 2019 23:48:59 +0300 Subject: [PATCH 08/14] add Image.py class Image provides simple abstraction for pixels and colors with possible save context --- ch06_objects/Image.py | 124 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 ch06_objects/Image.py diff --git a/ch06_objects/Image.py b/ch06_objects/Image.py new file mode 100644 index 0000000..89e828a --- /dev/null +++ b/ch06_objects/Image.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +import os +import pickle + + +class ImageError(Exception): + pass + + +class CoordinateError(ImageError): + pass + + +class NoFilenameError(ImageError): + pass + + +class SaveError(ImageError): + pass + + +class LoadError(ImageError): + pass + + +class ExportError(ImageError): + pass + + +class Image: + def __init__(self, width, height, filename="", + background="#FFFFFF"): + self.filename = filename + self.__background = background + self.__data = {} + self.__height = height + self.__width = width + self.__colors = {self.__background} + + @property + def background(self): + return self.__background + + @property + def width(self): + return self.__width + + @property + def height(self): + return self.__height + + @property + def colors(self): + return set(self.__colors) + + def __getitem__(self, coordinate): + assert len(coordinate) == 2, "coordinate should be 2-tuple" + if (not 0 <= coordinate[0] <= self.__width or + not 0 <= coordinate[1] <= self.__height): + raise CoordinateError(str(coordinate)) + return self.__data.get(tuple(coordinate), self.__background) + + def __setitem__(self, coordinate, color): + assert len(coordinate) == 2, "coordinate should be 2-tuple" + if (not 0 <= coordinate[0] <= self.__width or + not 0 <= coordinate[1] <= self.__height): + raise CoordinateError(str(coordinate)) + if color == self.__background: + self.__data.pop(tuple(coordinate), None) + else: + self.__data[tuple(coordinate)] = color + self.__colors.add(color) + + def __delitem__(self, coordinate): + assert len(coordinate) == 2, "coordinate should be 2-tuple" + if (not 0 <= coordinate[0] <= self.__width or + not 0 <= coordinate[1] <= self.__height): + raise CoordinateError(str(coordinate)) + self.__data.pop(tuple(coordinate), None) + + def save(self, filename=None): + if filename is not None: + self.filename = filename + if not self.filename: + raise NoFilenameError() + + fh = None + try: + data = [self.width, self.height, self.__background, self.__data] + fh = open(self.filename, "wb") + pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL) + except (EnvironmentError, pickle.PicklingError) as err: + raise SaveError(err) + finally: + if fh is not None: + fh.close() + + def load(self, filename=None): + if filename is not None: + self.filename = filename + if not self.filename: + raise NoFilenameError() + + fh = None + try: + fh = open(self.filename, "rb") + data = pickle.load(fh) + (self.__width, self.__height, self.__background, self.__data) = data + self.__colors = (set(self.__data.values()) | {self.__background}) + except (EnvironmentError, pickle.UnpicklingError) as err: + raise LoadError(err) + finally: + if fh is not None: + fh.close() + + def export(self, filename): + if filename.lower().endswith(".xpm"): + self.__export_xpm(filename) + else: + raise ExportError("unsupported export format:" + + os.path.splitext(filename)[1]) + + def __export_xpm(self, filename): + raise NotImplementedError() From bd1f4e76b2f9000c34eb652c8e17520060f1df34 Mon Sep 17 00:00:00 2001 From: Maksim Kirianov Date: Sun, 5 May 2019 20:12:50 +0300 Subject: [PATCH 09/14] add SortedList.py class SortedList provides runtime-sorting list by specified key function --- ch06_objects/SortedList.py | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 ch06_objects/SortedList.py diff --git a/ch06_objects/SortedList.py b/ch06_objects/SortedList.py new file mode 100644 index 0000000..c24e9dd --- /dev/null +++ b/ch06_objects/SortedList.py @@ -0,0 +1,99 @@ +_identity = lambda x: x + + +class SortedList: + def __init__(self, sequence=None, key=None): + self.__key = key or _identity + assert hasattr(self.__key, "__call__") + if sequence is None: + self.__list = [] + elif (isinstance(sequence, SortedList) + and sequence.key == self.__key): + self.__list = sequence.__list[:] + else: + self.__list = sorted(list(sequence), key=self.__key) + + @property + def key(self): + return self.__key + + def add(self, value): + index = self.__bisect_left(value) + if index == len(self.__list): + self.__list.append(value) + else: + self.__list.insert(index, value) + + def __bisect_left(self, value): + key = self.__key(value) + left, right = 0, len(self.__list) + while left < right: + middle = (left + right) // 2 + if self.__key(self.__list[middle]) < key: + left = middle + 1 + else: + right = middle + return left + + def remove(self, value): + index = self.__bisect_left(value) + if index < len(self.__list) and self.__list[index] == value: + del self.__list[index] + else: + raise ValueError("{0}.remove(x): x not in list".format( + self.__class__.__name__ + )) + + def remove_every(self, value): + count = 0 + index = self.__bisect_left(value) + while (index < len(self.__list) and + self.__list[index] == value): + del self.__list[index] + count += 1 + return count + + def count(self, value): + count = 0 + index = self.__bisect_left(value) + while (index < len(self.__list) and + self.__list[index] == value): + index += 1 + count += 1 + return count + + def __del__(self, index): + del self.__list[index] + + def __getitem__(self, index): + return self.__list[index] + + def __setitem__(self, index, value): + raise TypeError("use add() to insert a value and reply to" + "the list and put it in the right place") + + def __iter__(self): + return iter(self.__list) + + def __reversed__(self): + return reversed(self.__list) + + def __contains__(self, value): + index = self.__bisect_left(value) + return (index < len(self.__list) and + self.__list[index] == value) + + def clear(self): + self.__list = [] + + def pop(self, index=-1): + return self.__list.pop(index) + + def __len__(self): + return len(self.__list) + + def __str__(self): + return str(self.__list) + + def copy(self): + return SortedList(self, self.__key) From 6b0609e3e9d9af8e60a16396a793d6cc7ac44cd6 Mon Sep 17 00:00:00 2001 From: Maksim Kirianov Date: Sun, 5 May 2019 22:03:23 +0300 Subject: [PATCH 10/14] add SortedDict.py class SortedDict provides runtime-sorting dict (keys) by specified key function --- ch06_objects/SortedDict.py | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 ch06_objects/SortedDict.py diff --git a/ch06_objects/SortedDict.py b/ch06_objects/SortedDict.py new file mode 100644 index 0000000..fd18403 --- /dev/null +++ b/ch06_objects/SortedDict.py @@ -0,0 +1,98 @@ +from typing import Optional, _KT, _VT, overload, Union, _T, Tuple, ValuesView + +from ch06_objects import SortedList + + +class SortedDict(dict): + + def __init__(self, dictionary=None, key=None, **kwargs): + dictionary = dictionary or {} + super().__init__(dictionary) + if kwargs: + super().update(kwargs) + self.__keys = SortedList.SortedList(super().keys(), key) + + def update(self, dictionary=None, **kwargs): + if dictionary is None: + pass + elif isinstance(dictionary, dict): + super().update(dictionary) + else: + for key, value in dictionary.items(): + super().__setitem__(key, value) + if kwargs: + super().update(kwargs) + self.__keys = SortedList.SortedList(super().keys(), + self.__keys.key) + + @classmethod + def fromkeys(cls, iterable, value=None, key=None): + return cls({k: value for k in iterable}, key) + + def __setitem__(self, key, value): + if key not in self: + self.__keys.add(key) + return super().__setitem__(key, value) + + def __delitem__(self, key): + try: + self.__keys.remove(key) + except ValueError: + raise KeyError(key) + super().__delitem__(key) + + def setdefault(self, key, value=None): + if key not in self: + self.__keys.add(key) + return super().setdefault(key, value) + + def pop(self, key, *args): + if key not in self: + if len(args) == 0: + raise KeyError(key) + return args[0] + self.__keys.remove(key) + return super().pop(key, args) + + def popitem(self): + item = super().popitem() + self.__keys.remove(item[0]) + return item + + def clear(self): + super().clear() + self.__keys.clear() + + def values(self): + for key in self.__keys: + yield self[key] + + def items(self): + for key in self.__keys: + yield (key, self[key]) + + def __iter__(self): + return iter(self.__keys) + + keys = __iter__ + + def __repr__(self): + return super().__repr__() + + def __str__(self): + return ("{" + ", ".join(["{0!r}: {1!r}".format(k, v) + for k, v in self.items()]) + "}") + + def copy(self): + d = SortedDict() + super(SortedDict, d).update(self) + d.__keys = self.__keys.copy() + return d + + __copy__ = copy + + def value_at(self, index): + return self[self.__keys[index]] + + def set_value_at(self, index, value): + self[self.__keys[index]] = value From c50180d88b22006f43820707df45ab3a99eb5090 Mon Sep 17 00:00:00 2001 From: Maksim Kirianov Date: Sun, 5 May 2019 22:49:42 +0300 Subject: [PATCH 11/14] implement various numeric operations for class Point --- ch06_objects/Shape.py | 160 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/ch06_objects/Shape.py b/ch06_objects/Shape.py index fee0114..6146433 100644 --- a/ch06_objects/Shape.py +++ b/ch06_objects/Shape.py @@ -18,6 +18,166 @@ def __repr__(self): def __str__(self): return "({0.x!r}, {0.y!r})".format(self) + def __add__(self, other): + """ + >>> q = Point(1, 5) + >>> r = Point(2, 10) + >>> p = q + r + >>> p + Point(3, 15) + >>> q = Point(1, 5) + >>> r = Point(2, -10) + >>> p = q + r + >>> p + Point(3, -5) + """ + return Point(self.x + other.x, self.y + other.y) + + def __iadd__(self, other): + """ + >>> p = Point(1, 5) + >>> q = Point(2, 10) + >>> p += q + >>> p + Point(3, 15) + >>> p = Point(1, 5) + >>> q = Point(2, -10) + >>> p += q + >>> p + Point(3, -5) + """ + self.x = self.x + other.x + self.y = self.y + other.y + return self + + def __sub__(self, other): + """ + >>> q = Point(1, 5) + >>> r = Point(2, 10) + >>> p = q - r + >>> p + Point(-1, -5) + >>> q = Point(1, 5) + >>> r = Point(2, -10) + >>> p = q - r + >>> p + Point(-1, 15) + """ + return Point(self.x - other.x, self.y - other.y) + + def __isub__(self, other): + """ + >>> p = Point(1, 5) + >>> q = Point(2, 10) + >>> p -= q + >>> p + Point(-1, -5) + >>> p = Point(1, 5) + >>> q = Point(2, -10) + >>> p -= q + >>> p + Point(-1, 15) + """ + self.x = self.x - other.x + self.y = self.y - other.y + return self + + def __mul__(self, number): + """ + >>> q = Point(1, 5) + >>> n = 2 + >>> p = q * n + >>> p + Point(2, 10) + >>> q = Point(-1, 5) + >>> n = -2 + >>> p = q * n + >>> p + Point(2, -10) + """ + return Point(self.x * number, self.y * number) + + def __imul__(self, number): + """ + >>> p = Point(1, 5) + >>> n = 2 + >>> p *= n + >>> p + Point(2, 10) + >>> p = Point(-1, 5) + >>> n = -2 + >>> p *= n + >>> p + Point(2, -10) + """ + self.x = self.x * number + self.y = self.y * number + return self + + def __truediv__(self, number): + """ + >>> q = Point(1, 5) + >>> n = 2 + >>> p = q / n + >>> p + Point(0.5, 2.5) + >>> q = Point(-2, -10) + >>> n = 2 + >>> p = q / n + >>> p + Point(-1.0, -5.0) + """ + return Point(self.x / number, self.y / number) + + def __itruediv__(self, number): + """ + >>> p = Point(1, 5) + >>> n = 2 + >>> p /= n + >>> p + Point(0.5, 2.5) + >>> p = Point(-2, -10) + >>> n = 2 + >>> p /= n + >>> p + Point(-1.0, -5.0) + """ + self.x = self.x / number + self.y = self.y / number + return self + + def __floordiv__(self, number): + """ + >>> q = Point(1, 5) + >>> n = 2 + >>> p = q // n + >>> p + Point(0, 2) + >>> q = Point(-2, -10) + >>> n = 2 + >>> p = q // n + >>> p + Point(-1, -5) + """ + return Point(self.x // number, self.y // number) + + def __ifloordiv__(self, number): + """ + >>> p = Point(1, 5) + >>> n = 2 + >>> p //= n + >>> p + Point(0, 2) + >>> p = Point(-2, -10) + >>> n = 2 + >>> p //= n + >>> p + Point(-1, -5) + """ + self.x = self.x // number + self.y = self.y // number + return self + class Circle(Point): def __init__(self, radius, x=0, y=0): From a2fb1a8fe1761ab9b9b03895f219deac85289aa1 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Sun, 19 May 2019 23:03:09 +0300 Subject: [PATCH 12/14] implement Transaction class --- ch06_objects/Account.py | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 ch06_objects/Account.py diff --git a/ch06_objects/Account.py b/ch06_objects/Account.py new file mode 100644 index 0000000..a328203 --- /dev/null +++ b/ch06_objects/Account.py @@ -0,0 +1,89 @@ +from datetime import datetime + + +class Transaction: + """Transaction store info about currency transaction + and calculate amount in USD (based on usd_conversion_rate) + + usd_conversion_rate for USD must always be equal to 1.0 + """ + + def __init__(self, amount, date, currency='USD', usd_conversion_rate=1.0, + description=None): + """ + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> str(Transaction(1, timestamp, currency='USD')) + '1 USD at 2019-05-19 22:20:00' + >>> Transaction(1, timestamp, currency='USD', usd_conversion_rate=10.0) + Traceback (most recent call last): + ... + ValueError: USD conversion rate should be equal to 1.0 + >>> str(Transaction(1, timestamp, currency='EUR', usd_conversion_rate=1.0)) + '1 EUR at 2019-05-19 22:20:00' + >>> Transaction(None, timestamp) + Traceback (most recent call last): + ... + ValueError: amount could not be None + >>> Transaction(1, None) + Traceback (most recent call last): + ... + ValueError: date could not be None + """ + if amount is None: + raise ValueError('amount could not be None') + if date is None: + raise ValueError('date could not be None') + if currency == 'USD' and not usd_conversion_rate == 1.0: + raise ValueError('USD conversion rate should be equal to 1.0') + self._amount = amount + self._date = date + self._currency = currency + self._usd_conversion_rate = usd_conversion_rate + self._description = description + + @property + def amount(self): + return self._amount + + @property + def date(self): + return self._date + + @property + def currency(self): + return self._currency + + @property + def usd_conversion_rate(self): + return self._usd_conversion_rate + + @property + def description(self): + return self._description + + @property + def usd(self): + """Return calculated amount in USD + + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> t = Transaction(1, timestamp) + >>> t.usd + 1.0 + >>> t = Transaction(100, timestamp, currency='RUB', usd_conversion_rate=62.5) + >>> t.usd + 6250.0 + """ + return self._amount * self._usd_conversion_rate + + def __str__(self): + return '{0} {1} at {2}'.format( + self._amount, self._currency, self._date + ) + + def __repr__(self): + return ("{0}(amount={1}, date={2}, currency={3}, " + "usd_conversion_rate={4}, description={5})").format( + self.__class__.__name__, + self._amount, repr(self._date), self._currency, + self._usd_conversion_rate, repr(self._description) + ) From c0644b293046ad2862cf3794ddd4fc8f428fb091 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Wed, 22 May 2019 23:15:32 +0300 Subject: [PATCH 13/14] fix Transaction#usd calculation --- ch06_objects/Account.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ch06_objects/Account.py b/ch06_objects/Account.py index a328203..d31b00b 100644 --- a/ch06_objects/Account.py +++ b/ch06_objects/Account.py @@ -69,11 +69,11 @@ def usd(self): >>> t = Transaction(1, timestamp) >>> t.usd 1.0 - >>> t = Transaction(100, timestamp, currency='RUB', usd_conversion_rate=62.5) + >>> t = Transaction(6250, timestamp, currency='RUB', usd_conversion_rate=62.5) >>> t.usd - 6250.0 + 100.0 """ - return self._amount * self._usd_conversion_rate + return self._amount / self._usd_conversion_rate def __str__(self): return '{0} {1} at {2}'.format( From 0415b986c848b3d243bda79b89bc557ff433a206 Mon Sep 17 00:00:00 2001 From: Maksim Kiryanov Date: Tue, 28 May 2019 23:10:46 +0300 Subject: [PATCH 14/14] implement Account class --- ch06_objects/Account.py | 215 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/ch06_objects/Account.py b/ch06_objects/Account.py index d31b00b..0380fca 100644 --- a/ch06_objects/Account.py +++ b/ch06_objects/Account.py @@ -1,6 +1,215 @@ +import os +import pickle +import tempfile from datetime import datetime +class AccountError(Exception): + pass + + +class NoFilenameError(AccountError): + pass + + +class SaveError(AccountError): + pass + + +class LoadError(AccountError): + pass + + +class Account: + """Account store info about account number, name and list of Transactions + """ + + def __init__(self, number, name): + if number is None: + raise ValueError('number should be provided') + self._verify_name(name) + self._number = number + self._name = name + self._transactions = [] + + def _verify_name(self, name): + if name is None: + raise ValueError('name should be provided') + if len(name) < 4: + raise ValueError('name should be at least 4 characters') + + @property + def number(self): + """ + >>> acc = Account(1, 'account') + >>> acc.number + 1 + >>> acc.number = 100 + Traceback (most recent call last): + ... + AttributeError: can't set attribute + """ + return self._number + + @property + def name(self): + """ + >>> acc = Account(1, 'new account') + >>> acc.name + 'new account' + >>> acc.name = 'updated account' + >>> acc.name + 'updated account' + >>> acc.name = 'acc' + Traceback (most recent call last): + ... + ValueError: name should be at least 4 characters + """ + return self._name + + @name.setter + def name(self, name): + self._verify_name(name) + self._name = name + + def apply(self, transaction): + if transaction is None: + raise ValueError('transaction should be provided') + self._transactions.append(transaction) + + def __len__(self): + return len(self._transactions) + + @property + def balance(self): + """Return balance in USD for all account transactions + + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> acc = Account(42, 'default') + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.balance + 100.0 + + >>> acc.apply(Transaction(6250, timestamp, currency='RUB', usd_conversion_rate=62.5)) + >>> acc.balance + 200.0 + """ + balance = 0 + for t in self._transactions: + balance += t.usd + return balance + + @property + def all_usd(self): + """Return balance in USD for all account transactions + + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> acc = Account(42, 'default') + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.all_usd + True + + >>> acc.apply(Transaction(6250, timestamp, currency='RUB', usd_conversion_rate=62.5)) + >>> acc.all_usd + False + """ + for t in self._transactions: + if t.currency != 'USD': + return False + return True + + def save(self, filename=None): + """Save Account state in pickle format + + >>> account_name = 'default' + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> acc = Account(42, account_name) + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.apply(Transaction(6250, timestamp, currency='RUB', usd_conversion_rate=62.5)) + + >>> tmp_dir = tempfile.gettempdir() + >>> dump_filename = os.path.join(tmp_dir, account_name) + >>> dump_full_filename = dump_filename + '.acc' + + >>> acc.save(dump_full_filename) + >>> os.path.exists(dump_full_filename) + True + + >>> os.remove(dump_full_filename) + + >>> acc.save(dump_filename ) + >>> os.path.exists(dump_full_filename) + True + + >>> os.remove(dump_full_filename) + + >>> acc.save() + >>> os.path.exists(dump_filename + '.acc') + True + """ + if filename is not None: + self.filename = filename + if self.filename is None: + raise NoFilenameError(filename) + + if not self.filename.endswith('.acc'): + self.filename += '.acc' + + fd = None + try: + fd = open(self.filename, 'wb') + data = (self._name, self._number, self._transactions) + pickle.dump(data, fd, pickle.HIGHEST_PROTOCOL) + except (EnvironmentError, pickle.PicklingError) as err: + raise SaveError(err) + finally: + if fd is not None: + fd.close() + + def load(self, filename=None): + """Load Account state from pickle format + + >>> account_name = 'default' + >>> timestamp = datetime(2019, 5, 19, 22, 20, 0) + >>> acc = Account(42, account_name) + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.apply(Transaction(100, timestamp)) + >>> acc.apply(Transaction(6250, timestamp, currency='RUB', usd_conversion_rate=62.5)) + >>> tmp_dir = tempfile.gettempdir() + >>> dump_full_filename = os.path.join(tmp_dir, account_name) + '.acc' + >>> acc.save(dump_full_filename) + + >>> acc = Account(112, 'new name') + >>> acc.load(dump_full_filename) + >>> acc.name + 'default' + >>> acc.number + 42 + """ + if filename is not None: + self.filename = filename + if self.filename is None: + raise NoFilenameError(filename) + + if not self.filename.endswith('.acc'): + self.filename += '.acc' + + fd = None + try: + fd = open(self.filename, 'rb') + (name, number, transactions) = pickle.load(fd) + self.name = name + self._number = number + self._transactions = transactions + except (EnvironmentError, pickle.PicklingError) as err: + raise LoadError(err) + finally: + if fd is not None: + fd.close() + + class Transaction: """Transaction store info about currency transaction and calculate amount in USD (based on usd_conversion_rate) @@ -87,3 +296,9 @@ def __repr__(self): self._amount, repr(self._date), self._currency, self._usd_conversion_rate, repr(self._description) ) + + +if __name__ == '__main__': + import doctest + + doctest.testmod()