__float__ и __round__ в python 2 и 3

Одно из изменений между python 2 и 3 состоит в том, что последний делегирует x.__round__([n]) round(x, n) операции round(x, n) . В python 2 для моих классов, реализующих __round__ и __float__ , при вызове round(x) x.__float__ .

Как я могу узнать, что round(x) (а не float(x) ) был вызван, чтобы перенаправить вызов, подходящий в python 2, и получить поведение типа python 3.

благодаря

Обновление: я придумал уродливый хак. Я уверен, что:

  • его можно улучшить.
  • это не всегда будет работать.
  • Параметр ndigits не обрабатывается в python 2.
  • его нельзя использовать в производстве.

но все равно было интересно его построить. Спасибо за все разъяснения.

 import dis import sys import inspect import functools #'CALL_FUNCTION', 'CALL_FUNCTION_VAR', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_VAR_KW' HUNGRY = (131, 140, 141, 142) if sys.version < '3': def is_round(frame): """Disassemble a code object.""" co = frame.f_code lasti = frame.f_lasti code = co.co_code i, n = 0, len(code) extended_arg = 0 free = None codes = list() while i < n: c = code[i] op = ord(c) tmp = [op, ] i += 1 if op >= dis.HAVE_ARGUMENT: oparg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg extended_arg = 0 i += 2 if op == dis.EXTENDED_ARG: extended_arg = oparg * long(65536) tmp.append(oparg) if op in dis.hasconst: tmp.append(repr(co.co_consts[oparg])) elif op in dis.hasname: tmp.append(co.co_names[oparg]) elif op in dis.hasjrel: tmp.append(repr(i + oparg)), elif op in dis.haslocal: tmp.append(co.co_varnames[oparg]) elif op in dis.hascompare: tmp.append(dis.cmp_op[oparg]) elif op in dis.hasfree: if free is None: free = co.co_cellvars + co.co_freevars tmp.append(free[oparg]) else: tmp.append(None) else: tmp.append(None) tmp.append(None) codes.append(tmp) if i > lasti: break pending = 1 for (opcode, arguments, param) in reversed(codes): pending -= 1 if opcode in HUNGRY: pending += arguments + 1 if not pending: seen = dict(frame.f_builtins) seen.update(frame.f_globals) seen.update(frame.f_locals) while param in seen: param = seen[param] return param == round def round_check(func): @functools.wraps(func) def wrapped(self): if is_round(inspect.currentframe().f_back): return self.__round__() return func(self) return wrapped else: def round_check(func): return func class X(): @round_check def __float__(self): return 1.0 def __round__(self, ndigits=0): return 2.0 x = X() r = round f = float assert round(x) == 2.0 assert float(x) == 1.0 assert r(x) == 2.0 assert f(x) == 1.0 assert round(float(x)) == 1.0 assert float(round(x)) == 2.0 

Вы всегда можете переопределить round чтобы сначала попробовать __round__ . К сожалению, это не импорт __future__ , поэтому я не думаю, что вы многое можете сделать.

 >>> class X(object): ... def __round__(self, n=0): return 1. ... def __float__(self): return 2. ... >>> x = X() >>> round(x) 2.0 >>> float(x) 2.0 >>> old_round = round >>> def round(x, n=0): ... try: ... return x.__round__(n) ... except AttributeError: ... return old_round(x) ... >>> >>> round(x) 1.0 >>> float(x) 2.0 >>> 

Обратите внимание, что это, по крайней мере, документированное изменение :

Изменена стратегия округления функции round() и тип возвращаемого значения. Точные половинные случаи округлены до ближайшего четного результата, а не от нуля. (Например, round(2.5) теперь возвращает 2 а не 3 ) round(x[, n])() теперь делегирует x.__round__([n]) вместо того, чтобы всегда возвращать float. Он обычно возвращает целое число при вызове с единственным аргументом и значением того же типа, что и при вызове с двумя аргументами.

В Python 2 вы не можете переопределить, что делает round() . Он не делегирует __float__ ; он сначала вызывает float() (который, в свою очередь, делегирует __float__ ), затем __float__ . Поэтому нет смысла знать, __float__ ли __float__ из round() или нет, поскольку он будет делать округление для вас. Вы не можете делегировать его.

Если вы хотите реализовать свое собственное округление в Python 2, вы должны реализовать метод custom_round() который выполняет пользовательское округление, и использовать это вместо round() .

Как я могу узнать, что round (x) (а не float (x)) был вызван, чтобы перенаправить вызов, подходящий в python 2, и получить поведение типа python 3.

Вам не нужно. если round(x) вызывает ваш метод __float__ , он будет округлять возвращаемые значения float с помощью обычной логики для float s. Вам не нужно учитывать это в реализации __float__ ; вы должны возвратить то же самое, независимо от контекста вызова. Все остальное – ответственность вызывающего контекста.

 >>> class hax(object): ... def __float__(self): return 2.6 ... >>> round(hax()) 3.0