Fun While Avoiding Lambda (Python)

Readers, meet X. X is a class I wrote in Python as an alternative to using lambda. It has two main features:

  1. It acts as an identity function ( so X(3) == 3, etc. )
  2. When performing operations on it, it returns a new class that acts as a corresponding function.

Let me explain. Doing X+2 will return a new class that whenever called with an argument, will return that argument added with 2. So:

>>> map( X+2, [1, 2, 3] )
[3, 4, 5]

>>> filter( X>0, [5, -3, 2, -1, 0, 13] )
[5, 2, 13]

>>> l = ["oh", "brave", "new", "world"]
>>> sorted(l,key=X[-1])
['world', 'brave', 'oh', 'new']

These operations can be chained:

>>> map(2**(X+1), range(10))
[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]

>>> map( "P" + X[3:]*2 + "!", ["Hello", "Suzzy"] )
['Plolo!', 'Pzyzy!']

Caveats

X has a few limitations.

  • Using X twice in the same expression probably won't work (this can be solved)
  • Since calling X evaluates it, it can't emulate method calls. For that you have to do use call, like: X.upper.call() ('hello') --> 'HELLO'.
  • Not all operations can be "captured". For example, the "in" operator. For that you have to use X.in_( ... ), like: X.in_(range(10)) (5) --> True
  • Not all attributes will be accessible
  • More problems? Likely.

Conclusion

While not innovative nor a complete solution, I believe X can be a useful replacement for some uses of anonymous functions, providing a shorter and simpler syntax which is easier to read and understand.

It is provided here in full, in hope that it will be useful to my readers (Improvements and fixes are welcome):

class _X(object):
	def __init__(self, func):
		self.__func = func

	def __call__(self, arg):
		return self.__func(arg)

	def __getattr__(self, attr):
		return _X(lambda x: getattr(self(x), attr))
	def call(self, *args, **kwargs):
		return _X(lambda x: self(x)(*args,**kwargs))

	# Containers
	def __getitem__(self, other):
		return _X(lambda x: self(x)[other])
	def __getslice__(self, a,b=None,c=None):
		return _X(lambda x: self(x)[a:b:c])
	def in_(self, other):
		return _X(lambda x: self(x) in other)

	# Arith
	def __add__(self, other):
		return _X(lambda x: self(x) + other)
	def __sub__(self, other):
		return _X(lambda x: self(x) - other)
	def __mul__(self, other):
		return _X(lambda x: self(x) * other)
	def __div__(self, other):
		return _X(lambda x: self(x) / other)
	def __floordiv__(self, other):
		return _X(lambda x: self(x) // other)
	def __mod__(self, other):
		return _X(lambda x: self(x) % other)
	def __pow__(self, other):
		return _X(lambda x: self(x) ** other)

	def __radd__(self, other):
		return _X(lambda x: other + self(x))
	def __rsub__(self, other):
		return _X(lambda x: other - self(x))
	def __rmul__(self, other):
		return _X(lambda x: other * self(x))
	def __rdiv__(self, other):
		return _X(lambda x: other / self(x))
	def __rfloordiv__(self, other):
		return _X(lambda x: other // self(x))
	def __rmod__(self, other):
		return _X(lambda x: other % self(x))
	def __rpow__(self, other):
		return _X(lambda x: other ** self(x))

	# bitwise
	def __and__(self, other):
		return _X(lambda x: self(x) & other)
	def __or__(self, other):
		return _X(lambda x: self(x) | other)
	def __xor__(self, other):
		return _X(lambda x: self(x) ^ other)

	def __rand__(self, other):
		return _X(lambda x: other & self(x))
	def __ror__(self, other):
		return _X(lambda x: other | self(x))
	def __rxor__(self, other):
		return _X(lambda x: other ^ self(x))

	def __rshift__(self, other):
		return _X(lambda x: self(x) >> other)
	def __lshift__(self, other):
		return _X(lambda x: self(x) << other)

	# Comparison
	def __lt__(self, other):
		return _X(lambda x: self(x) < other)
	def __le__(self, other):
		return _X(lambda x: self(x) <= other)
	def __eq__(self, other):
		return _X(lambda x: self(x) == other)
	def __ne__(self, other):
		return _X(lambda x: self(x) != other)
	def __ge__(self, other):
		return _X(lambda x: self(x) >= other)
	def __gt__(self, other):
		return _X(lambda x: self(x) > other)

	def __abs__(self):
		return _X(lambda x: abs(self(x)))

X = _X(lambda x:x)

Put it in x.py and import as:
from x import X

Tags: , ,

Categorised in:

13 Comments

  • Noam Mor says:

    It's a pretty fun exercise, I guess. Boost for C++ has exactly that as boost::lambda, and it is horrifying to use, partly because C++ does not have closures. C++ is now (for a certain value of "now") adding new lambda syntax (not a particularly comfortable one), because these things should just be handled as part of the language. There's also a performance penalty for a non-trivial use of X.

    Personally I am quite in favor of the current Python lambda syntax. I dislike replacing simple "lambda employee: employee.salary" expressions with something as obscure as operator.attrgetter("salary"). I find it harder to read and understand, but to each his own I guess.

  • erezsh says:

    There is a performance penalty, that's true. However, a more serious implementation can solve this problem, and also make X-expressions pickle-able at the same time.

    I'm don't think " X.salary " is harder to read or understand than its lambda counterpart. I think the opposite is true.
    Perhaps in some cases where the context is not clear, argument names can add some clarity. But in my experience it's rare. Also, you can do " employeeX = X " 🙂

  • Evan says:

    Looks really neat. A bit impractical in my opinion, however. It becomes just another concept that you have to explain to people if you ever decide to share your code.

  • If you omit __getslice__ then your getitem probably already does that. You could do '5 in X' if you implemented __contains__.

    Otherwise nice. 🙂

  • Alain says:

    Hi,
    very cool indeed.

    I have a question though.
    What is the technical issue that prevents from support the " X in ..." construct. Wouldn't it suffice to override the __contains__ method?

  • erezsh says:

    Alain and Foord:
    __contains__ can only return a bool. No matter what I return in it, it's converted into True or False.

    Evan:
    It's a good point, but I don't think it's much different from any utility library that you use in your code. When I first started using itertools, people at my office would ask me "what does chain do, what does groupby do" etc.

  • Mark says:

    Cool! No support for two argument functions, but still neat!

  • Mark says:

    Ok, wow. I would have expected X+Y though. Can't use the same argument twice this way. Still, wow.

Leave a Reply

Your email address will not be published. Required fields are marked *