Open In Colab

9. Clases#

En Python, todo es un objeto, es decir, una instancia o realización de un clase. Cada objeto tiene unos métodos:

objeto.metodo(...)
que corresponde  a funciónes internas del objeto, y también puede tener _atributos_:
objeto.atributo

que son variables internas dentro del objeto: self.atributo=.....

Algunos objetos en Python también pueden recibir parámetros de entrada a modo de claves de un diccionario interno del objeto

objeto['key']='value'

entro otras muchas propiedades

Ejemplos de objetos son:

  • int: Enteros

  • float: Números punto flotante (floating point numbers)

  • str: Cadenas de caracteres

  • list: Listas

  • dict: Diccionarios.

Si el tipo de objeto es conocido, uno pude comprobar si un objeto determinado corresponde a ese tipo con isinstance:

s='hola mundo'
print('Is `s` an string?: {}'.format( isinstance(s,str)) )
print('Is `s` a float?: {}'.format( isinstance(s,float)) )
Is `s` an string?: True
Is `s` a float?: False

Dentro del paradigma de objetos es posible agrupar diferentes conjuntos de variables de modo que el nombre de un atributo a método adquiere dos partes, una parte principal, que en el análogo con el nombre completo de una persona podríamos asimilar a su apellido, y el método o atributo, que sería como el primer nombre, separados por un punto:

  • Método: last_name.first_name()

  • Atributo: last_name.first_name.

Esto nos permite tener objetos dentro de un programa con igual nombre, pero diferente appellido. Como por ejemplo, los diferentes \(\cos(x)\) que vienen en los diferentes módulos matématicos implementados en Python. Por eso es recomendado cargar los módulos manteniendo el espacio de nombres (que en nuestra analogía sería el espacio de apellidos). Cuando el modulo tenga un nombre muy largo (más de cuatro caracteres), se puede usar una abreviatura lo suficientemente original para evitar que pueda ser sobreescrita por un nuevo objeto:

import math as math
import numpy as np
k=3 #N/m
m=2 #Kg
A=3 #m
t=2 #s
ω=np.sqrt(k/m) #rad/s
#ver https://pyformat.info/
print('x(t)={:.2f} m'.format( 
    A*np.cos( ω*t )
     ))
print('x(t)={:.2f} m'.format( 
    A*math.cos( ω*t )
     ))
x(t)=-2.31 m
x(t)=-2.31 m

Note que import math as m entraría en conflicto con la definición de m en m=2

La forma recomendada de importar los diferentes módulos y el uso de sus métodos y atributos suele resumirse en Cheat Sheets. Para Python científico recomendamos las elaboradas por Data Camp, que pueden consultarse aquí

Para programación de Python en general se recomienda el estándar PEP 8 de Python

Antes de comenzar con las clases, es conveniente resumir el paradigma de programación funcional:

9.1. Programación funcional#

En cálculo científico el paradigma funcional, en el cual el programa se escribe en términos de funciones, suele ser suficiente.

El esqueleto de un programa funcional es típicamente del siguiente tipo

#!/usr/bin/env python3
import somemodule as sm
def func1(...):
    '''
    Ayuda func1
    '''
    .....

def func2(...):
    '''
    Ayuda func2
    '''
    .....
    
 
def main(...):
    '''
    Ayuda función principal
    '''
    x=func1(...)
    y=func2(x)
    
if __name__=='__main__':
    z=main(...)
    print('El resultado final es: {}'.format(z))

Para su diseño, el programa se debe separar sus partes independientes y para cada una de ellas se debe definir una función.

La función ideal es una que se pueda reutilizar facilamente en otro contexto.

La última función, main(...) combina todas las anteriores para entregar el resultado final del programa.

La instrucción

if __name__=='__main__'

permite que el programa pueda ser usado también como un módulo de Python, es decir que se pueda cargar desde otro programa con el import. En tal caso, la variable interna de Python __name__ es diferente a la cadena de caracteres '__main__' y esa parte del programa no se ejecuta. Dentro de Jupyter:

__name__
'__main__'

Ejemplo módulo

%%writefile example.py
#!/usr/bin/env python3
print( f'check: {__name__}')

def hola():
    print( f'check {__name__} como módulo:')
    print('mundo')
    
if __name__=='__main__':
    hola()
Overwriting example.py

ls funciona

