Args and kwargs are great features of Python. There is a measurable (though highly variable) cost of them however:
>>> timeit.timeit(lambda: (lambda a, b: None)(1, b=2))
0.16460260000000204
>>> timeit.timeit(lambda: (lambda *a, **kw: None)(1, b=2))
0.21245309999999762
>>> timeit.timeit(lambda: (lambda *a, **kw: None)(1, b=2)) - timeit.timeit(lambda: (lambda a, b: None)(1, b=2))
0.14699769999992895
Constructing that dict and tuple doesn't happen for free:
>>> timeit.timeit(lambda: ((1,), {'b': 2})) - timeit.timeit(lambda: None)
0.16881599999999253
Specifically, it takes about 1/5,000,000th of a second.
Kind of like "hey guys, check it out you can just duct tape down the dead-man's switch on this power tool and use it one handed". In Python.
Friday, September 14, 2018
Friday, August 10, 2018
Tuesday, June 5, 2018
when no-ops attack VII: assignment's revenge
Let's define a very simple class:
>>> class F(object):
... @staticmethod
... def f(): return "I'm such a simple function, nothing could go wrong"
...
>>> F.f()
"I'm such a simple function, nothing could go wrong"
Now, let's do a trivial no-op to this class:
>>> F.f = F.f
Surely nothing changed, right?
>>> F.f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method f() must be called with F instance as first argument (got nothing instead)
What happened? staticmethod uses the descriptor protocol in order to return something other than itself when accessed as an attribute. The assignment above is not a no-op, because it is not setting the value back to what it already was, but to what was returned by __get__ of the staticmethod object.
>>> class F(object):
... @staticmethod
... def f(): return "I'm not what I seem"
...
>>> F.f
<function f at 0x7f05eda596e0>
>>> F.__dict__['f']
<staticmethod object at 0x7f05eda5ce50>
Version note -- Python3 doesn't raise an exception, although the type still changes from staticmethod to function.
>>> class F:
... @staticmethod
... def f(): return "I'm protected by python3 wizardry"
...
>>> F.f()
"I'm protected by python3 wizardry"
>>> F.__dict__['f']
<staticmethod object at 0x7fd087b739b0>
>>> F.f = F.f
>>> F.__dict__['f']
<function F.f at 0x7fd087b5cae8>
>>> F.f()
"I'm protected by python3 wizardry"
>>> class F(object):
... @staticmethod
... def f(): return "I'm such a simple function, nothing could go wrong"
...
>>> F.f()
"I'm such a simple function, nothing could go wrong"
Now, let's do a trivial no-op to this class:
>>> F.f = F.f
Surely nothing changed, right?
>>> F.f()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method f() must be called with F instance as first argument (got nothing instead)
What happened? staticmethod uses the descriptor protocol in order to return something other than itself when accessed as an attribute. The assignment above is not a no-op, because it is not setting the value back to what it already was, but to what was returned by __get__ of the staticmethod object.
>>> class F(object):
... @staticmethod
... def f(): return "I'm not what I seem"
...
>>> F.f
<function f at 0x7f05eda596e0>
>>> F.__dict__['f']
<staticmethod object at 0x7f05eda5ce50>
Version note -- Python3 doesn't raise an exception, although the type still changes from staticmethod to function.
>>> class F:
... @staticmethod
... def f(): return "I'm protected by python3 wizardry"
...
>>> F.f()
"I'm protected by python3 wizardry"
>>> F.__dict__['f']
<staticmethod object at 0x7fd087b739b0>
>>> F.f = F.f
>>> F.__dict__['f']
<function F.f at 0x7fd087b5cae8>
>>> F.f()
"I'm protected by python3 wizardry"
Thursday, May 31, 2018
(i)t(er)able for one
When you expect that a sequence will only have one item, and are only interested in the first it is common to grab the zeroth element. This will fail if the sequence is unexpectedly empty, but you might unintentionally silently throw away extra elements:
>>> a = 'a'[0]
>>> a = ''[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
>>> a = 'ab'[0] # oops, silently dropped b
An alternative idiom is to use sequence unpacking with a single item. This way neither unexpected condition will silently pass.
>>> a, = 'a'
>>> a, = ''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: need more than 0 values to unpack
>>> a, = 'ab'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack
>>> a = 'a'[0]
>>> a = ''[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
>>> a = 'ab'[0] # oops, silently dropped b
An alternative idiom is to use sequence unpacking with a single item. This way neither unexpected condition will silently pass.
>>> a, = 'a'
>>> a, = ''
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: need more than 0 values to unpack
>>> a, = 'ab'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack
Saturday, May 12, 2018
Captain, the python grammar can't take anymore!
The expressions are going to tear themselves to pieces!
>>> 'a' .strip ( ) [ 0 ]
'a'
>>> 'a' .strip ( ) [ 0 ]
'a'
Friday, April 20, 2018
DISappearing and
Python has a very rich set of operators that can be overloaded. From __get__ to __getattr__, __repr__ to __format__, and __complex__ to __iadd__ you can modify almost every behavior of your type. Conspicuously absent however, are the boolean operators.
This is why Django ORM and SQLAlchemy use bitwise & and | to represent SQL and / or.
Let's take a closer look at how the Python compiler treats these operators:
>>> import dis
>>> dis.dis(lambda: a & b)
1 0 LOAD_GLOBAL 0 (a)
3 LOAD_GLOBAL 1 (b)
6 BINARY_AND
7 RETURN_VALUE
>>> dis.dis(lambda: a and b)
1 0 LOAD_GLOBAL 0 (a)
3 JUMP_IF_FALSE_OR_POP 9
6 LOAD_GLOBAL 1 (b)
>> 9 RETURN_VALUE
Not only can you not override the and operator, the Python VM doesn't even have an opcode for it.
In return, Python gives you the semantics that a or b returns not True or False, but either a or b (or False if neither is truthy).
This is why Django ORM and SQLAlchemy use bitwise & and | to represent SQL and / or.
Let's take a closer look at how the Python compiler treats these operators:
>>> import dis
>>> dis.dis(lambda: a & b)
1 0 LOAD_GLOBAL 0 (a)
3 LOAD_GLOBAL 1 (b)
6 BINARY_AND
7 RETURN_VALUE
>>> dis.dis(lambda: a and b)
1 0 LOAD_GLOBAL 0 (a)
3 JUMP_IF_FALSE_OR_POP 9
6 LOAD_GLOBAL 1 (b)
>> 9 RETURN_VALUE
Not only can you not override the and operator, the Python VM doesn't even have an opcode for it.
In return, Python gives you the semantics that a or b returns not True or False, but either a or b (or False if neither is truthy).
Thursday, March 22, 2018
The Zen of Empty Lists
"There should be one-- and preferably only one --obvious way to do it". One of the many philosophies that has earned Python its acclaim.
But while the Zen of Python limits on the number of obvious ways, the Zen of Python says nothing about the boundless freedom of unobvious ways.
Let's empty a list named bucket.
The most obvious way is to simply not. 99 times out of 100, you want to assign a new empty list rather than mutating the old.
But while the Zen of Python limits on the number of obvious ways, the Zen of Python says nothing about the boundless freedom of unobvious ways.
Let's empty a list named bucket.
The most obvious way is to simply not. 99 times out of 100, you want to assign a new empty list rather than mutating the old.
bucket = []But let's say you really wanted to empty it, well the clearest way is clear:
bucket.clear()But even the docs say this is equivalent to:
del bucket[:]I guess that's crossing the obvious line. Of course, it may be more obvious than Norvig's "dumbell" operator:
bucket[:]=[]Actually the slice assignment can take any iterable, so our list can lift plates of many shapes:
bucket[:]={}If you don't want your list getting ripped and/or cut, maybe keep it warm with Norvig's ski hat:
bucket *=0The ski hat is of particular interest because it's using a very obvious list feature, much more commonly used than list.clear(). Nobody would bat an eye at:
bucket = [0, 1, 2, 3] * 2If you multiply any list by 0, you make a new list of length 0. Bizarrely, this is actually true of any multiplier less than 0, too.
# bucket = [0, 1, 2, 3, 0, 1, 2, 3]
bucket *= -3Safe to say we are deep in the territory of the unobvious. Is there a syntax we might meditate on to take us further?
# bucket = []
Sunday, January 21, 2018
None on the left
A natural default,
In Python 2:
None
is probably the most commonly assigned value in Python. But what happens if you move it to the left side of that equation?In Python 2:
>>> None = 2
File "<stdin>", line 1
SyntaxError: cannot assign to None
This is similar to what happens when you assign to a literal:>>> 1 = 2
File "<stdin>", line 1
SyntaxError: can't assign to literal
In Python 3 this walk on the wild side will get you a slightly different error:>>> None = 1
File "<stdin>", line 1
SyntaxError: can't assign to keyword
None
has graduated from useful snowflake to full-blown keyword!
Subscribe to:
Posts (Atom)