resueltos - poo python 3
¿Inicializar automáticamente variables de instancia? (15)
Citando el Zen de Python ,
Explícito es mejor que implícito.
Tengo una clase de python que se ve así:
class Process:
def __init__(self, PID, PPID, cmd, FDs, reachable, user):
seguido por:
self.PID=PID
self.PPID=PPID
self.cmd=cmd
...
¿Hay alguna manera de autoinicializar estas variables de instancia, como la lista de inicialización de C ++? Ahorraría un montón de código redundante.
El Sistema de Objetos Common Lisp (CLOS) manejó esto hace décadas:
(defclass person ()
((name :accessor person-name ;make a getter and a setter
:initform ''bill ;default value
:initarg :name) ;initialization arg can be different than
;instance variable
(age :accessor person-age
:initform 10
:initarg :age)))
(setf eric (make-instance ''person :name "eric" :age 75## Heading ##))
Es posible que no sea necesario inicializar las variables, ya que los locales () ya contienen los valores.
clase Maniquí (objeto):
def __init__(self, a, b, default=''Fred''):
self.params = {k:v for k,v in locals().items() if k != ''self''}
d = Maniquí (2, 3)
d.params
{''a'': 2, ''b'': 3, ''predeterminado'': ''Fred''}
d.params [''b'']
3
Por supuesto, dentro de una clase uno podría usar self.params
La biblioteca attrs hace algo como esto.
La solución de Nadia es mejor y más poderosa, pero creo que esto también es interesante:
def constructor(*arg_names):
def __init__(self, *args):
for name, val in zip(arg_names, args):
self.__setattr__(name, val)
return __init__
class MyClass(object):
__init__ = constructor("var1", "var2", "var3")
>>> c = MyClass("fish", "cheese", "beans")
>>> c.var2
"cheese"
Necesitaba algo para el mismo propósito, pero ninguna de las respuestas existentes cubría todos los casos que probé. La respuesta de Nadia fue la más cercana a lo que estaba buscando, así que comencé con su código como base.
El decorador a continuación funciona con todas las combinaciones válidas de argumentos:
Positional __init__(self, a, b )
Keyword __init__(self, a=None, b=None )
Positional + Keyword __init__(self, a, b, c=None, d=None)
Variable Positional __init__(self, *a )
Variable Positional + Keyword __init__(self, *a, b=None )
Variable Positional + Variable Keyword __init__(self, *a, **kwargs )
Positional + Variable Positional + Keyword __init__(self, a, *b, c=None )
Positional + Variable Positional + Variable Keyword __init__(self, a, *b, **kwargs )
Keyword Only __init__(self, *, a=None )
Positional + Keyword Only __init__(self, a, *, b=None )
También implementa la convención estándar _
-prefix para permitir __init__
variables que no se asignarán a instancias de clase.
### StdLib ###
from functools import wraps
import inspect
###########################################################################################################################
#//////| Decorator |//////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################
def auto_assign_arguments(function):
@wraps(function)
def wrapped(self, *args, **kwargs):
_assign_args(self, list(args), kwargs, function)
function(self, *args, **kwargs)
return wrapped
###########################################################################################################################
#//////| Utils |//////////////////////////////////////////////////////////////////////////////////////////////////////#
###########################################################################################################################
def _assign_args(instance, args, kwargs, function):
def set_attribute(instance, parameter, default_arg):
if not(parameter.startswith("_")):
setattr(instance, parameter, default_arg)
def assign_keyword_defaults(parameters, defaults):
for parameter, default_arg in zip(reversed(parameters), reversed(defaults)):
set_attribute(instance, parameter, default_arg)
def assign_positional_args(parameters, args):
for parameter, arg in zip(parameters, args.copy()):
set_attribute(instance, parameter, arg)
args.remove(arg)
def assign_keyword_args(kwargs):
for parameter, arg in kwargs.items():
set_attribute(instance, parameter, arg)
def assign_keyword_only_defaults(defaults):
return assign_keyword_args(defaults)
def assign_variable_args(parameter, args):
set_attribute(instance, parameter, args)
POSITIONAL_PARAMS, VARIABLE_PARAM, _, KEYWORD_DEFAULTS, _, KEYWORD_ONLY_DEFAULTS, _ = inspect.getfullargspec(function)
POSITIONAL_PARAMS = POSITIONAL_PARAMS[1:] # remove ''self''
if(KEYWORD_DEFAULTS ): assign_keyword_defaults (parameters=POSITIONAL_PARAMS, defaults=KEYWORD_DEFAULTS)
if(KEYWORD_ONLY_DEFAULTS): assign_keyword_only_defaults(defaults=KEYWORD_ONLY_DEFAULTS )
if(args ): assign_positional_args (parameters=POSITIONAL_PARAMS, args=args )
if(kwargs ): assign_keyword_args (kwargs=kwargs )
if(VARIABLE_PARAM ): assign_variable_args (parameter=VARIABLE_PARAM, args=args )
###########################################################################################################################$#//////| Tests |//////////////////////////////////////////////////////////////////////////////////////////////////////#$###########################################################################################################################$$if __name__ == "__main__":$$#######| Positional |##################################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, b):$ pass$$ t = T(1, 2)$ assert (t.a == 1) and (t.b == 2)$$#######| Keyword |#####################################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a="KW_DEFAULT_1", b="KW_DEFAULT_2"):$ pass$$ t = T(a="kw_arg_1", b="kw_arg_2")$ assert (t.a == "kw_arg_1") and (t.b == "kw_arg_2")$$#######| Positional + Keyword |########################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, b, c="KW_DEFAULT_1", d="KW_DEFAULT_2"):$ pass$$ t = T(1, 2)$ assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "KW_DEFAULT_2")$$ t = T(1, 2, c="kw_arg_1")$ assert (t.a == 1) and (t.b == 2) and (t.c == "kw_arg_1") and (t.d == "KW_DEFAULT_2")$$ t = T(1, 2, d="kw_arg_2")$ assert (t.a == 1) and (t.b == 2) and (t.c == "KW_DEFAULT_1") and (t.d == "kw_arg_2")$$#######| Variable Positional |#########################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, *a):$ pass$$ t = T(1, 2, 3)$ assert (t.a == [1, 2, 3])$$#######| Variable Positional + Keyword |###############################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, *a, b="KW_DEFAULT_1"):$ pass$$ t = T(1, 2, 3)$ assert (t.a == [1, 2, 3]) and (t.b == "KW_DEFAULT_1")$$ t = T(1, 2, 3, b="kw_arg_1")$ assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1")$$#######| Variable Positional + Variable Keyword |######################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, *a, **kwargs):$ pass$$ t = T(1, 2, 3, b="kw_arg_1", c="kw_arg_2")$ assert (t.a == [1, 2, 3]) and (t.b == "kw_arg_1") and (t.c == "kw_arg_2")$$#######| Positional + Variable Positional + Keyword |##################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, *b, c="KW_DEFAULT_1"):$ pass$$ t = T(1, 2, 3, c="kw_arg_1")$ assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1")$$#######| Positional + Variable Positional + Variable Keyword |#########################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, *b, **kwargs):$ pass$$ t = T(1, 2, 3, c="kw_arg_1", d="kw_arg_2")$ assert (t.a == 1) and (t.b == [2, 3]) and (t.c == "kw_arg_1") and (t.d == "kw_arg_2")$$#######| Keyword Only |################################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, *, a="KW_DEFAULT_1"):$ pass$$ t = T(a="kw_arg_1")$ assert (t.a == "kw_arg_1")$$#######| Positional + Keyword Only |###################################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, *, b="KW_DEFAULT_1"):$ pass$$ t = T(1)$ assert (t.a == 1) and (t.b == "KW_DEFAULT_1")$$ t = T(1, b="kw_arg_1")$ assert (t.a == 1) and (t.b == "kw_arg_1")$$#######| Private __init__ Variables (underscored) |####################################################################$$ class T:$ @auto_assign_arguments$ def __init__(self, a, b, _c):$ pass$$ t = T(1, 2, 3)$ assert hasattr(t, "a") and hasattr(t, "b") and not(hasattr(t, "_c"))
Nota:
Incluí pruebas, pero las colapsé en la última línea ( 58 ) para abreviar. Puede expandir las pruebas, que detallan todos los casos de uso potenciales, find/replace
-todos los $
caracteres con una nueva línea.
Otra cosa que puedes hacer:
class X(object):
def __init__(self, a,b,c,d):
vars = locals() # dict of local names
self.__dict__.update(vars) # __dict__ holds and object''s attributes
del self.__dict__["self"] # don''t need `self`
Pero la única solución que recomendaría, además de solo deletrearla, es "hacer una macro en su editor" ;-p
Para Python 3.3+:
from functools import wraps
from inspect import Parameter, signature
def instance_variables(f):
sig = signature(f)
@wraps(f)
def wrapper(self, *args, **kwargs):
values = sig.bind(self, *args, **kwargs)
for k, p in sig.parameters.items():
if k != ''self'':
if k in values.arguments:
val = values.arguments[k]
if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
setattr(self, k, val)
elif p.kind == Parameter.VAR_KEYWORD:
for k, v in values.arguments[k].items():
setattr(self, k, v)
else:
setattr(self, k, p.default)
return wrapper
class Point(object):
@instance_variables
def __init__(self, x, y, z=1, *, m=''meh'', **kwargs):
pass
Manifestación:
>>> p = Point(''foo'', ''bar'', r=100, u=200)
>>> p.x, p.y, p.z, p.m, p.r, p.u
(''foo'', ''bar'', 1, ''meh'', 100, 200)
Un enfoque sin decorador para Python 2 y 3 utilizando marcos:
import inspect
def populate_self(self):
frame = inspect.getouterframes(inspect.currentframe())[1][0]
for k, v in frame.f_locals.items():
if k != ''self'':
setattr(self, k, v)
class Point(object):
def __init__(self, x, y):
populate_self(self)
Manifestación:
>>> p = Point(''foo'', ''bar'')
>>> p.x
''foo''
>>> p.y
''bar''
Para Python 3.7+ puede usar una clase de datos .
Le permite definir campos para su clase, que son sus variables de instancia inicializadas automáticamente.
Se vería algo así:
@dataclass
class Process:
PID: int
PPID: int
cmd: str
...
El método __init__
ya estará en tu clase.
Tenga en cuenta que aquí se requiere una sugerencia de tipo , es por eso que he usado int
y str
en el ejemplo. Si no conoce el tipo de campo, puede usar Cualquiera del módulo de typing
.
Puede hacerlo fácilmente con los argumentos de la palabra clave, por ejemplo, de esta manera:
>>> class D:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
>>> D(test=''d'').test
''d''
una implementación similar para los argumentos posicionales sería:
>> class C:
def __init__(self, *args):
self.t, self.d = args
>>> C(''abc'', ''def'').t
''abc''
>>> C(''abc'', ''def'').d
''def''
lo cual para mí no parece resolver tu problema.
Puedes usar un decorador:
from functools import wraps
import inspect
def initializer(func):
"""
Automatically assigns the parameters.
>>> class process:
... @initializer
... def __init__(self, cmd, reachable=False, user=''root''):
... pass
>>> p = process(''halt'', True)
>>> p.cmd, p.reachable, p.user
(''halt'', True, ''root'')
"""
names, varargs, keywords, defaults = inspect.getargspec(func)
@wraps(func)
def wrapper(self, *args, **kargs):
for name, arg in list(zip(names[1:], args)) + list(kargs.items()):
setattr(self, name, arg)
for name, default in zip(reversed(names), reversed(defaults)):
if not hasattr(self, name):
setattr(self, name, default)
func(self, *args, **kargs)
return wrapper
Úselo para decorar el método __init__
:
class process:
@initializer
def __init__(self, PID, PPID, cmd, FDs, reachable, user):
pass
Salida:
>>> c = process(1, 2, 3, 4, 5, 6)
>>> c.PID
1
>>> dir(c)
[''FDs'', ''PID'', ''PPID'', ''__doc__'', ''__init__'', ''__module__'', ''cmd'', ''reachable'', ''user''
Si usa Python 2.6 o una versión superior, puede usar collections.namedtuple :
>>> from collections import namedtuple
>>> Process = namedtuple(''Process'', ''PID PPID cmd'')
>>> proc = Process(1, 2, 3)
>>> proc.PID
1
>>> proc.PPID
2
Esto es apropiado especialmente cuando su clase es realmente solo una gran bolsa de valores.
Tal vez esta es una pregunta cerrada, pero me gustaría proponer mi solución para saber lo que piensas al respecto. He usado una metaclase que aplica un decorador al método init
import inspect
class AutoInit(type):
def __new__(meta, classname, supers, classdict):
classdict[''__init__''] = wrapper(classdict[''__init__''])
return type.__new__(meta, classname, supers, classdict)
def wrapper(old_init):
def autoinit(*args):
formals = inspect.getfullargspec(old_init).args
for name, value in zip(formals[1:], args[1:]):
setattr(args[0], name, value)
return autoinit
Tan pronto como getargspec
desuso desde Python 3.5, aquí hay una solución que usa inspect.signature
:
from inspect import signature, Parameter
import functools
def auto_assign(func):
# Signature:
sig = signature(func)
for name, param in sig.parameters.items():
if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
raise RuntimeError(''Unable to auto assign if *args or **kwargs in signature.'')
# Wrapper:
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
for i, (name, param) in enumerate(sig.parameters.items()):
# Skip ''self'' param:
if i == 0: continue
# Search value in args, kwargs or defaults:
if i - 1 < len(args):
val = args[i - 1]
elif name in kwargs:
val = kwargs[name]
else:
val = param.default
setattr(self, name, val)
func(self, *args, **kwargs)
return wrapper
Verifique si funciona:
class Foo(object):
@auto_assign
def __init__(self, a, b, c=None, d=None, e=3):
pass
f = Foo(1, 2, d="a")
assert f.a == 1
assert f.b == 2
assert f.c is None
assert f.d == "a"
assert f.e == 3
print("Ok")
nu11ptr ha creado un pequeño módulo, PyInstanceVars , que incluye esta funcionalidad como decorador de funciones. En el README del módulo se afirma que el " [...] rendimiento ahora es solo un 30-40% peor que la inicialización explícita en CPython ".
Ejemplo de uso, levantado directamente de la documentation del módulo:
>>> from instancevars import *
>>> class TestMe(object):
... @instancevars(omit=[''arg2_''])
... def __init__(self, _arg1, arg2_, arg3=''test''):
... self.arg2 = arg2_ + 1
...
>>> testme = TestMe(1, 2)
>>> testme._arg1
1
>>> testme.arg2_
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: ''TestMe'' object has no attribute ''arg2_''
>>> testme.arg2
3
>>> testme.arg3
''test''