ls -l example.py
-rwxr-xr-x 1 restrepo restrepo 171 Mar 31 14:11 example.py*

cat funciona

cat example.py 
#!/usr/bin/env python3
print( f'check: {__name__}')

def hola():
    print( f'check {__name__} como módulo:')
    print('mundo')
    
if __name__=='__main__':
    hola()

Cambia los permisos a ejecución

! chmod a+x example.py
ls -l example.py
-rwxr-xr-x 1 restrepo restrepo 171 Mar 31 14:11 example.py*

Corre el programa desde la consola

!./example.py
check: __main__
check __main__ como módulo:
mundo

Uso como módulo

import example
check: example
example.hola()
check example como módulo:
mundo

Ejecutarlo desde una celda de Jupyter es equivalente a ejecutarlo desde la consola

#!/usr/bin/env python3
print( 'check __name__: {}'.format(__name__))

def hola():
    print('mundo')
    
if __name__=='__main__':
    hola()
check __name__: __main__
mundo
%%writefile example.py
#!/usr/bin/env python3
import sys
if __name__=='__main__':
    print(sys.argv)
Overwriting example.py
!./example.py df ll 1 3
['./example.py', 'df', 'll', '1', '3']

9.2. Clases#

Aunque en Python se puede trabajar directamente con objetos, en general un objeto es una instancia de un clase. Es decir, debe incializarse a partir de una Clase. Esto típicamente involucra ejecutar varios métodos e inicializar varios atributos bajo el espacio de nombres del objeto incializado. Por ejemplo, para inicializar las clases ‘int’, ‘float’, ‘str’, ‘list’,’dict’

n=int() ## ⬄ to n=0
n
0
  • int es la clase

  • int() es la instancia de la clase

  • n=int() es el objeto asociado a la clase: n.→ contiene todos los atributos y los métodos de la clase int

x=float(6) ## ⬄ x=3.
x
6.0
l=list() ## ⬄ l=[]
l
[]
d=dict() ## ⬄ d={}
d
{}
#tupla vacía
A=set()

La principal motivación para escribir una clase en lugar de una función, es que la clase puede ser la base para generar nuevas clases que hereden los métodos y atributos de la clase original.

Por ejemplo el DataFrame de Pandas es una clase que puede ser inicializada de múltiples formas. Además, está diseñada para que pueda ser extendida facilmente: https://pandas.pydata.org/pandas-docs/stable/development/extending.html

import pandas as pd
In[1]: pd.DataFrame??
...
class DataFrame(NDFrame):
    ...
    ## ----------------------------------------------------------------------
    ## Constructors

    def __init__(self, data=None, index=None, columns=None, dtype=None,
                 copy=False):
        if data is None:
            ...

Como puede verse, un DataFrame es una subclase (es decir, un caso especial) de NDFrame.

Una instancia u objeto de la clase DataFrame, df a continuación, se puede inicializar de diferentes maneras

df=pd.DataFrame()
df
df=pd.DataFrame([{'A':1,'B':2}])
df
A B
0 1 2
df=pd.DataFrame({'A':[1],'B':[2]})
df
A B
0 1 2

9.2.1. Programación por clases#

Una clase se puede pensar como un conjuto de funciones y atributos que comparten algo en común.

Algunas veces, cuando la complejidad del problema se puede descomponer en una estructura de capas, donde la capa interna es la mas simple y las capas más externas van aumentando la complejidad, pude ser conveniente pensar en una estructura de clases.

De hecho, la clase básica puede heredar todas sus propiedades a subclases basadas en ella.

El espacio de nombres asociado a la clase se define con el nombre génerico de self el cual toma el nombre de las instancia (objeto) asociada a la inicialización de la clase. Las variables globales de la clase pasán a ser automáticamente atributos del objeto, y nuevo atributos de pueden definir dentro del espacio de nombres self dentro de cada función de la clase.

Esqueleto:

class clasenueva:
     '''
     Ayuda de la clase
    '''

    var1='valor' #variable global → atributo de la clase
    def func1(self,...): #método de la clase
        '''
        Ayuda del método
        '''
        self.var2='hola mundo' #atributo de la clase
        ....
    def func2(self,....):
        '''
        Ayuda del método
        '''        
        print(self.var1)
        print(self.var2)
        ....
    
    def main(self,....):
        ....

Cada una de las funciones pasan a ser métodos de la clase, mientras que cada una de la variables globales y con espacio de nombre self pasan a ser atributos de la clase.

