Assignment & Mutable vs Immutable¶
In [1]:
def f(x:list):
x += ["f"]
def g(x:list):
x = x + ["g"]
In [2]:
a = [1,2,3]
f(a)
a
Out[2]:
[1, 2, 3, 'f']
In [3]:
a = [1,2,3]
g(a)
a
Out[3]:
[1, 2, 3]
In [4]:
def h(x, y = []):
y.append(x)
print(y)
In [5]:
h(4,a)
[1, 2, 3, 4]
In [6]:
a
Out[6]:
[1, 2, 3, 4]
In [7]:
h(5)
[5]
In [8]:
a
Out[8]:
[1, 2, 3, 4]
In [9]:
h(6)
[5, 6]
In [10]:
def h(x, y = None):
if y is None:
y = []
y.append(x)
print(y)
In [11]:
h(5)
[5]
In [12]:
h(6)
[6]
OOP¶
In [13]:
class ClassName(object):
def __new__(cls, *args, **kwargs):
print("Creating instance with __new__.")
self = super().__new__(cls)
return self
def __init__(self, arg):
print(f"Initializing instance with __init__ and agument {arg}.")
self.arg = arg
In [14]:
a = ClassName("my_argument")
Creating instance with __new__. Initializing instance with __init__ and agument my_argument.
In [15]:
type(a)
Out[15]:
__main__.ClassName
In [16]:
a.arg
Out[16]:
'my_argument'
In [17]:
class Person:
def __init__(self, name, sex, age, salary = 0, algebra = 0):
self.name = name
self.sex = sex
self.age = age
self._salary = salary
self.__algebra = algebra
def __repr__(self) -> str:
return f"repr {self.name}"
def __str__(self):
return "str " + str(self.name)
def __add__(self, other:"Person"): # self + other
if not isinstance(other, Person):
raise ValueError("other needs to be Person")
name = self.name + " " + other.name
sex = "M" if "M" in (self.sex, other.sex) else "F"
age = max(self.age, other.age)
salary = self._salary + other._salary
algebra = min(self.__algebra, other._Person__algebra)
return Person(name, sex, age, salary, algebra)
def introduce_myself(self, to = "all"):
print(f"Hello {to}! I am {self.name} and I am {self.age} years old.")
In [18]:
person1 = Person("name1", "M",1)
person2 = Person("name2", "F",2)
print(person1)
person2
str name1
Out[18]:
repr name2
In [19]:
person1.name, person1.sex
Out[19]:
('name1', 'M')
In [20]:
person2.name, person2.sex
Out[20]:
('name2', 'F')
In [21]:
person1.introduce_myself(to="all")
Hello all! I am name1 and I am 1 years old.
In [22]:
person1.introduce_myself(to="class")
Hello class! I am name1 and I am 1 years old.
In [23]:
class Woman(Person):
def __init__(self, name, age) -> None:
Person.__init__(self, name, "F", age)
def __repr__(self) -> str:
return "Woman repr"
def introduce_myself(self, to = "all"):
print(f"Hello {to}! my name is {self.name}. I am a girl of {self.age} years old.")
class Man(Person):
def __init__(self, name, age) -> None:
super().__init__(name, "M", age)
def __repr__(self) -> str:
return "Man repr"
In [24]:
man = Man("man_name",21)
woman = Woman("woman_name",20)
man.sex, woman.sex
Out[24]:
('M', 'F')
In [25]:
isinstance(man, Person)
Out[25]:
True
In [26]:
woman.age
Out[26]:
20
In [27]:
man
Out[27]:
Man repr
In [28]:
man._salary
Out[28]:
0
In [29]:
woman._Person__algebra
Out[29]:
0
In [30]:
man.introduce_myself()
Hello all! I am man_name and I am 21 years old.
In [31]:
woman.introduce_myself()
Hello all! my name is woman_name. I am a girl of 20 years old.
In [32]:
baby = man + woman
In [33]:
baby.sex
Out[33]:
'M'
In [34]:
man + 1
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/1757345412.py in <cell line: 1>() ----> 1 man + 1 /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/1173461491.py in __add__(self, other) 15 def __add__(self, other:"Person"): # self + other 16 if not isinstance(other, Person): ---> 17 raise ValueError("other needs to be Person") 18 name = self.name + " " + other.name 19 sex = "M" if "M" in (self.sex, other.sex) else "F" ValueError: other needs to be Person
In [35]:
a = [1,2,3]
s = 4
In [36]:
a + s
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/161887175.py in <cell line: 1>() ----> 1 a + s TypeError: can only concatenate list (not "int") to list
In [37]:
class OurList(list):
def __add__(self, other):
if isinstance(other,list):
return super().__add__(other)
else:
tmp = self.copy()
tmp.append(other)
return tmp
In [38]:
a = OurList([1,2,3])
s = 4
In [39]:
a + s
Out[39]:
[1, 2, 3, 4]
In [40]:
isinstance(a, list)
Out[40]:
True
In [41]:
a = 2
In [42]:
isinstance(a, float)
Out[42]:
False
Fraction¶
In [43]:
class Frac:
def __init__(self, num, denom=1) -> None:
if denom == 0:
raise ZeroDivisionError
self.num = num
self.denom = denom
def __repr__(self) -> str:
if self.denom == 1:
return str(self.num)
return f"Frac({self.num}, {self.denom})"
def latex(self) -> str:
try:
latex_n = self.num.latex()
except AttributeError:
latex_n = str(self.num)
if self.denom == 1:
return latex_n
try:
latex_d = self.denom.latex()
except AttributeError:
latex_d = str(self.denom)
return "\\frac{" + latex_n + "}" + "{" + latex_d + "}"
def __eq__(self, other: object) -> bool: # self == other
if isinstance(other, Frac):
return self.num * other.denom == self.denom * other.num
return self == Frac(other)
def __add__(self, other: object) -> bool: # self + other
if isinstance(other, Frac):
return Frac(
self.num * other.denom + self.denom * other.num,
self.denom * other.denom,
)
return self + Frac(other)
def __radd__(self, other): # other + self
return self + other
def __mul__(self, other: object) -> bool: # self * other
if isinstance(other, Frac):
return Frac(self.num * other.num, self.denom * other.denom)
return self * Frac(other)
def __rmul__(self, other): # other * self
return self * other
def __truediv__(self, other): # self / other
if other == 0:
raise ZeroDivisionError
if isinstance(other, Frac):
return Frac(self.num * other.denom, self.denom * other.num)
return self / Frac(other)
def __rtruediv__(self, other): # other / self
return Frac(other) / self
def simplify(self): # self.simplify()
try:
d = self.num.gcd(self.denom)
except AttributeError:
if isinstance(self.num, int) and isinstance(self.denom, int):
n = self.num
d = self.denom
while r := n % d:
n, d = d, r
else:
d = 1
self.num //= d
self.denom //= d
if self.denom == -1:
self.denom = 1
self.num *= -1
return self
In [44]:
a = Frac(4,64)
In [45]:
8*a
Out[45]:
Frac(32, 64)
In [46]:
a/.5
Out[46]:
Frac(4, 32.0)
In [47]:
1/a
Out[47]:
Frac(64, 4)
In [48]:
a.simplify()
Out[48]:
Frac(1, 16)
In [49]:
print(a.latex())
\frac{1}{16}
Polynomial¶
In [50]:
import re
def process_string_polynomial(polynomial, indeterminate="X"):
# sign ai x^power
sign = r"([+-])"
ai = r"((?:\d*\.)?\d+)"
power = r"(?:\^(\d+))"
ai_x_power = rf"{ai}?\s*(?:{indeterminate}{power}?)"
monomial_without_sign = rf"(?:{ai_x_power}|{ai})"
assert re.match(
rf"^\s*{sign}?\s*{monomial_without_sign}(\s*{sign}\s*{monomial_without_sign})*\s*$",
polynomial,
), "Invalid input."
coeffs = {}
pattern_monomial = rf"\s*{sign}?\s*{monomial_without_sign}\s*"
monomials = re.findall(pattern_monomial, polynomial)
for monomial in monomials:
sign, c_x, pw, c = monomial # sign * c_x * indeterminate^pw or sign * c
s = -1 if sign == "-" else 1
if c != "":
i = 0
a = float(c)
else:
i = 1 if pw == "" else int(pw)
a = 1 if c_x == "" else float(c_x)
coeffs[i] = coeffs.get(i, 0) + s * a
return coeffs
In [51]:
process_string_polynomial("X^100-1")
Out[51]:
{100: 1, 0: -1.0}
In [52]:
process_string_polynomial("X^2-4X")
Out[52]:
{2: 1, 1: -4.0}
In [53]:
class Polynomial:
def __init__(self, coeffs=None, indeterminate="X") -> None:
self.coeffs = {}
if coeffs is not None:
if isinstance(coeffs, (int, float)) and coeffs != 0:
self.coeffs[0] = coeffs
elif isinstance(coeffs, dict):
for i, ai in coeffs.items():
if not isinstance(i, int) or i < 0:
raise ValueError
if ai != 0:
self.coeffs[i] = ai
elif isinstance(coeffs, str):
self.coeffs = process_string_polynomial(coeffs, indeterminate)
else:
for i, ai in enumerate(coeffs):
if ai != 0:
self.coeffs[i] = ai
self._var = indeterminate
def __getitem__(self, key): # self[key]
return self.coeffs.get(key, 0)
def __iter__(self): # for ai in self or next(self)
i = 0
while True:
yield self[i]
i += 1
if self.degree < i:
break
@property
def degree(self): # self.degree
if self == 0:
return -1
return max(self.coeffs.keys())
@property
def variable(self):
return self._var
@variable.setter
def variable(self, new_variable):
assert isinstance(new_variable, str) and " " not in new_variable
self._var = new_variable
def __eq__(self, other: object) -> bool: # self == other
if other == 0:
return self.coeffs == {}
elif isinstance(other, Polynomial):
return self.coeffs == other.coeffs
elif isinstance(other, (int, float, complex)):
return self.degree == 0 and self[0] == other
else:
raise NotImplementedError
def _representation(self, use_latex=True) -> str:
with_latex = lambda char: char if use_latex else ""
if self.degree < 1:
return str(self[0])
string_version = ""
for i in sorted(self.coeffs):
ai = self[i]
string_version += "+" if ai > 0 else "-"
if int(ai) == ai:
ai = int(ai)
string_version += str(abs(ai)) if abs(ai) != 1 or i == 0 else ""
string_version += self._var if i > 0 else ""
string_version += (
"^" + with_latex("{") + str(i) + with_latex("}") if i > 1 else ""
)
if string_version[0] == "+":
string_version = string_version[1:]
return string_version
latex = lambda self: self._representation(True)
__str__ = lambda self: self._representation(False)
__repr__ = lambda self: f"Polynomial({self})"
def __add__(self, other): # self + other
if other == 0:
return self
elif isinstance(other, Polynomial):
coeffs = {}
for i in range(max(self.degree, other.degree) + 1):
ai = self[i]
bi = other[i]
coeffs[i] = ai + bi
return Polynomial(coeffs)
elif isinstance(other, (int, float, complex)):
return self + Polynomial(other)
else:
raise NotImplementedError
def __radd__(self, other): # other + self
return self + other
def __mul__(self, other): # self * other
if other == 0:
return Polynomial() # Zero polynomial
elif isinstance(other, Polynomial):
coeffs = {}
for i in range(self.degree + other.degree + 1):
ci = 0
for k in range(i + 1):
ci += self[k] * other[i - k]
coeffs[i] = ci
return Polynomial(coeffs)
elif isinstance(other, (int, float, complex)):
return self * Polynomial(other)
else:
raise NotImplementedError
def __rmul__(self, other): # other * self
return self * other
def __sub__(self, other): # self - other
return self + (-1 * other)
def __rsub__(self, other): # other - self
return -1 * self + other
def __divmod__(self, other): # divmod(self, other) = self//other, self%other
if other == 0:
raise ZeroDivisionError
elif isinstance(other, (int, float, complex)):
coeffs = {i: ai / other for i, ai in self.coeffs.items()}
return Polynomial(coeffs), Polynomial()
elif isinstance(other, Polynomial):
if other.degree == 0:
return divmod(self, other[0])
R = self.coeffs.copy()
Q = {}
leading_other = other[other.degree]
while other.degree <= (d := max(R.keys())):
k = d - other.degree
Q[k] = R[d] / leading_other
# new_R = R - other*Q[k]*X^k
new_R = {}
for i in range(d):
Ri = R.get(i, 0) - other[i - k] * Q[k]
if Ri != 0:
new_R[i] = Ri
R = new_R
if R == {}:
break
return Polynomial(Q), Polynomial(R)
else:
raise NotImplementedError
def __floordiv__(self, other): # self // other
return divmod(self, other)[0]
def __rfloordiv__(self, other): # other // self
if isinstance(other, (int, float, complex)):
if self.degree == 0:
return Polynomial(other / self[0])
else:
return Polynomial()
else:
raise NotImplementedError
def __mod__(self, other): # self % other
return divmod(self, other)[1]
def __call__(self, x): # self(x)
d = self.degree
result = 0
for i in range(d + 1):
result *= x
result += self[d - i]
return result
def gcd(self, other): # self.gcd(other)
d = self
r = other
while r != 0:
d, r = r, d % r
if d.degree == 0:
d = Polynomial(1)
return d
def __truediv__(self, other):
Q, R = divmod(self, other)
if R == 0:
return Q
else:
return Frac(self, other).simplify()
In [54]:
P = Polynomial("1-X^5") # __init__
P # __repr__
Out[54]:
Polynomial(1-X^5)
In [55]:
Q = Polynomial({0: 1, 1: -1}) # __init__
print(Q) # __str__
1-X
In [56]:
R = Polynomial([1, 1, 1, 1, 1])
R
Out[56]:
Polynomial(1+X+X^2+X^3+X^4)
In [57]:
P + Q # __add__(P,Q)
Out[57]:
Polynomial(2-X-X^5)
In [58]:
1 + P # __radd__(P,1)
Out[58]:
Polynomial(2-X^5)
In [59]:
P * Q # __mul__(P,Q)
Out[59]:
Polynomial(1-X-X^5+X^6)
In [60]:
0 * P # __rmul__(P,0)
Out[60]:
Polynomial(0)
In [61]:
divmod(P, Q) # __divmod__(P,Q)
Out[61]:
(Polynomial(1+X+X^2+X^3+X^4), Polynomial(0))
In [62]:
P // Q # __floordiv__(P,Q)
Out[62]:
Polynomial(1+X+X^2+X^3+X^4)
In [63]:
P // Q == R
Out[63]:
True
In [64]:
P - Q # __sub__(P,Q)
Out[64]:
Polynomial(X-X^5)
In [65]:
2 - P # __sub__(P,2)
Out[65]:
Polynomial(1+X^5)
In [66]:
P(2) # __call__(P, 2)
Out[66]:
-31.0
In [67]:
R(P)
Out[67]:
Polynomial(5-10X^5+10X^10-5X^15+X^20)
In [68]:
P.gcd(Q)
Out[68]:
Polynomial(1-X)
In [69]:
P.gcd(R)
Out[69]:
Polynomial(1+X+X^2+X^3+X^4)
In [70]:
Q*R
Out[70]:
Polynomial(1-X^5)
In [71]:
R.variable
Out[71]:
'X'
In [72]:
R.variable = "t"
In [73]:
R
Out[73]:
Polynomial(1+t+t^2+t^3+t^4)
In [74]:
R.latex()
Out[74]:
'1+t+t^{2}+t^{3}+t^{4}'
Rational function¶
In [75]:
P_over_Q = Frac(P,Q)
P_over_Q
Out[75]:
Frac(1-X^5, 1-X)
In [76]:
print(P_over_Q.latex())
\frac{1-X^{5}}{1-X}
In [77]:
P_over_Q + Q
Out[77]:
Frac(2-2X+X^2-X^5, 1-X)
In [78]:
P_over_Q * Q
Out[78]:
Frac(1-X-X^5+X^6, 1-X)
In [79]:
P_over_Q * Q == P
Out[79]:
True
In [80]:
P_over_Q.simplify()
Out[80]:
1+X+X^2+X^3+X^4
In [81]:
P_over_Q == R
Out[81]:
True
In [82]:
P_over_Q(8)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/524967069.py in <cell line: 1>() ----> 1 P_over_Q(8) TypeError: 'Frac' object is not callable
In [83]:
class CallableFrac(Frac):
def __init__(self, num, denom=1) -> None:
super().__init__(num, denom)
def __call__(self, *args, **kwargs):
return self.num(*args, **kwargs)/self.denom(*args, **kwargs)
In [84]:
P_over_Q = CallableFrac(P,Q)
P_over_Q
Out[84]:
Frac(1-X^5, 1-X)
In [85]:
P_over_Q(0)
Out[85]:
1.0
In [86]:
Q(1)
Out[86]:
0
In [87]:
P_over_Q(1)
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/1789074747.py in <cell line: 1>() ----> 1 P_over_Q(1) /var/folders/x4/45r2t3bx5l3dc_b3tm_yz7380000gn/T/ipykernel_9440/3893800112.py in __call__(self, *args, **kwargs) 4 5 def __call__(self, *args, **kwargs): ----> 6 return self.num(*args, **kwargs)/self.denom(*args, **kwargs) ZeroDivisionError: float division by zero
In [88]:
P_over_Q.simplify()
Out[88]:
1+X+X^2+X^3+X^4
In [89]:
P_over_Q(1)
Out[89]:
5.0
In [90]:
P_over_Q(R)
Out[90]:
Polynomial(5+10X+20X^2+35X^3+56X^4+74X^5+90X^6+100X^7+101X^8+90X^9+74X^10+55X^11+36X^12+20X^13+10X^14+4X^15+X^16)
In [91]:
P(R).gcd(Q(R))
Out[91]:
Polynomial(-X-X^2-X^3-X^4)
In [ ]: