What does "evaluated only once" mean for chained comparisons in Python? - python

A friend brought this to my attention, and after I pointed out an oddity, we're both confused.
Python's docs, say, and have said since at least 2.5.1 (haven't checked further back:
Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).
Our confusion lies in the meaning of "y is evaluated only once".
Given a simple but contrived class:
class Magic(object):
def __init__(self, name, val):
self.name = name
self.val = val
def __lt__(self, other):
print("Magic: Called lt on {0}".format(self.name))
if self.val < other.val:
return True
else:
return False
def __le__(self, other):
print("Magic: Called le on {0}".format(self.name))
if self.val <= other.val:
return True
else:
return False
We can produce this result:
>>> x = Magic("x", 0)
>>> y = Magic("y", 5)
>>> z = Magic("z", 10)
>>>
>>> if x < y <= z:
... print ("More magic.")
...
Magic: Called lt on x
Magic: Called le on y
More magic.
>>>
This certainly looks like 'y' is, in a traditional sense "evaluated" twice -- once when x.__lt__(y) is called and performs a comparison on it, and once when y.__le__(z) is called.
So with this in mind, what exactly do the Python docs mean when they say "y is evaluated only once"?

The 'expression' y is evaluated once. I.e., in the following expression, the function is executed only one time.
>>> def five():
... print 'returning 5'
... return 5
...
>>> 1 < five() <= 5
returning 5
True
As opposed to:
>>> 1 < five() and five() <= 5
returning 5
returning 5
True

In the context of y being evaluated, y is meant as an arbitrary expression that could have side-effects. For instance:
class Foo(object):
#property
def complain(self):
print("Evaluated!")
return 2
f = Foo()
print(1 < f.complain < 3) # Prints evaluated once
print(1 < f.complain and f.complain < 3) # Prints evaluated twice

Related

What argument 'isinstance' takes on declaring 'k' object?

class Point:
def __init__(self, x_or_obj = 0, y = 0):
if isinstance(x_or_obj, Point):
self.x = x_or_obj.x
self.y = x_or_obj.y
else:
self.x = x_or_obj
self.y = y
m = Point(1,2)
k = Point(m)
So I have difficulties with understanding why isinstance evaluating True in this code. I see it as int is checking against class, which makes no sense to me.
Looking at this article about isinstance:
The isinstance() function returns True if the specified object is of the specified type, otherwise False.
In m's definition:
m = Point(1,2)
You're passing 1 as the value of x_or_obj. 1 is an integer, not a Point, therefore it evaluates to False.
However, in k's definition:
k = Point(m)
you're passing m as the value of x_or_obj. You earlier defined m as type Point, so isinstance evaluates to True.

How come the order of parameters doesn't matter in this function?

Why it seems that in check() function it doesn't matter if I pass the parameters as check(x,y) or check(y,x)?
I tried shifting x and y to see if it would give me a different output
import random
def guess(a, b):
x = random.randint(a, b)
return x
def check(a, b):
if y**2 == x:
print(x)
print(y)
return True
else:
return False
x = 100
left, right = 0, x
y = guess(left, right)
while not check(x, y):
y = guess(left, right)
print("answer", y)
I think you're making a salad between (a,b) and (x,y).
Your function check uses the "global" (from the outer scope) x and y and not the (x,y) you pass it in the call check(x,y).
Maybe you meant to use a and b instead of x and y inside the definition of check.
You function expects to get 2 arguments as your signature suggests (def check(a, b):) but then nowhere inside it you actually use those arguments. Instead you use x and y. This does not raise an error because Python looks in the outer scopes for those variables and finds them. If you want the order to matter you should change to:
def check(a, b):
if b**2 == a:
print(a)
print(b)
return True
else:
return False

A function with itself as a default argument

Defaults are parsed at definition. So this will not work
def f(x, func = f):
if x<1:
return x
else:
return x + func(x-1)
I did find a way, though. I start with some dummy function
def a(x):
return x
def f(x, func=a):
if x < 1:
return x
else:
return x + func(x-1)
And then issue
f.__defaults__ = (f,)
Obviously awkward. Is there another way or is this bad python? If it's bad, can you explain why? Where will it break things?
In any case, it works:
In [99]: f(10)
Out[99]: 55
In [100]: f(10, f)
Out[100]: 55
In [101]: f(10, lambda x : 2*x)
Out[101]: 28
To elaborate on Andrea Corbellini's suggestion, you can do something like this:
def f(x, func=None):
if func is None:
func = f
if x < 1:
return x
else:
return x + func(x-1)
It is a pretty standard idiom in Python to implement the actual defaults inside of the function and defining the default parameter as None (or some private sentinel object in case None is valid input), due to problems with mutable default values such as list objects.