Ejemplo:

class clasenueva:
    var='hola'
    var2=[]
    def main(self):
        self.var=self.var+' mundo'
        self.var3=self.var2+[3]
        print(self.var)

Creación del objeto c, de manera que selfc. c es una instancia de la clase clasenueva a continuación:

c=clasenueva()
c.var
'hola'

main es un método de la clase

c.main()
hola mundo

var es un atributo de la clase

c.var
'hola mundo'
c.var3
[3]

Para la creación de clases disponemos de métodos especiales, constructores, que podemos definir para adicionar “magia” a nuestras clases. Estas están simpre rodeadas de un doble guión bajo, por ejemplo: __init__ o __lt__. Una lista completa de ellos con su explicación de uso se puede encontrar en 1.

Resaltamos a continuación algunos de ellos

  • __init__: Se ejecuta automáticamente al inicializar la clase

  • __add__: Sobrecarga el operador suma, + → self + other

For example, with __init__, the previous class is

class clasenueva:
    def __init__(self):
        self.var='hola'
        self.var2=[]
    def main(self):
        self.var=self.var+' mundo'
        self.var3=self.var2+[3]
        print(self.var)
c=clasenueva()
c.var
'hola'

9.2.2. Herencia#

Los métodos especiales se pueden heredar automáticamente desde la clase inicial, que llamaremos superclass. Para ello inicializamos la clase con la superclass como argumento, es decir, en forma genérica como:

class subclass(superclass):
    ...
import copy
import random

class animal:
    stage='baby' #or kid or adult
    sex='male'
    eyes=2
    live=True
    def __add__(self,other):
        if (type(self)==type(other) and 
            self.stage=='adult' and 
            other.stage=='adult' and 
            self.sex!=other.sex):
            baby=copy.copy(self)
            baby.stage='baby'
            baby.sex=random.choice(['female','male'])
            if isinstance(self,insect):
                baby.stage=='larvae'            
            return baby
        else:
            return None

class insect(animal):
    def __init__(self,stage='larvae',sex='male',wings=0,
                      legs=0,anntenaes=0,eyes=0,stings=0):
        self.stage=stage
        self.sex=sex
        self.wings=wings
        self.legs=legs
        self.anntenaes=anntenaes
        self.eyes=eyes
        self.stings=stings        
        if self.stage=='baby':
            self.stage=='larvae'
        self.bones=False

class vertebrate(animal):
    bones=True

class bird(vertebrate):
    def __init__(self,stage='baby',sex='male'):
        self.stage=stage
        self.sex=sex
        self.wings=2
        self.legs=2
        self.eyes=2
        self.feathers=True
        
butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700)
pigeon = bird(stage='adult',sex='female')
other_pigeon=bird(stage='adult',sex='male')
butterfly.live
True
#__add__( self  ,  other      )    
squab  = pigeon + other_pigeon
squab.stage,squab.sex
('baby', 'male')
pigeon+pigeon
butterfly+pigeon

Cree una clase mamal o una clase fish

9.2.3. Reescribir método de superclass#

class animal:
    def __init__(self):
        self.stage='baby' #or kid or adult
        self.sex='male'
        self.eyes=2
        self.live=True
    def __add__(self,other):
        if (type(self)==type(other) and 
            self.stage=='adult' and 
            other.stage=='adult' and 
            self.sex!=other.sex):
            baby=copy.copy(self)
            baby.stage='baby'
            baby.sex=random.choice(['female','male'])
            if isinstance(self,insect):
                baby.stage=='larvae'            
            return baby
        else:
            return None

class insect(animal):
    def __init__(self,stage='larvae',sex='male',wings=0,
                      legs=0,anntenaes=0,eyes=0,stings=0):
        self.stage=stage
        self.sex=sex
        self.wings=wings
        self.legs=legs
        self.anntenaes=anntenaes
        self.eyes=eyes
        self.stings=stings        
        if self.stage=='baby':
            self.stage=='larvae'
        self.bones=False            
butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700)
butterfly.live
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In [169], line 2
      1 butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700)
----> 2 butterfly.live

AttributeError: 'insect' object has no attribute 'live'

9.2.4. Use un método de la superclass con super#

Si lo que queremos es modificar el comportomiento de un método de una superclase entonces debemos usar la función super.

