Python: classe nouvelle coerce de style
Question
Je veux ce code "fonctionne":
def main():
c = Castable()
print c/3
print 2-c
print c%7
print c**2
print "%s" % c
print "%i" % c
print "%f" % c
Bien sûr, l'out moyen facile est de int(c)/3
d'écriture, mais je voudrais permettre une syntaxe plus simple de Perl-ish pour un mini-langage configuration.
Il est à noter que si j'utilise une classe « ancienne » (ne pas hériter de l'objet) Je peux le faire tout simplement en définissant une méthode __coerce__
, mais les classes de style ancien sont dépréciées et seront supprimés dans python3.
Quand je fais la même chose avec une nouvelle classe de style, je reçois cette erreur:
TypeError: unsupported operand type(s) for /: 'Castable' and 'int'
Je crois que c'est par la conception, mais comment puis-je simuler l'ancien style de comportement __coerce__
avec une nouvelle classe de style? Vous pouvez trouver ma solution actuelle ci-dessous, mais il est assez laid et de longue haleine.
Ceci est la documentation pertinente: (je pense)
Points Bonus:
print pow(c, 2, 100)
La solution 2
Cela fonctionne, et est moins brut après plusieurs améliorations (accessoires pour @jchl), mais semble encore comme il devrait être unecessary, d'autant plus que vous obtenez ce gratuitement avec des classes « style ancien ».
Je suis toujours à la recherche d'une meilleure réponse. S'il n'y a pas de méthode meilleure, cela me semble comme une régression dans le langage Python.
def ops_list():
"calculate the list of overloadable operators"
#<type 'object'> has functions but no operations
not_ops = dir(object)
#calculate the list of operation names
ops = set()
for mytype in (int, float, str):
for op in dir(mytype):
if op.endswith("__") and op not in not_ops:
ops.add(op)
return sorted(ops)
class MetaCastable(type):
__ops = ops_list()
def __new__(mcs, name, bases, dict):
#pass any undefined ops to self.__op__
def add_op(op):
if op in dict:
return
fn = lambda self, *args: self.__op__(op, args)
fn.__name__ = op
dict[op] = fn
for op in mcs.__ops:
add_op( op )
return type.__new__(mcs, name, bases, dict)
class Castable(object):
__metaclass__ = MetaCastable
def __str__(self):
print "str!"
return "<Castable>"
def __int__(self):
print "int!"
return 42
def __float__(self):
print "float!"
return 2.718281828459045
def __op__(self, op, args):
try:
other = args[0]
except IndexError:
other = None
print "%s %s %s" % (self, op, other)
self, other = coerce(self, other)
return getattr(self, op)(*args)
def __coerce__(self, other):
print "coercing like %r!" % other
if other is None: other = 0.0
return (type(other)(self), other)
Autres conseils
Vous devez définir __div__
si vous voulez c/3
au travail. Python ne convertit pas votre objet à un premier numéro pour vous.
class MetaCastable(type):
__binary_ops = (
'add', 'sub', 'mul', 'floordiv', 'mod', 'divmod', 'pow', 'lshift',
'rshift', 'and', 'xor', 'or', 'div', 'truediv',
)
__unary_ops = ( 'neg', 'pos', 'abs', 'invert', )
def __new__(mcs, name, bases, dict):
def make_binary_op(op):
fn = lambda self, other: self.__op__(op, other)
fn.__name__ = op
return fn
for opname in mcs.__binary_ops:
for op in ( '__%s__', '__r%s__' ):
op %= opname
if op in dict:
continue
dict[op] = make_binary_op(op)
def make_unary_op(op):
fn = lambda self: self.__op__(op, None)
fn.__name__ = op
return fn
for opname in mcs.__unary_ops:
op = '__%s__' % opname
if op in dict:
continue
dict[op] = make_unary_op(op)
return type.__new__(mcs, name, bases, dict)
class Castable(object):
__metaclass__ = MetaCastable
def __str__(self):
print "str!"
return "<Castable>"
def __int__(self):
print "int!"
return 42
def __float__(self):
print "float!"
return 2.718281828459045
def __op__(self, op, other):
if other is None:
print "%s(%s)" % (op, self)
self, other = coerce(self, 0.0)
return getattr(self, op)()
else:
print "%s %s %s" % (self, op, other)
self, other = coerce(self, other)
return getattr(self, op)(other)
def __coerce__(self, other):
print "coercing like %r!" % other
return (type(other)(self), other)
class Castable(object):
def __div__(self, other):
return 42 / other
Les nouvelles classes de style fonctionnent plus rapide et plus précis que les classes de style ancien. Donc pas __getattr__
plus cher, __getattribute__
, appels __coerce__
pour des raisons économiques et dans un ordre discutable.
L'ancien style __coerce__
avait aussi le problème, qu'il a été appelé, même si vous avez déjà surchargé une méthode d'opérateur pour un but spécial. Elle exige coulée à l'égalité des types communs, et se limite à certaines opérations binaires. Pensez à toutes les autres méthodes et propriétés d'un int / flotteur / string - et environ pow (). En raison de toutes ces limitations coerce
manque dans PY3. Les exemples de question visent à la virtualisation assez large.
Avec de nouvelles classes de style est juste sur une boucle pour fournir de nombreuses méthodes avec peu de code « similaires », ou l'itinéraire les appels vers un gestionnaire virtuel, puis son rapide et définie avec précision et dérivables de manière correcte et à grains fins. Cest pas une « régression dans le langage Python ».
Cependant, je ne serais pas employer une classe méta comme indiqué dans d'autres réponses juste pour une telle boucle ou pour fournir une simple classe de base genre de comportement. Ce serait cassant une noix avec un marteau de forgeron.
Voici un exemple d'aide pour la virtualisation d'une « variante »:
def Virtual(*methods):
"""Build a (new style) base or mixin class, which routes method or
operator calls to one __virtualmeth__ and attribute lookups to
__virtualget__ and __virtualset__ optionally.
*methods (strings, classes): Providing method names to be routed
"""
class VirtualBase(object):
def __virtualmeth__(self, methname, *args, **kw):
raise NotImplementedError
def _mkmeth(methname, thing):
if not callable(thing):
prop = property(lambda self:self.__virtualget__(methname),
lambda self, v:self.__virtualset__(methname, v))
return prop
def _meth(self, *args, **kw):
return self.__virtualmeth__(methname, *args, **kw)
_meth.__name__ = methname
return _meth
for m in methods:
for name, thing in (isinstance(m, str) and
{m:lambda:None} or m.__dict__).items():
if name not in ('__new__', '__init__', '__setattr__', ##'__cmp__',
'__getattribute__', '__doc__', ): ##'__getattr__',
setattr(VirtualBase, name, _mkmeth(name, thing))
return VirtualBase
Et voici un exemple de cas d'utilisation: une anaphore! (AP2 et AP3):
import operator
class Anaphor(Virtual(int, float, str)):
"""remember a sub-expression comfortably:
A = Anaphor() # at least per thread / TLS
if re.search(...) >> A:
print(A.groups(), +A)
if A(x % 7) != 0:
print(A, 1 + A, A < 3.0, A.real, '%.2f' % A, +A)
"""
value = 0
def __virtualmeth__(self, methname, *args, **kw):
try: r = getattr(self.value, methname)(*args, **kw)
except AttributeError:
return getattr(operator, methname)(self.value, *args, **kw)
if r is NotImplemented: # simple type -> coerce
try: tcommon = type(self.value + args[0]) # PY2 coerce
except: return NotImplemented
return getattr(tcommon(self.value), methname)(*args, **kw)
return r
def __call__(self, value):
self.value = value
return value
__lshift__ = __rrshift__ = __call__ # A << x; x >> A
def __pos__(self): # real = +A
return self.value
def __getattr__(self, name):
return getattr(self.value, name)
def __repr__(self):
return '<Anaphor:%r>' % self.value
il gère également de manière transparente le 3-arg opertor pow()
:-):
>>> A = Anaphor()
>>> x = 1
>>> if x + 11 >> A:
... print repr(A), A, +A, 'y' * A, 3.0 < A, pow(A, 2, 100)
...
<Anaphor:12> 12 12 yyyyyyyyyyyy True 44