__float__ and __round__ in python 2 and 3

One of the changes between python 2 and 3 is that the latter delegates to x.__round__([n]) the operation round(x, n). In python 2, for my classes implementing __round__ and __float__, when I call round(x), x.__float__ is called.
How can I know that round(x) (and not float(x)) was called to reroute the call appropriate in python 2 and obtain a python 3 like behaviour.
thanks
Update: I came up with an ugly hack. I am sure that:
it can be improved.
it will not always work.
The ndigits parameter is not handled in python 2.
it should not be used in production.
but it was interesting to build it anyway. Thanks for all the clarifications.
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
You could always redefine round to try __round__ first. Unfortunately this isn't a __future__ import, so I don't think there's much else you can do.
>>> 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
>>>
Note that this is at least a documented change:
The round() function rounding strategy and return type have changed.
Exact halfway cases are now rounded to the nearest even result instead
of away from zero. (For example, round(2.5) now returns 2 rather than
3.) round(x[, n])() now delegates to x.__round__([n]) instead of always returning a float. It generally returns an integer when called
with a single argument and a value of the same type as x when called
with two arguments.
In Python 2, you can not override what round() does. It does not delegate to __float__; it first calls float() (which in turn delegates to __float__), then does the rounding. There is therefore no point in knowing if __float__ is called from round() or not, as it will do the rounding for you. You can't delegate it.
If you want to implement your own custom rounding in Python 2, you should implement a custom_round() method that does the custom rounding, and use that instead of round().
How can I know that round(x) (and not float(x)) was called to reroute the call appropriate in python 2 and obtain a python 3 like behaviour.
You don't need to. if round(x) calls your __float__ method, it will round the returned floats using the normal logic for floats. You do not need to consider it in the __float__ implementation; you should return the same thing regardless of the calling context. Everything else is the calling context's responsibility.
>>> class hax(object):
... def __float__(self): return 2.6
...
>>> round(hax())
3.0

Overriding "+=" in Python? (__iadd__() method)

Is it possible to override += in Python?
Yes, override the __iadd__ method. Example:
def __iadd__(self, other):
self.number += other.number
return self
In addition to what's correctly given in answers above, it is worth explicitly clarifying that when __iadd__ is overriden, the x += y operation does NOT end with the end of __iadd__ method.
Instead, it ends with x = x.__iadd__(y). In other words, Python assigns the return value of your __iadd__ implementation to the object you're "adding to", AFTER the implementation completes.
This means it is possible to mutate the left side of the x += y operation so that the final implicit step fails. Consider what can happen when you are adding to something that's within a list:
>>> x[1] += y # x has two items
Now, if your __iadd__ implementation (a method of an object at x[1]) erroneously or on purpose removes the first item (x[0]) from the beginning of the list, Python will then run your __iadd__ method) & try to assign its return value to x[1]. Which will no longer exist (it will be at x[0]), resulting in an ÌndexError.
Or, if your __iadd__ inserts something to beginning of x of the above example, your object will be at x[2], not x[1], and whatever was earlier at x[0] will now be at x[1]and be assigned the return value of the __iadd__ invocation.
Unless one understands what's happening, resulting bugs can be a nightmare to fix.
In addition to overloading __iadd__ (remember to return self!), you can also fallback on __add__, as x += y will work like x = x + y. (This is one of the pitfalls of the += operator.)
>>> class A(object):
... def __init__(self, x):
... self.x = x
... def __add__(self, other):
... return A(self.x + other.x)
>>> a = A(42)
>>> b = A(3)
>>> print a.x, b.x
42 3
>>> old_id = id(a)
>>> a += b
>>> print a.x
45
>>> print old_id == id(a)
False
It even trips up experts:
class Resource(object):
class_counter = 0
def __init__(self):
self.id = self.class_counter
self.class_counter += 1
x = Resource()
y = Resource()
What values do you expect x.id, y.id, and Resource.class_counter to have?
http://docs.python.org/reference/datamodel.html#emulating-numeric-types
For instance, to execute the statement
x += y, where x is an instance of a
class that has an __iadd__() method,
x.__iadd__(y) is called.

Categories