<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Numero complessi builtin
L'esempio più semplice in Python di istanza di una classe in Python è un oggetto che rappresenta un numero complesso.
Step4:  Definizione di un nuovo ADT
Step5:  Danger ZONE !!!
Step8:  Inheritance e Operator Overloading
Supponiamo ora di definire un nuovo tipo chiamato NCO che rappresenta un numero complesso, ma su cui vogliamo specificare l'overloading di due operazione
Step9:  Classes vs. Closures
Step10:  NOTA
<ASSISTANT_TASK:>
Python Code:
a = 2 + 3j
print(a, type(a))
Explanation: Numero complessi builtin
L'esempio più semplice in Python di istanza di una classe in Python è un oggetto che rappresenta un numero complesso.
End of explanation
class NumeroComplesso(object):
    def __init__(self, real, imag):
        Metodo costruttore, chiamato quando viene 
           inizializzato un nuovo oggetto
        self.a = real
        self.b = imag
        
    def somma(self, c):
        Somma al numero corrente il numero complesso c
        # DA COMPLETARE
        pass
        
    def __str__(self):
        Ritorna una stringa che rappresenta il numero
        return str(self.a) + ' + ' + str(self.b) +'i'
type(NumeroComplesso)
a = NumeroComplesso(2,3)
b = NumeroComplesso(1,-2)
print(a)
a.somma(b)
print(a)
Explanation: Definizione di un nuovo ADT: NumeroComplesso
Vediamo ora come aggiungere un nuovo tipo che sia una nostra implementazione di un numero complesso. Una possibile implementazione di base è la seguente. Si noti che qualsiasi oggetto in Python eredità dall'oggetto base object.
ESERCIZIO 1: Completare il metodo somma nel codice seguente.
End of explanation
a.a = 0
a.somma(b)
print(a)
Explanation: Danger ZONE !!!
End of explanation
class NCO(NumeroComplesso):
    # EREDITA DI I METODI E ATTRIBUTI DELLA CLASSE "NUMERO COMPLESSO"
    def __add__(self, c):
        Esempio di OPERATOR OVERLOADING: addizione
        # DA COMPLETARE
        pass
    
    def __eq__(self, c):
        Esempio di OPERATOR OVERLOADING: confronto
        return self.a == c.a and self.b == c.b
c = NCO(1,2)
print(c)
type(c.somma)
type(c.a)
a == c
d = NCO(1,2)
print(c, id(c))
print(d, id(d))
print(d == c)
Explanation: Inheritance e Operator Overloading
Supponiamo ora di definire un nuovo tipo chiamato NCO che rappresenta un numero complesso, ma su cui vogliamo specificare l'overloading di due operazione:
La somma di due numeri complessi
L'operatore di confronto logico di uguaglianza tra due numeri complessi
Per specificare che la classe NCO estende la classe base NumeroComplesso basta dichiararla come:
class NCO(NumeroComplesso):
Si notiche si è racchiuso tra parentesi il nomde del tipo da cui si ereditano attributi e metodi.
ESERCIZIO 2: Si completi il metodo __add__ nella classe NCO in modo che restituisca un nuovo oggetto che, dati due numeri a e b, rappresenti il numero complesso c = a + b.
End of explanation
# Definizione di una classe che implementa un "adder"
class Adder(object):
    def __init__(self, n=0):
        self.n = n   # Stato mutabile della classe (DANGER ZONE!!!)
    def __call__(self, m):
        return self.n + m
    
add5_istanza = Adder(5)
print(add5_istanza(1), add5_istanza(7))
# Definizione di una closure che implementa un "adder"
def make_adder(n=0):
    def adder(m):
        return n + m
    return adder
add5_function = make_adder(5)
print(add5_function(1), add5_function(7))
add5_istanza.n = 1
print(add5_istanza(1), add5_istanza(7))
print(add5_function(1), add5_function(7))
Explanation: Classes vs. Closures
End of explanation
# Definizione di una classe che implementa un "counter"
class Counter(object):
    def __init__(self, n=0):
        self.n = n
    def __call__(self):
        self.n += 1
        return self.n
    
counter_istanza = Counter(5)
print(counter_istanza(), counter_istanza())
# Definizione di una closure che implementa un "counter"
def make_counter(n=0):
    def state():
        c = n
        while True:
            c += 1
            yield c
        
    def counter():
        return next(f)
        
    f = state()
    
    return counter
counter_function = make_counter(5)
print(counter_function(), counter_function())
# faccio una modifica apparentemente "innocua"
counter_istanza.n = 'ciao'
print(counter_function(), counter_function())
# ... dopo un po` riutilizzo il counter_istanza
print(counter_istanza())
Explanation: NOTA: Con gli oggetti per poter capire l'esecuzione del codice bisogna capire qual'è stata la storia di esecuzione precedente...
End of explanation
<END_TASK>