condensar pyqtproperties

Estoy escribiendo un script en Python y tengo un pequeño problema:

class LightDMUser(QObject): def __init__(self, user): super(LightDMUser, self).__init__() self.user = user @pyqtProperty(QVariant) def background(self): return self.user.get_background() @pyqtProperty(QVariant) def display_name(self): return self.user.get_display_name() @pyqtProperty(QVariant) def has_messages(self): return self.user.get_has_messages() @pyqtProperty(QVariant) def home_directory(self): return self.user.get_home_directory() @pyqtProperty(QVariant) def image(self): return self.user.get_image() @pyqtProperty(QVariant) def language(self): return self.user.get_language() @pyqtProperty(QVariant) def layout(self): return self.user.get_layout() @pyqtProperty(QVariant) def layouts(self): return self.user.get_layouts() @pyqtProperty(QVariant) def logged_in(self): return self.user.get_logged_in() @pyqtProperty(QVariant) def name(self): return self.user.get_name() @pyqtProperty(QVariant) def real_name(self): return self.user.get_real_name() @pyqtProperty(QVariant) def session(self): return self.user.get_session() 

Como puede ver, este código es horriblemente redundante. Intenté condensarlo así:

 class LightDMUser(QObject): attributes = ['background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session'] def __init__(self, user): super(LightDMUser, self).__init__() self.user = user for attribute in self.attributes: setattr(self, attribute, pyqtProperty(QVariant, getattr(self.user, 'get_' + attribute))) 

PyQt4, sin embargo, espera que los métodos de clase estén presentes para la clase en sí, no una instancia. Mover el código setattr fuera del bloque __init__ tampoco funcionó porque self no estaba definido para la clase, así que realmente no sé qué hacer.

¿Alguien puede ver una manera de condensar este código?

Hay varias formas de hacerlo: decorador de clase, metaclase, Mixin.

Función auxiliar común:

 def set_pyqtproperties(klass, properties, proxy='user'): def make_prop(prop): def property_(self): return getattr(getattr(self, proxy), 'get_' + prop) property_.__name__ = prop return property_ if isinstance(properties, basestring): properties = properties.split() for prop in properties: setattr(klass, prop, pyqtProperty(QVariant, make_prop(prop))) 

Decorador de clase

 def set_properties(properties): def decorator(klass): set_pyqtproperties(klass, properties) return klass return decorator 

Uso

 @set_properties("display background") class LightDMUser(QObject): pass 

Si no hay soporte para los decoradores de clase, entonces puedes probar:

 class LightDMUser(QObject): pass LightDMUser = set_properties("display background")(LightDMUser) 

Metaclase

 def set_properties_meta(properties): def meta(name, bases, attrs): cls = type(name, bases, attrs) set_pyqtproperties(cls, properties) return cls return meta 

Uso

 class LightDMUser(QObject): __metaclass__ = set_properties_meta("display background") 

Nota: podría reutilizar la misma metaclase si establece la lista de propiedades como un atributo de clase:

 def MetaClass(name, bases, attrs): cls = type(name, bases, attrs) set_pyqtproperties(cls, attrs.get('properties', '')) return cls class LightDMUser(QObject): properties = "display background" __metaclass__ = MetaClass 

También puede manipular attrs directamente: attrs[name] = value antes de llamar a type() lugar de setattr(cls, name, value) .

Lo anterior asume que QObject.__class__ is type .

Mezclar

 def properties_mixin(classname, properties): #note: create a new class by whatever means necessary # eg, even using exec() as namedtuple does # http://hg.python.org/cpython/file/3.2/Lib/collections.py#l235 # reuse class decorator here return set_properties(properties)(type(classname, (), {})) 

Uso

 PropertiesMixin = properties_mixin('PropertiesMixin', 'display background') class LightDMUser(PropertiesMixin, QObject): pass 

No he probado nada de eso. El código está aquí para mostrar la cantidad y el tipo de código que podría requerir para implementar la función.

Puede adjuntar estos métodos desde fuera de la definición de clase:

 class LightDMUser(QObject): def __init__(self, user): super(LightDMUser, self).__init__() self.user = user 

La forma más sencilla es crear un cierre para cada propiedad, anular su __name__ (solo en el caso de que @pyqtProperty necesite) y vincularlo a la clase:

 for attribute in [ 'background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session' ]: def delegating(self): return getattr(self.user, 'get_' + attribute)() delegating.__name__ = attribute delegating = pyqtProperty(QVariant)(delegating) setattr(LightDMUser, attribute, delegating) 

Estoy bastante seguro de que esto puede funcionar si mueve su bucle fuera de la clase y crea un cierre para mantener cada uno de los nombres de atributo:

 class LightDMUser(QObject): attributes = ['background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session'] def __init__(self, user): super(LightDMUser, self).__init__() self.user = user for attribute in LightDMUser.attributes: closure = lambda self, attribute=attribute : getattr(self.user, 'get_' + attribute)() setattr(LightDMUser, attribute, pyqtProperty(QVariant, closure)) 