Para mantener la herencia el comportamiento de los métodos de una clase inicial, ls super clase, se usa la función super que mantiene la herencia. Ver:

  • https://realpython.com/python-super/ (Backup)

  • https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ (Backup)

super() gives you access to methods in a superclass from the subclass that inherits from it.

super() is the same as super(__class__, <first argument>): the first is the subclass, and the second parameter is an object that is an instance of that subclass.

La estructura general es como la siguiente, basada en Inherited class variable modification in Python:

class subclass(superclass):
    def __special__(self,*args, **kwargs):
        #Modifications to __especial__ here
        ...
        super(subclass, self).__special__(*args, **kwargs)
    ...

Modificar la clase animal con un __init__ y generar las subclases apropiadamente

class animal:
    def __init__(self):
        self.stage='baby' #or kid or adult
        self.sex='male'
        self.eyes=2
        self.live=True
    def __add__(self,other):
        if (type(self)==type(other) and 
            self.stage=='adult' and 
            other.stage=='adult' and 
            self.sex!=other.sex):
            baby=copy.copy(self)
            baby.stage='baby'
            baby.sex=random.choice(['female','male'])
            if isinstance(self,insect):
                baby.stage=='larvae'            
            return baby
        else:
            return None

class insect(animal):
    '''
    The pointers `*args` and `**kwargs` are not really necessary
    because the `__init__` in superclass does not have any
    '''
    def __init__(self,*args,stage='larvae',sex='male',wings=0,
        legs=0,anntenaes=0,eyes=0,stings=0,**kwargs):
        '''
        stage='larvae',sex='male',wings=0,
        legs=0,anntenaes=0,eyes=0,stings=0        
        '''
        super(insect, self).__init__(*args,**kwargs)        
        
        self.stage=stage
        self.sex=sex
        self.wings=wings
        self.legs=legs
        self.anntenaes=anntenaes
        self.eyes=eyes
        self.stings=stings        
        if self.stage=='baby':
            self.stage=='larvae'
        self.bones=False
butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700)
butterfly.live
True
class animal:
    def __init__(self,live=True):
        self.stage='baby' #or kid or adult
        self.sex='male'
        self.eyes=2
        self.live=live
    def __add__(self,other):
        if (type(self)==type(other) and 
            self.stage=='adult' and 
            other.stage=='adult' and 
            self.sex!=other.sex):
            baby=copy.copy(self)
            baby.stage='baby'
            baby.sex=random.choice(['female','male'])
            if isinstance(self,insect):
                baby.stage=='larvae'            
            return baby
        else:
            return None

class insect(animal):
    '''
    The pointers `*args` and `**kwargs` are necessary
    to access to the `**kwargs` of  the `__init__` in superclass
    '''    
    def __init__(self,*args,stage='larvae',sex='male',wings=0,
        legs=0,anntenaes=0,eyes=0,stings=0,**kwargs):
        '''
        stage='larvae',sex='male',wings=0,
        legs=0,anntenaes=0,eyes=0,stings=0        
        '''
        super(insect, self).__init__(*args,**kwargs)        
        
        self.stage=stage
        self.sex=sex
        self.wings=wings
        self.legs=legs
        self.anntenaes=anntenaes
        self.eyes=eyes
        self.stings=stings        
        if self.stage=='baby':
            self.stage=='larvae'
        self.bones=False
butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700,live=False)
butterfly.live
False

9.3. Clase vector#

Comenzaremos definiendo un alias de clase list que llameremos vector y que funcione exactamente igual que la clase lista. Todas los métodos especiales se heredan automáticamente

class vector(list):
    pass
l=list()
v=vector()
v.append(1)
v
[1]
l1=[1,2]
l2=[3,4]
l1+l2
[1, 2, 3, 4]

Vamos a inicializar dos instancias de la clase vector y comprobar que suman como listas

v1=vector(l1)
v2=vector(l2)
v1+v2
[1, 2, 3, 4]

Ahora reemplazaremos el método mágico __add__ de la lista para que el operador + realice la suma vectorial:

l1,l2
([1, 2], [3, 4])
map?
Init signature: map(self, /, *args, **kwargs)
Docstring:     
map(func, *iterables) --> map object

Make an iterator that computes the function using arguments from
each of the iterables.  Stops when the shortest iterable is exhausted.
Type:           type
Subclasses:     
list(map(lambda x,y:x+y,l1,l2))
[4, 6]
class vector(list):
    def __add__(self, other):
        '''
        __add__ asocia la operación del símbolo '+'
        '''
        return vector(map(lambda x,y: x+y, self, other))    
