Опасен ли этот абстрактный базовый класс с "лучшим" __repr__()?
Это меня глючит, что по умолчанию __repr__()
для класса так неинформативно:
>>> class Opaque(object): pass
...
>>> Opaque()
<__main__.Opaque object at 0x7f3ac50eba90>
... так что я думал о том, как его улучшить. После небольшого рассмотрения я придумал этот абстрактный базовый класс, который использует pickle
пРОТОКОЛ __getnewargs__()
метод:
from abc import abstractmethod
class Repro(object):
"""Abstract base class for objects with informative ``repr()`` behaviour."""
@abstractmethod
def __getnewargs__(self):
raise NotImplementedError
def __repr__(self):
signature = ", ".join(repr(arg) for arg in self.__getnewargs__())
return "%s(%s)" % (self.__class__.__name__, signature)
Вот тривиальный пример его использования:
class Transparent(Repro):
"""An example of a ``Repro`` subclass."""
def __init__(self, *args):
self.args = args
def __getnewargs__(self):
return self.args
... и в результате repr()
поведение:
>>> Transparent("an absurd signature", [1, 2, 3], str)
Transparent('an absurd signature', [1, 2, 3], <type 'str'>)
>>>
Теперь я вижу одну причину, по которой Python не делает этого по умолчанию сразу - требует, чтобы каждый класс определял __getnewargs__()
метод будет более обременительным, чем ожидание (но не обязательное), что он определяет __repr__()
метод.
Что я хотел бы знать: насколько это опасно и / или хрупко? Я не могу придумать ничего такого, что могло бы пойти не так, кроме как если бы Repro
Экземпляр сам по себе, вы получите бесконечную рекурсию... но это решаемо за счет того, что код выше будет более уродливым.
Что еще я пропустил?
2 ответа
Одна проблема с этой идеей состоит в том, что могут быть некоторые виды объектов, состояние которых не полностью зависит от аргументов, данных его конструктору. Для тривиального случая рассмотрим класс со случайным состоянием:
import random
def A(object):
def __init__(self):
self.state = random.random()
Для этого класса нет способа правильно реализовать __getnewargs__
и так ваша имплантация __repr__
тоже невозможно. Может случиться так, что класс, подобный приведенному выше, не очень хорошо разработан. Но pickle
может справиться с этим без проблем (я предполагаю, используя __reduce__
метод, унаследованный от object
, но моего рассола-фу недостаточно, чтобы сказать это с уверенностью).
Вот почему хорошо, что __repr__
можно кодировать, чтобы делать все, что вы хотите. Если вы хотите, чтобы внутреннее состояние было видимым, вы можете сделать свой класс __repr__
сделай это. Если объект должен быть непрозрачным, вы можете сделать это тоже. Для класса выше, я бы, вероятно, реализовать __repr__
как это:
def __repr__(self):
return "<A object with state=%f>" % self.state
Если вы в такой вещи, почему бы не получить аргументы автоматически __init__
с помощью декоратора? Тогда вам не нужно обременять пользователя ручной обработкой их, и вы можете прозрачно обрабатывать обычные сигнатуры методов с несколькими аргументами. Вот быстрая версия, которую я придумал:
def deco(f):
def newFunc(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
f(self, *args, **kwargs)
return newFunc
class AutoRepr(object):
def __repr__(self):
args = ', '.join(repr(arg) for arg in self._args)
kwargs = ', '.join('{0}={1}'.format(k, repr(v)) for k, v in self._kwargs.iteritems())
allArgs = ', '.join([args, kwargs]).strip(', ')
return '{0}({1})'.format(self.__class__.__name__, allArgs)
Теперь вы можете определить подклассы AutoRepr как обычно, с обычным __init__
подписи:
class Thingy(AutoRepr):
@deco
def __init__(self, foo, bar=88):
self.foo = foo
self.bar = bar
И __repr__
автоматически работает:
>>> Thingy(1, 2)
Thingy(1, 2)
>>> Thingy(10)
Thingy(10)
>>> Thingy(1, bar=2)
Thingy(1, bar=2)
>>> Thingy(bar=1, foo=2)
Thingy(foo=2, bar=1)
>>> Thingy([1, 2, 3], "Some junk")
Thingy([1, 2, 3], 'Some junk')
Ввод @deco
на ваше __init__
это намного проще, чем писать целое __getnewargs__
, И если вы даже не хотите этого делать, вы можете написать метакласс, который автоматически __init__
метод таким образом.