No he probado esto con las clases reales basadas en QT con las que estás lidiando, pero una versión más simple que usa instancias de property Python normales funcionó perfectamente. Tampoco estoy seguro de que sea una buena idea, ya que sería bastante difícil averiguar qué está pasando si no está familiarizado con ella.

No estoy convencido de que me guste esto, pero es una opción posible, no es demasiado difícil de entender y elimina la necesidad de getattr los de getattr … Lo siguiente se puede usar un poco como una macro, pero es posible que necesite ajustes. .. (p. ej., tomar funciones de una definición de clase que comience con get, o de un objeto existente, etc.) También se podría agregar una repetición allí para describir que es una clase compatible para interactuar con las propiedades de los objetos de usuario o lo que sea. .)

 def get_properties(name, funcs): get_text = """ class {name}(QObject): """.format(name=name) for func in funcs: get_text += ( "\n\t@pyqtProperty(QVariant)\n" "\tdef {func}(self): return self.user.get_{func}()\n" ).format(func=func) print get_text # this should be exec... >>> get_properties('UserProperties', ['display', 'background']) class UserProperties(QObject): @pyqtProperty(QVariant) def display(self): return self.user.get_display() @pyqtProperty(QVariant) def background(self): return self.user.get_background() 

Cuando eso se ejecuta, puede escribir su clase principal como:

 class LightDMUser(QObject, UserProperties): def __init__(self, user): super(LightDMUser, self).__init__() self.user = user 

Probé la solución a continuación, para Python 3. Utiliza la palabra clave metaclase

 # A bit of scaffolding def pyqtProperty(cls, method): return method class QObject: pass class QVariant: pass class User: def __init__(self, name="No Name"): self.name = name def get_background(self): return self.name def get_display_name(self): return self.name def get_has_messages(self): return self.name def get_home_directory(self): return self.name def get_image(self): return self.name def get_language(self): return self.name def get_layout(self): return self.name def get_layouts(self): return self.name def get_logged_in(self): return self.name def get_name(self): return self.name def get_real_name(self): return self.name def get_session(self): return self.name # The Meta Class class MetaLightDMUser(type): @classmethod def __prepare__(cls, name, baseClasses): classdict = {} for attribute in ['background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session']: classdict[attribute] = eval("lambda self: pyqtProperty(QVariant, getattr(self.user, 'get_" + attribute +"'))()") return classdict def __new__(cls, name, baseClasses, classdict): return type.__new__(cls, name, baseClasses, classdict) # The class itself class LightDMUser(QObject, metaclass = MetaLightDMUser): def __init__(self, user): super(LightDMUser, self).__init__() self.user = user 

Alternativamente podría haber creado las entradas de classdict como esta

classdict[attribute] = lambda self, attr=attribute: pyqtProperty(QVariant, getattr(self.user, 'get_' + attr))()

Pero eso presenta un argumento attr. Con eval() hard-core este argumento

También podríamos haber utilizado functools.partial :

classdict[attribute] = functools.partial(lambda self, attr: pyqtProperty(QVariant, getattr(self.user, 'get_' + attr))(), attr=attribute)

pero entonces la llamada debe ser u.method(u) . No puede ser u.method()

La llamada LightDMUser.method(u) funciona con las 3 implementaciones

Saludos

Es difícil reducir la plantilla para pyqtProperty usando una metaclase o un decorador de clase, pero esto es algo que tenemos que trabajar aquí que nos ayudará como punto de partida. El inconveniente, supongo, es que ya no puedes usar la syntax de @decorator, pero parece que enrollar esto en una sola línea de código es más deseable en esta situación.

Puede configurarlo para que llame a su objeto de usuario en lugar de solo a sí mismo, o podría implementar un comportamiento personalizado de getattr para LightDMUser que llamará a self.user automáticamente.

 from PyQt4.QtCore import pyqtProperty from PyQt4.QtGui import QWidget, QColor from functools import partial def pyqtPropertyInit(name, default): def _getattrDefault(default, self, attrName): try: value = getattr(self, attrName) except AttributeError: setattr(self, attrName, default) return default return value ga = partial(_getattrDefault, default) return pyqtProperty( default.__class__, fget=(lambda s: ga(s, name)), fset=(lambda s, v: setattr(s, name, v)), ) class TestClass(QWidget): def __init__(self, *args, **kwargs): super(TestClass, self).__init__(*args, **kwargs) stdoutColor = pyqtPropertyInit('_stdoutColor', QColor(0, 0, 255)) pyForegroundColor = pyqtPropertyInit('_pyForegroundColor', QColor(0, 0, 255))