Specifying constants for functions

There is known trick to optimize a function: def foo(x, y=0, len=len, pack=struct.pack, maxsize=1<<BPF): ... It has a side effect: change function's signature. Would be nice to have a way to set function's local variables at creation time without affecting a signature. Possible syntax (I'm not sure what is better): 1. Similar to "global" and "nonlocal" declarations with optional initializer. def foo(x, y=0): const len const pack=struct.pack, maxsize=1<<BPF ... 2. Same as 1, but using "as" instead of "=". def foo(x, y=0): uses len, struct.pack as pack uses 1<<BPF as maxsize ... 3. Declaration is moved to function header. The keyword "given" is inspired by PEP 3150. def foo(x, y=0) given len=len, pack=struct.pack, maxsize=1<<BPF: ... 4. Declaration is moved out of the function. The advantage is that bound names can be used to evaluate default values for actual parameters (it is useful to implement sentinel default value), and all expression are evaluated in natural order. using len, struct.pack as pack, 1<<BPF as maxsize: def foo(x, y=0): ... 5. The least wordy syntax. No new keyword needed. def foo(x, y=0; len=len, pack=struct.pack, maxsize=1<<BPF): ... All above examples would be roughly equivalent to the following code: def create(len=len, pack=struct.pack, maxsize=1<<BPF): def foo(x, y=0): ... return foo tmp = create() def foo(x, y=0): pass update_wrapper(tmp, foo) foo = tmp del create, tmp This feature is rather ideologically opposite to Victor's approach.

Serhiy, On 2015-10-27 1:45 PM, Serhiy Storchaka wrote:
I see this a lot in all kinds of code. In my experience it doesn't actually speed things up in a measurable way. Is the below code really much slower? def foo(x, y=0): pack=struct.pack maxsize=1<<BPF #CODE If the #CODE is a tight long-running loop - then no, because the loop will probably run much longer than an extra attribute lookup + one extra bit shift on each "foo()" call. And if there is no tight loop - then you won't probably notice those optimizations anyways. I think that adding a "const" statement deserves some discussion, but not from the standpoint of micro-optimizations. Thanks, Yury

IIRC it's an old micro-optimization; this was a common idiom at Zope. But I think it's way overused -- people believe it works so they do it all the time, even for code that's not performance sensitive, just because it's become a habit. (Like "register" in C in the '80s.) On Tue, Oct 27, 2015 at 10:55 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
-- --Guido van Rossum (python.org/~guido)

On Tue, Oct 27, 2015 at 11:18 AM, Guido van Rossum <guido@python.org> wrote:
def foo(x, SENTINEL=object()): ... I don't like mangling function signatures to do this. What I really want is the equivalent of C's static here: def foo(x): static SENTINEL = object() ... This has two important semantics: (1) the scope of the SENTINEL variable is limited to the function foo; (2) it is only initialized once. Hosting the value out of the function into a decorator like @asconstants is fine. --- Bruce Check out my new puzzle book: http://J.mp/ingToConclusions <http://j.mp/ingToConclusions> Get it free here: http://J.mp/ingToConclusionsFree <http://j.mp/ingToConclusionsFree> (available on iOS)

On Oct 27 2015, Yury Selivanov <yselivanov.ml-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:
Because it leaks into the enclosing scope. Best, -Nikolaus -- GPG encrypted emails preferred. Key id: 0xD113FCAC3C4E599F Fingerprint: ED31 791B 2C5C 1613 AF38 8B8A D113 FCAC 3C4E 599F »Time flies like an arrow, fruit flies like a Banana.«

On Tue, Oct 27, 2015 at 04:30:41PM -0400, Yury Selivanov wrote:
It is still in the enclosing scope. Bruce is right that what we really want is something like "static". Lacking that feature, one work-around is "make it a global variable, and give it a leading underscore", but that's just a work-around. Consider: _SENTINEL = object() # Later: def spam(): ... def eggs(): ... def cheese(): ... def aardvark(): ... As the module user, you can see the leading underscore in _SENTINEL and immediately forget all about it. But as the module maintainer, you cannot ignore _SENTINEL. Leading underscore or not, it is still part of the implementation, and maintenance is all about the implementation. Which of the functions uses _SENTINEL? You can assume that *at least* one function uses it, but it may be more. Is it safe to rename it? Change it's value? To the maintainer, _SENTINEL is just another global variable, with all the disadvantages that has. Better to put it inside the function, so the maintainer knows that it is local to the function: def spam(): SENTINEL = object() ... That's not bad, but what if the value is something harder to calculate? def spam(): SENTINEL = next_prime_number(2**512) ... At the moment, Python has an obvious way to calculate a value once only (make it a module-level global), and there is an obvious way to make a value local to a function (put it inside the function body). But there's no obvious way to do both together: "Calculate this thing once only, AND make it local to this scope." The unobvious and ugly way is to put the calculation in the function declaration as a default value: def spam(SENTINEL=next_prime_number(2**512)): ... which complicates the function signature and risks errors if the caller accidentally calls the function with too many arguments. -- Steve

On Tue, Oct 27, 2015 at 4:36 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Python intentionally doesn't have this feature, and the argument you present sounds pretty weak (compared to the compelling argument *against* abusing the default argument syntax -- which also has a maintenance cost, as you have to explain it over and over). -- --Guido van Rossum (python.org/~guido)

On 27.10.15 19:55, Yury Selivanov wrote:
Agree, we slowly gots rid of such microoptimizations. Seems that builtins and globals lookup is much faster in current CPython than was when such microoptimizations were added. Yet one application of this trick is binding globals and module's members in __del__ methods and functions that can be called at shutdown time after module's content cleared. But this is very limited application.

On 27/10/2015 17:55, Yury Selivanov wrote:
From my very naive perspective I'd have thought that the only real difference between the two implementations is that Yury's has the optimization hard coded within the function body, while Serhiy's allows you to override the hard coded defaults at run time. Am I hot, warm, tepid, cold or approaching 0 Kelvin? -- My fellow Pythonistas, ask not what our language can do for you, ask what you can do for our language. Mark Lawrence

On Tue, Oct 27, 2015 at 12:51 PM, Mark Lawrence <breamoreboy@yahoo.co.uk> wrote:
Yury's code has to look up the global value of struct, and then get its pack attribute, every time the function is called. Serhiy's code only does this once, when the function is created, and when it is called the local value is loaded directly from the function defaults, which amounts to a single tuple lookup rather than two sequential dict lookups.

On 27.10.15 19:45, Serhiy Storchaka wrote:
Oh, only after sending my message I had read Scott's message about the asconstants decorator, that does just this optimization [1]. With this decorator above example can be written as: from codetransformer.transformers import asconstants @asconstants(len=len, pack=struct.pack, maxsize=1<<BPF) def foo(x, y=0): ... But it depends on bytecode implementation details. I wondering if it is worth to support this feature by syntax. [1] http://permalink.gmane.org/gmane.comp.python.ideas/36958

Making a decorator is easy: def opt(**kw): def func(f): globals().update(dis.opmap) # Someone's going to kill me for this... code = list(f.__code__.co_code) offs = 0 varnames = list(f.__code__.co_varnames) nlocals = f.__code__.co_nlocals names = list(f.__code__.co_names) globls = f.__globals__ mapping = {} anon_count = 0 for k, v in kw.items(): if k in names: try: i = names.index(k) except ValueError: raise ValueError('variable %r must be a global' % k) from None anon_var = '$__varopt__%d$' % anon_count anon_count += 1 names[i] = anon_var globls[anon_var] = v assert k not in varnames, '%r is both global and local' % k varnames.append(k) nlocals += 1 mapping[i] = nlocals-1 code[:0] = [ LOAD_GLOBAL, i, 0, STORE_FAST, nlocals-1, 0, ] offs += 6 else: raise ValueError('variable %r is not a global' % k) i = offs while i < len(code): if code[i] in dis.hasjabs: code[i+1] += offs elif code[i] in (LOAD_GLOBAL, STORE_GLOBAL) and code[i+1] in mapping: code[i] = LOAD_FAST if code[i] == LOAD_GLOBAL else STORE_FAST code[i+1] = mapping[code[i+1]] i += 3 if code[i] > dis.HAVE_ARGUMENT else 1 rescode = types.CodeType(f.__code__.co_argcount, f.__code__.co_kwonlyargcount, nlocals, f.__code__.co_stacksize, f.__code__.co_flags, bytes(code), f.__code__.co_consts, tuple(names), tuple(varnames), f.__code__.co_filename, f.__code__.co_name, f.__code__.co_firstlineno, f.__code__.co_lnotab, f.__code__.co_freevars, f.__code__.co_cellvars) return types.FunctionType(rescode, globls, f.__name__, f.__defaults__, f.__closure__) return func On Tue, Oct 27, 2015 at 12:45 PM, Serhiy Storchaka <storchaka@gmail.com> wrote:
-- Ryan [ERROR]: Your autotools build scripts are 200 lines longer than your program. Something’s wrong. http://kirbyfan64.github.io/

Hi, 2015-10-28 2:45 GMT+09:00 Serhiy Storchaka <storchaka@gmail.com>:
Yeah, it can show a speedup on a micro-benchmark. Probably not a macro benchmark. As it was said in other answers, this hack is also abused for bad reasons. This hack is mainly used in the stdlib to keep symbols alive during Python shutdown to be able to cleanup properly objects. Just one example from Lib/subprocess.py of Python 3.6: "def __del__(self, _maxsize=sys.maxsize):". I guess that sys.maxsize symbol is removed or set to None during Python shutdown. So depending on the order in which modules are cleared (subprocess,sys or sys,subprocess), the __del__() method may fail or may not fail without the "_maxsize=sys.maxsize" hack. I would appreciate a syntax to not change the function signature, even if this hack is mostly used in destructors and destructors must *not* be called explicitly.
This feature is rather ideologically opposite to Victor's approach.
I disagree, it's not incompatible with my FAT Python project. In some cases, we may still see speedup if you combine copying globals to locals and using FAT Python optimizations. My idea is more to optimize code without having to modify manually the code to optimize it. Using FAT Python, you can implement an optimizer producing code like: --- import builtins def f(data): lengths = [] for item in data: lengths.append(len(item)) return lengths def f_copy_globals(data, _len=len): lengths = [] for item in data: lengths.append("fast: %s" % _len(item)) # add "fast" to ensure that we call the "fast" function return lengths i = f.specialize(f_copy_globals) f.add_dict_guard(i, builtins.__dict__, 'len') f.add_dict_guard(i, globals(), 'len') # test specialized function with "fast" _len local symbol data = ["abc", list(range(5))] print(f(data)) # test with a mocked len() builtin function builtins.len = lambda obj: 10 data = ["abc", list(range(5))] print(f(data)) --- Output: --- ['fast: 3', 'fast: 5'] [10, 10] --- This optimization doesn't respect strictly Python semantic because the len() builtin function can be modified during two loop iterations in a different Python thread. In some cases, it can be worth to optimize the function and doesn't respect stricly the Python semantic. As it was discussed in the FAT Python thread, depending on your use case, you may or may not allow some optimizations. Note: This example doesn't work with my current implementation of FAT Python, because f() and f_copy_globals() don't have the same default values for parameters. You can test with "def f(data, _len=len):". I have to modify FAT Python to support this example. There is also a bug if the specialized function uses a free variable, but not the original function. Again, it should enhance FAT Python to support this case. Victor

Serhiy, On 2015-10-27 1:45 PM, Serhiy Storchaka wrote:
I see this a lot in all kinds of code. In my experience it doesn't actually speed things up in a measurable way. Is the below code really much slower? def foo(x, y=0): pack=struct.pack maxsize=1<<BPF #CODE If the #CODE is a tight long-running loop - then no, because the loop will probably run much longer than an extra attribute lookup + one extra bit shift on each "foo()" call. And if there is no tight loop - then you won't probably notice those optimizations anyways. I think that adding a "const" statement deserves some discussion, but not from the standpoint of micro-optimizations. Thanks, Yury

IIRC it's an old micro-optimization; this was a common idiom at Zope. But I think it's way overused -- people believe it works so they do it all the time, even for code that's not performance sensitive, just because it's become a habit. (Like "register" in C in the '80s.) On Tue, Oct 27, 2015 at 10:55 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
-- --Guido van Rossum (python.org/~guido)

On Tue, Oct 27, 2015 at 11:18 AM, Guido van Rossum <guido@python.org> wrote:
def foo(x, SENTINEL=object()): ... I don't like mangling function signatures to do this. What I really want is the equivalent of C's static here: def foo(x): static SENTINEL = object() ... This has two important semantics: (1) the scope of the SENTINEL variable is limited to the function foo; (2) it is only initialized once. Hosting the value out of the function into a decorator like @asconstants is fine. --- Bruce Check out my new puzzle book: http://J.mp/ingToConclusions <http://j.mp/ingToConclusions> Get it free here: http://J.mp/ingToConclusionsFree <http://j.mp/ingToConclusionsFree> (available on iOS)

On Oct 27 2015, Yury Selivanov <yselivanov.ml-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:
Because it leaks into the enclosing scope. Best, -Nikolaus -- GPG encrypted emails preferred. Key id: 0xD113FCAC3C4E599F Fingerprint: ED31 791B 2C5C 1613 AF38 8B8A D113 FCAC 3C4E 599F »Time flies like an arrow, fruit flies like a Banana.«

On Tue, Oct 27, 2015 at 04:30:41PM -0400, Yury Selivanov wrote:
It is still in the enclosing scope. Bruce is right that what we really want is something like "static". Lacking that feature, one work-around is "make it a global variable, and give it a leading underscore", but that's just a work-around. Consider: _SENTINEL = object() # Later: def spam(): ... def eggs(): ... def cheese(): ... def aardvark(): ... As the module user, you can see the leading underscore in _SENTINEL and immediately forget all about it. But as the module maintainer, you cannot ignore _SENTINEL. Leading underscore or not, it is still part of the implementation, and maintenance is all about the implementation. Which of the functions uses _SENTINEL? You can assume that *at least* one function uses it, but it may be more. Is it safe to rename it? Change it's value? To the maintainer, _SENTINEL is just another global variable, with all the disadvantages that has. Better to put it inside the function, so the maintainer knows that it is local to the function: def spam(): SENTINEL = object() ... That's not bad, but what if the value is something harder to calculate? def spam(): SENTINEL = next_prime_number(2**512) ... At the moment, Python has an obvious way to calculate a value once only (make it a module-level global), and there is an obvious way to make a value local to a function (put it inside the function body). But there's no obvious way to do both together: "Calculate this thing once only, AND make it local to this scope." The unobvious and ugly way is to put the calculation in the function declaration as a default value: def spam(SENTINEL=next_prime_number(2**512)): ... which complicates the function signature and risks errors if the caller accidentally calls the function with too many arguments. -- Steve

On Tue, Oct 27, 2015 at 4:36 PM, Steven D'Aprano <steve@pearwood.info> wrote:
Python intentionally doesn't have this feature, and the argument you present sounds pretty weak (compared to the compelling argument *against* abusing the default argument syntax -- which also has a maintenance cost, as you have to explain it over and over). -- --Guido van Rossum (python.org/~guido)

On 27.10.15 19:55, Yury Selivanov wrote:
Agree, we slowly gots rid of such microoptimizations. Seems that builtins and globals lookup is much faster in current CPython than was when such microoptimizations were added. Yet one application of this trick is binding globals and module's members in __del__ methods and functions that can be called at shutdown time after module's content cleared. But this is very limited application.

On 27/10/2015 17:55, Yury Selivanov wrote:
From my very naive perspective I'd have thought that the only real difference between the two implementations is that Yury's has the optimization hard coded within the function body, while Serhiy's allows you to override the hard coded defaults at run time. Am I hot, warm, tepid, cold or approaching 0 Kelvin? -- My fellow Pythonistas, ask not what our language can do for you, ask what you can do for our language. Mark Lawrence

On Tue, Oct 27, 2015 at 12:51 PM, Mark Lawrence <breamoreboy@yahoo.co.uk> wrote:
Yury's code has to look up the global value of struct, and then get its pack attribute, every time the function is called. Serhiy's code only does this once, when the function is created, and when it is called the local value is loaded directly from the function defaults, which amounts to a single tuple lookup rather than two sequential dict lookups.

On 27.10.15 19:45, Serhiy Storchaka wrote:
Oh, only after sending my message I had read Scott's message about the asconstants decorator, that does just this optimization [1]. With this decorator above example can be written as: from codetransformer.transformers import asconstants @asconstants(len=len, pack=struct.pack, maxsize=1<<BPF) def foo(x, y=0): ... But it depends on bytecode implementation details. I wondering if it is worth to support this feature by syntax. [1] http://permalink.gmane.org/gmane.comp.python.ideas/36958

Making a decorator is easy: def opt(**kw): def func(f): globals().update(dis.opmap) # Someone's going to kill me for this... code = list(f.__code__.co_code) offs = 0 varnames = list(f.__code__.co_varnames) nlocals = f.__code__.co_nlocals names = list(f.__code__.co_names) globls = f.__globals__ mapping = {} anon_count = 0 for k, v in kw.items(): if k in names: try: i = names.index(k) except ValueError: raise ValueError('variable %r must be a global' % k) from None anon_var = '$__varopt__%d$' % anon_count anon_count += 1 names[i] = anon_var globls[anon_var] = v assert k not in varnames, '%r is both global and local' % k varnames.append(k) nlocals += 1 mapping[i] = nlocals-1 code[:0] = [ LOAD_GLOBAL, i, 0, STORE_FAST, nlocals-1, 0, ] offs += 6 else: raise ValueError('variable %r is not a global' % k) i = offs while i < len(code): if code[i] in dis.hasjabs: code[i+1] += offs elif code[i] in (LOAD_GLOBAL, STORE_GLOBAL) and code[i+1] in mapping: code[i] = LOAD_FAST if code[i] == LOAD_GLOBAL else STORE_FAST code[i+1] = mapping[code[i+1]] i += 3 if code[i] > dis.HAVE_ARGUMENT else 1 rescode = types.CodeType(f.__code__.co_argcount, f.__code__.co_kwonlyargcount, nlocals, f.__code__.co_stacksize, f.__code__.co_flags, bytes(code), f.__code__.co_consts, tuple(names), tuple(varnames), f.__code__.co_filename, f.__code__.co_name, f.__code__.co_firstlineno, f.__code__.co_lnotab, f.__code__.co_freevars, f.__code__.co_cellvars) return types.FunctionType(rescode, globls, f.__name__, f.__defaults__, f.__closure__) return func On Tue, Oct 27, 2015 at 12:45 PM, Serhiy Storchaka <storchaka@gmail.com> wrote:
-- Ryan [ERROR]: Your autotools build scripts are 200 lines longer than your program. Something’s wrong. http://kirbyfan64.github.io/

Hi, 2015-10-28 2:45 GMT+09:00 Serhiy Storchaka <storchaka@gmail.com>:
Yeah, it can show a speedup on a micro-benchmark. Probably not a macro benchmark. As it was said in other answers, this hack is also abused for bad reasons. This hack is mainly used in the stdlib to keep symbols alive during Python shutdown to be able to cleanup properly objects. Just one example from Lib/subprocess.py of Python 3.6: "def __del__(self, _maxsize=sys.maxsize):". I guess that sys.maxsize symbol is removed or set to None during Python shutdown. So depending on the order in which modules are cleared (subprocess,sys or sys,subprocess), the __del__() method may fail or may not fail without the "_maxsize=sys.maxsize" hack. I would appreciate a syntax to not change the function signature, even if this hack is mostly used in destructors and destructors must *not* be called explicitly.
This feature is rather ideologically opposite to Victor's approach.
I disagree, it's not incompatible with my FAT Python project. In some cases, we may still see speedup if you combine copying globals to locals and using FAT Python optimizations. My idea is more to optimize code without having to modify manually the code to optimize it. Using FAT Python, you can implement an optimizer producing code like: --- import builtins def f(data): lengths = [] for item in data: lengths.append(len(item)) return lengths def f_copy_globals(data, _len=len): lengths = [] for item in data: lengths.append("fast: %s" % _len(item)) # add "fast" to ensure that we call the "fast" function return lengths i = f.specialize(f_copy_globals) f.add_dict_guard(i, builtins.__dict__, 'len') f.add_dict_guard(i, globals(), 'len') # test specialized function with "fast" _len local symbol data = ["abc", list(range(5))] print(f(data)) # test with a mocked len() builtin function builtins.len = lambda obj: 10 data = ["abc", list(range(5))] print(f(data)) --- Output: --- ['fast: 3', 'fast: 5'] [10, 10] --- This optimization doesn't respect strictly Python semantic because the len() builtin function can be modified during two loop iterations in a different Python thread. In some cases, it can be worth to optimize the function and doesn't respect stricly the Python semantic. As it was discussed in the FAT Python thread, depending on your use case, you may or may not allow some optimizations. Note: This example doesn't work with my current implementation of FAT Python, because f() and f_copy_globals() don't have the same default values for parameters. You can test with "def f(data, _len=len):". I have to modify FAT Python to support this example. There is also a bug if the specialized function uses a free variable, but not the original function. Again, it should enhance FAT Python to support this case. Victor
participants (12)
-
Bruce Leban
-
Guido van Rossum
-
Ian Kelly
-
Mark Lawrence
-
Nikolaus Rath
-
Ryan Gonzalez
-
Serhiy Storchaka
-
Steven D'Aprano
-
Sven R. Kunze
-
Terry Reedy
-
Victor Stinner
-
Yury Selivanov