v1=vector(l1)
v2=vector(l2)
v1+v2
[4, 6]
v1-v2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [182], line 1
----> 1 v1-v2

TypeError: unsupported operand type(s) for -: 'vector' and 'vector'
class vector(list):
    def __add__(self, other):
        '''
        __add__ asocia la operación del símbolo '+'
        '''
        return vector(map(lambda x,y: x+y, self, other))
    def __sub__(self, other):
        '''
        __sub__ asocia la operación del símbolo '-'
        '''
        return vector(map(lambda x,y: x-y, self, other))        

Reiniciando el kernel de jupyter

v1=vector([5,8])
v2=vector([3,6])
v1+v2
[8, 14]
v1-v2
[2, 2]
butterfly=insect(stage='adult',wings=2,legs=6,anntenaes=2,eyes=1700)
butterfly.live
True

Como ejemplo, vamos a implemetar el atributo modulus a la clase vector dentro del método especial __init__:

import math
class vector(list):
    def __init__(self,*args, **kwargs):
        '''
        Add the modulus of a vector at initialization 
        '''
        try:
            l=args[0]
            self.modulus=math.sqrt( sum( map( lambda l: l*l,l )) )
        except:
            self.modulus=0
        super(vector, self).__init__(*args, **kwargs)
    def __add__(self, other):
        '''
        __add__ asocia la operación del símbolo "+"
        '''
        return vector(map(lambda x,y: x+y, self, other))
    def __sub__(self, other):
        '''
        __sub__ asocia la operación del símbolo "-"
        '''
        return vector(map(lambda x,y: x-y, self, other))        
v1=vector( [3,2] )
v1
[3, 2]
v1.modulus
3.605551275463989
type(v1)
__main__.vector
[d for d in dir(v1) if d.find('__')==-1] 
['append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'modulus',
 'pop',
 'remove',
 'reverse',
 'sort']
[d for d in dir(butterfly) if d.find('__')==-1] 
['anntenaes',
 'bones',
 'eyes',
 'legs',
 'live',
 'sex',
 'stage',
 'stings',
 'wings']

Una implementación completa de la clase vector, adapta de vector: a list based vector class supporting elementwise operations (python recipe), se puede encontrar aquí

9.4. Otro ejemplo#

class veterinaria:
    def __init__(self,x):
        self.tipo={'perro':'guau','gato':'miau'}
        self.sonido=self.tipo.get(x)
    def __call__(self,nombre):
        if nombre=='greco':
            print(self.sonido)
        else:
            print("grrr!!")
    def color(self,nombre):
        if nombre=='greco':
            print('blanco')
  • Cundo se inicializa la clase la función llamada __init__, se ejecuta automáticamente.

  • La función __call__ permite usar el objeto directamente como función, sin hacer referencia al método, que por supuesto es __call__

Hay muchos otros métodos especiales, que comienzan con un __…. Usar <TAB> a continuación para ver algunos

veterinaria.__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_664677/3273899342.py in <module>
----> 1 veterinaria.__

AttributeError: type object 'veterinaria' has no attribute '__'

Para utilizar la clase, primero se debe incializar como un objeto. Crear la instancia de la clase.

Ejemplo: Crea la instancia mascotafeliz

mascotafeliz=veterinaria('perro')
mascotafeliz.sonido
'guau'
mascotafeliz('greco')
guau
mascotafeliz.color('greco')
blanco
mascotafeliz.tipo.get('vaca')

9.5. Herencia#

import pandas as pd
class finanzas(pd.DataFrame):
    pass
f=finanzas()
type(f)
__main__.finanzas
f([{'A':1}])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_664677/2358335894.py in <module>
----> 1 f([{'A':1}])

TypeError: 'finanzas' object is not callable

Para realmente heradar hay que inicializarlo de forma especial con la función super

class hkdict(dict):
    def __init__(self,*args, **kwargs):
        super(hkdict, self).__init__(*args, **kwargs)
    def has_key(self,k):
        return k in self
d=dict()
d['perro']='guau'
d['gato']='miau'
d.get('vaca')
d.has_key('gato')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/tmp/ipykernel_664677/1012315857.py in <module>
----> 1 d.has_key('gato')

