Recipe 4.19. Assigning and Testing with One Statement
Credit: Alex Martelli, Martin Miller
Problem
You are transliterating C or Perl code
to Python, and to keep close to the original's
structure, you'd like an
expression's result to be both assigned and tested
(as in if((x=foo( )) or while((x=foo(
)) in such other languages).
Solution
In Python, you can't code if x=foo(): . . . . Assignment is a statement, so it cannot fit into
an expression, and you can only use expressions as conditions of
if and while statements. This
isn't a problem, it just means you have to structure
your code Pythonically! For example, to process a file object
f line by line, instead of the following
C-like (and syntactically incorrect, in Python) approach:
while (line=f.readline( )) != '':you can code a highly Pythonic (readable, clean, fast) approach:
process(line)
for line in f:But sometimes, you're transliterating from C, Perl,
process(line)
or another language, and you'd like your
transliteration to be structurally close to the original. One simple
utility class makes it easy:
class DataHolder(object):With the help of the DataHolder class and its
def _ _init_ _(self, value=None):
self.value = value
def set(self, value):
self.value = value
return value
def get(self):
return self.value
# optional and strongly discouraged, but nevertheless handy at times:
import _ _builtin_ _
_ _builtin_ _.DataHolder = DataHolder
_ _builtin_ _.data = data = DataHolder( )
instance data, you can keep your C-like code
structure intact in transliteration:
while data.set(file.readline( )) != '':
process(data.get( ))
Discussion
In Python, assignment is a statement, not an expression. Thus, you
cannot assign the result that you are also testing, for example, in
the condition of an if, elif,
or while statement. This is usually fine: just
structure your code to avoid the need to assign while testing (in
fact, your code will often become clearer as a result). In
particular, whenever you feel the need to assign-and-test within the
condition of a while loop, that's
a good hint that your loop's structure probably
wants to be refactored into a generator (or other iterator). Once you
have refactored in this way, your loops become plain and simple
for statements. The example given in the recipe,
looping over each line read from a text file, is one where the
refactoring has already been done on your behalf by Python itself,
since a file object is an iterator whose items are
the file's lines.However, sometimes you may be writing Python code that is the
transliteration of code originally written in C, Perl, or some other
language that supports assignment-as-expression. Such
transliterations often occur in the first Python version of an
algorithm for which a reference implementation is supplied, an
algorithm taken from a book, and so on. In such cases,
it's often preferable to have the structure of your
initial transliteration be close to that of the code
you're transcribing. You can refactor later and make
your code more Pythonicclearer, faster, and so on. But first,
you want to get working code as soon as possible, and specifically
you want code that is easy to check for compliance to the original it
has been transliterated from. Fortunately, Python offers enough power
to make it quite easy for you to satisfy this requirement.Python doesn't let us redefine the meaning of
assignment, but we can have a method (or function) that saves its
argument somewhere and also returns that
argument so it can be tested. That somewhere is
most naturally an attribute of an object, so a method is a more
natural choice than a function. Of course, we could just retrieve the
attribute directly (i.e., the get method is
redundant), but it looks nicer to me to have symmetry between
data.set and data.get.data.set(whatever) can be seen as little more than
syntactic sugar around data.value=whatever, with
the added value of being acceptable as an expression. Therefore,
it's the one obviously right way to satisfy the
requirement for a reasonably faithful transliteration. The only
difference between the resulting Python code and the original (say) C
or Perl code, is at the syntactic sugar levelthe overall
structure is the same, and that's the key issue.Importing _ _builtin_ _ and assigning to its
attributes is a trick that basically defines a new built-in object at
runtime. You can use that trick in your
application's start-up code, and then all other
modules will automatically be able to access your new built-ins
without having to do an import.
It's not good Python practice,
though; on the contrary, it's pushing the boundaries
of Pythonic good taste, since the readers of all those other modules
should not have to know about the strange side effects performed in
your application's startup code. But since this
recipe is meant to offer a quick-and-dirty approach for a first
transliteration that will soon be refactored to make it better, it
may be acceptable in this specific context to cut more corners than
one would in production-level code.On the other hand, one trick you should definitely
not use is the following abuse of a currently
existing wart in list comprehensions:
while [line for line in [f.readline( )] if line!='']:This trick currently works, since both Python 2.3 and 2.4 still
process(line)
"leak" the list comprehension
control variable (here, line) into the
surrounding scope. However, besides being obscure and unreadable,
this trick is specifically deprecated: list comprehension control
variable leakage will be fixed in some future
version of Python, and this trick will then stop working at all.
See Also
The Tutorial section on classes; the
documentation for the _ _builtin_ _ module in the
Library Reference and Python in a
Nutshell; Language Reference and
Python in a Nutshell documentation on list
comprehensions.