AttributeError: 'dict' object has no attribute 'has_key'
dd=hkdict()
dd['perro']='guau'
dd['gato']='miau'
dd.has_key('vaca')
False

9.6. Implementation of arXiv:1905.13729#

See also: DOI: 10.1103/PhysRevD.101.095032

Repo: https://github.com/restrepo/anomalies

9.6.1. General solution to the U(1) anomaly equations#

Let a vector \(\boldsymbol{z}\) with \(N\) integer entries such that $\( \sum_{i=1}^N z_i=0\,,\qquad \sum_{i=1}^N z_i^3=0\,.\)\( We like to build this set of of \)N\( from two subsets \)\boldsymbol{\ell}\( and \)\boldsymbol{k}$ with sizes

(9.1)#\[\begin{align} \operatorname{dim}(\boldsymbol{\ell})=& \begin{cases} \alpha=\frac{N}{2}-1\,, & \text{ if $N$ even } \\ \beta=\frac{N-3}{2}\,, & \text{ if $N$ odd }\\ \end{cases};& \operatorname{dim}(\boldsymbol{k})=& \begin{cases} \alpha=\frac{N}{2}-1\,, & \text{ if $N$ even } \\ \beta+1=\frac{N-1}{2}\,, & \text{ if $N$ odd }\\ \end{cases}; \end{align}\]
  • \(N\) even: Consider the following two examples of \(\boldsymbol{z}\) with vector-like solutions, i.e, with opposite integeres which automatically satisfy the equations

(9.2)#\[\begin{align} \boldsymbol{x}=&\left(\ell_{1}, {k}_{1}, \ldots, k_{\alpha},-\ell_{1},-k_{1}, \ldots,-k_{\alpha}\right)\\ \boldsymbol{y}=&\left(0,0,\ell_{1}, \ldots, \ell_{\alpha},-\ell_1, \ldots,-\ell_{\alpha}\right)\,. \end{align}\]
  • \(N\) odd: Consider the two vector-like solutions

(9.3)#\[\begin{align} \begin{array} \boldsymbol{x}=&\left(0, k_{1}, \ldots, k_{\beta+1},-k_{1}, \ldots,-k_{\beta+1}\right) \\ \boldsymbol{y}=&\left(\ell_{1}, \ldots, \ell_{\beta}, k_{1}, 0,-\ell_{1}, \ldots,-\ell_{\beta},-k_{1}\right) \end{array} \end{align}\]

From any of this, we can build a final \(\boldsymbol{z}\) which can includes chiral solutions, i.e, non vector-like solutions

\[ \boldsymbol{x} \oplus \boldsymbol{y} \equiv\left(\sum_{i=1}^{N} x_{i} y_{i}^{2}\right)\boldsymbol{x}-\left(\sum_{i=1}^{N} x_{i}^{2} y_{i}\right)\boldsymbol{y}\,. \]
import numpy as np
class free(list):
    def __init__(self,*args, **kwargs):
        '''
        Convert list to anomaly free solution
        '''
        assert np.array(args).sum()==0
        assert (np.array(args)**3).sum()==0        
        super(free, self).__init__(*args, **kwargs)
    def __add__(self, other):
        '''
        Add to anomaly free solutions to obtain 
        a new anomaly free solutions which is not
        necessarily vector-like
        '''
        x=np.array(self)
        y=np.array(other)
        return free((x*y**2).sum()*x-(x**2*y).sum()*y)
        
def _z(l,k,sort=True,reverse=False):
    '''
    Implementation of arXiv:1905.13729
    For l,k two set of same dimensions (or k with an extra dimension)
    return a builded array z, such that
     sum( z )=0
     sum( z**3)=0
    '''
    l=list(l)
    k=list(k)
    #Build vector-like solutions x,y
    if len(l)==len(k) :
        x=free( [l[0]]+k+[-l[0]]+[-i for i in k ] )
        y=free( [0,0] +l        +[-i for i in l ] )
    else:
        x=free( [0]+k+[-i for i in k ] )
        y=free( l+[k[0]]+[0]+[-i for i in l ]+[-k[0]])
    xfac=0
    yfac=0
    ## Build not trivial solution
    zz=x+y
    if sort:
        zz=sorted( zz ,key=abs, reverse=reverse ) 
    return zz
class solution(free):
    def __init__(self,l,k,sort=True,reverse=False,**kwargs):
        zz=_z(l,k,sort=sort,reverse=reverse)
        self.gcd=np.gcd.reduce(zz)
        self.simplified=free((zz/self.gcd).astype(int))
        super(solution, self).__init__(zz, **kwargs)
    def asarray(self):
        self.simplified=np.asarray(self.simplified)
        return np.asarray(self)
    def to_list(self):
        self.simplified=list(self.simplified)
        return list(self)
free([-1, 4, -2, 1, -4, 2])
[-1, 4, -2, 1, -4, 2]
x=solution([-1,1],[4,-2])
type(x)
__main__.solution
x
[3, 3, 3, -12, -12, 15]
x.gcd
3
x.simplified
[1, 1, 1, -4, -4, 5]
type(x.simplified)
__main__.free
(np.asarray(x.simplified)**3).sum()
0
x.asarray()
array([  3,   3,   3, -12, -12,  15])
x.to_list()
[3, 3, 3, -12, -12, 15]
y=solution([1,1],[4,-2])
y
[-5, 5, -20, 20, 25, -25]
z=x.simplified+y.simplified
z
[1, 1, 1, -4, -4, 5, -1, 1, -4, 4, 5, -5]
np.asarray(z).sum()
0
(np.asarray(z)**3).sum()
0

9.6.2. Python implmentation#

Obtain a numpy array z of N integers which satisfy the Diophantine equations

>>> z.sum()
0
>>> (z**3).sum()
0

The input is two lists l and k with any (N-3)/2 and (N-1)/2 integers for N odd, or N/2-1 and N/2-1 for N even (N>4). The function is implemented below under the name: free(l,k)

Open In Colab

https://stackoverflow.com/a/43793179/2268280

import pandas as pd
import numpy as np
from astropy.table import Table
import itertools
import sys
import os
from functools import reduce
import warnings
warnings.filterwarnings("ignore")
!pip install anomalies 2>/dev/null > /dev/null
from anomalies import anomaly
anomaly.free([-1,1],[4,-2])
array([  3,   3,   3, -12, -12,  15])
anomaly.free.gcd
3
anomaly.free.simplified
array([ 1,  1,  1, -4, -4,  5])
z=anomaly.free

9.6.3. Analysis#

solutions class → Initialize the object to obtain anomaly free solutions for any set of N integers

#TODO: inherit from free class
import sys
def _get_chiral(q,q_max=np.inf):
    #Normalize to positive minimum
    if q[0]<0 or (q[0]==0 and q[1]<0):
        q=-q
    #Divide by GCD
    GCD=np.gcd.reduce(q)
    q=(q/GCD).astype(int)
    if ( #not 0 in z and 
          0 not in [ sum(p) for p in itertools.permutations(q, 2) ] and #avoid vector-like and multiple 0's
          #q.size > np.unique(q).size and ## check for at least a duplicated entry
          np.abs(q).max()<=q_max
           ):
        return q,GCD
    else:
        return None,None
class solutions(object):
    '''
    Obtain anomaly free solutions with N chiral fields
    
    Call the initialize object with N and get the solutions:
    Example:
    >>> s=solutions()
    >>> s(6) ## N = 6
    
    Redefine the self.chiral function to implement further restrictions:
    inherit from this class and define the new chiral function
    '''
    def __init__(self,nmin=-2,nmax=2,zmax=np.inf):
        self.nmin=nmin
        self.nmax=nmax
        self.zmax=zmax
        self.CALL=False

    def __call__(self,N,*args,**kwargs):
        self.CALL=True
        if N%2!=0: #odd
            N_l=(N-3)//2
            N_k=(N-1)//2
        else: #even
            N_l=N//2-1
            N_k=N_l
        r=range(self.nmin,self.nmax+1)
        self.ls=list(itertools.product( *(r for i in range(N_l)) ))
        self.ks=list(itertools.product( *(r for i in range(N_k)) ))
        return self.chiral(*args,**kwargs)
        
        
    def chiral(self,*args,**kwargs):
        if not self.CALL:
            sys.exit('Call the initialized object first:\n>>> s=solutions()\n>>> self(5)')
        self.list=[]
        solt=[]
        for l in self.ls:
            for k in self.ks:
                l=list(l)
                k=list(k)
                q,gcd=_get_chiral( z(l,k) )
                #print(z(l,k))
                if q is not None and list(q) not in self.list and list(-q) not in self.list:
                    self.list.append(list(q))
                    solt.append({'l':l,'k':k,'z':list(q),'gcd':gcd})
        return solt

Chiral solutions for l and k in the range [-2,2]

s=solutions()

solutions for \(N=5\) integers

s(5)
[{'l': [-2], 'k': [-1, 2], 'z': [2, 4, -7, -9, 10], 'gcd': 1},
 {'l': [-2], 'k': [2, -1], 'z': [1, 5, -7, -8, 9], 'gcd': 4}]
pd.DataFrame(  s(5)  )
l k z gcd
0 [-2] [-1, 2] [2, 4, -7, -9, 10] 1
1 [-2] [2, -1] [1, 5, -7, -8, 9] 4

To filter solutions with duplicate or triplicate integers, let us create a class dark that inherits from solutions. Therefore, in the argument of the new class is the old class instead of just object

class dark(solutions):
    '''
    Modify the self.chiral function to obtain solutions
    with either duplicate or triplicate integers
    '''
    def chiral(self,X=False,verbose=False,print_step=100000):
        m=2
        if X:
            m=3
        self.list=[]
        solt=[]
        tot=len(self.ls)*len(self.ks)
        i=0
        for l in self.ls:
            for k in self.ks:
                if verbose:
                    i=i+1
                    if i%print_step==0:
                        print('{}/{}'.format(i,tot))
                l=list(l)
                k=list(k)
                q,gcd=_get_chiral( z(l,k) )
                #print(z(l,k))
                if (q is not None and 
                    list(q) not in self.list and list(-q) not in self.list and
                    1 in [ len(set(p)) for p in itertools.permutations(q, m) ] and
                    #q.size-np.unique(q).size>m
                    np.abs(q).max()<=self.zmax
                   ):
                    self.list.append(list(q))
                    solt.append({'l':l,'k':k,'z':list(q),'gcd':gcd})
        return solt        

Chiral solutions with repeated integers

s=dark()

Example: Force solutions with triplicate integers

s(5)
[]
pd.DataFrame(   s(6,X=True)   )
l k z gcd
0 [-2, 2] [-2, -1] [1, 1, 1, -4, -4, 5] 8
%%time
s=dark(nmin=-30,nmax=30)
s(5)
CPU times: user 16.5 s, sys: 7.15 ms, total: 16.5 s
Wall time: 16.6 s
[]
%%time
s=dark(nmin=-10,nmax=10,zmax=32)
s(6,X=True,verbose=True)
100000/194481
CPU times: user 19.9 s, sys: 0 ns, total: 19.9 s
Wall time: 19.9 s
[{'l': [-10, 5], 'k': [-2, 4], 'z': [1, 1, 1, -4, -4, 5], 'gcd': 1500},
 {'l': [-10, 5], 'k': [3, 4], 'z': [3, 3, 3, -10, -17, 18], 'gcd': 250}]
%%time
s=dark(nmin=-21,nmax=21,zmax=32)
slt=s(6,verbose=True,print_step=500000)
500000/3418801
1000000/3418801
1500000/3418801
2000000/3418801
2500000/3418801
3000000/3418801
CPU times: user 4min 20s, sys: 27.1 ms, total: 4min 20s
Wall time: 4min 20s

9.6.4. Example#

Simple csv to json converter

import csv as CSV
class read_csv:
    def __init__(self,f):
        self.data=[]
        f=open(f,'r')
        dt=CSV.reader(f)
        for row in dt:
              self.data.append(row)
        f.close()
    def to_json(self):
        return [dict(zip(data[0],d)) for d in self.data[1:]]

9.7. Decorating classes#

From https://realpython.com/primer-on-python-decorators/#decorating-classes

Some commonly used decorators that are even built-ins in Python are @classmethod, @staticmethod, and @property. The @classmethod and @staticmethod decorators are used to define methods inside a class namespace that are not connected to a particular instance of that class. The @property decorator is used to customize getters and setters for class attributes.

9.8. References#

[1] A Guide to Python’s Magic Methods

[2] https://realpython.com/python3-object-oriented-programming/

[3] Building Skills in Object-Oriented Design

[4] Ver también: https://gist.github.com/mcleonard/5351452

[5] Bhasin, Harsh. Python Basics: A Self-teaching Introduction. Stylus Publishing, LLC, 2018. [PDF] [Google Scholar]