There are several well known python code style rules, which are considered default and to which I try to adhere:
Wrap lines so that they don’t exceed 79 characters.
Keep indents 4 spaces long.
Another common programming advice is
Avoid global variables
In other words, one should always use functions that accept all their variables as parameters and avoid Pascal-like procedures that read directly from higher scope.
However, in some cases, one should definitely break some of these rules. For example, if functions with long parameters lists are involved. There are two separate issues with them:
First, in heavily indented blocks there is too little room left.
def function(variable1, variable2, variable3, variable4, variable5,\
variable6, variable7, variable8, variable9):
return variable1 + variable2 + variable3 + variable4 + variable5 +\
variable6 + variable7 + variable8 + variable9
def...
for variable1...
if variable 2...
while variable3...
if variable4...
for variable5...
...
variable10 =\
function(\
variable1,\
variable2,\
variable3,\
variable4,\
variable5,\
variable6,\
variable7,\
variable8,\
variable9)
...
Here a procedure as a closure, though it is considered a bad practice, can become helpful:
def...
def procedure():
variable10 = variable1 + variable2 + variable3 + variable4 +\
variable5 + variable6 + variable7 + variable8 + variable9
for variable1...
if variable 2...
while variable3...
if variable4...
for variable5...
...
procedure()
...
Another issue (which is actually python-specific) is performance. Copying of function parameters can become quite costly if there is a lot of them:
import time
var1 = 1
var2 = 2
var3 = 3
var4 = 4
var5 = 5
var6 = 6
def function(var1, var2, var3, var4, var5, var6):
pass
def procedure():
pass
starttime = time.time()
for i in range(10000000):
function(var1, var2, var3, var4, var5, var6)
finishtime = time.time()
print('Classical function runtime: {:.3f} s'.format(finishtime - starttime))
starttime = time.time()
for i in range(10000000):
procedure()
finishtime = time.time()
print('Procedure runtime: {:.3f} s'.format(finishtime - start time))
This outputs:
Classical function runtime: 2.447 s
Procedure runtime: 1.180 s
Therefore, my question, directed to experienced developers, is:
Is there something that can justify use of Pascal-like procedures over classical functions or they should be avoided at any cost, even if they lead to bulkier and slower code?
EDIT:
Using *args
and **kwargs
solves the issue only partially, as all the parameters still have to be listed during the function call. Also it does not address the performance issue, as parameters are still getting copied. itertools
, as proposed in comments, is also not always applicable. In some cases, resolving the nesting is quite tricky (consider the code below) and would take a lot of developing time, probably leading to a very tangled code.
def my_function(**kwargs):
with open(kwargs['file_a'], 'r') as input1:
for line1 in input1:
if kwargs['number'] % 3 == 0:
if kwargs['number'] % 9 == 0:
with open(kwargs['file_0'], 'r') as input2:
for line2 in input2:
if line1.startswith('#'):
if line2.startswith('A'):
with open('output.txt') as output1:
for item in kwargs['items']:
if item is in kwargs['good']:
for sub in item:
if sub < 0:
result = process_vars(
kwargs['var1'],
kwargs['var2'],
kwargs['var3'],
kwargs['var4'],
kwargs['var5'],
kwargs['var6'],
kwargs['var7'],
kwargs['var8'],
kwargs['var9'],
kwargs['var10'])
output1.write(result)
elif sub >= 0 and sub < 1:
output1.write('hello')
else:
output1.write('byebye')
elif len(item) > 20:
item = item[: 20]
else:
output1.write(line2)
elif line2.startswith('B'):
print('warning')
else:
print('error')
elif line1.startswith('!'):
kwargs['wonders'].count += 1
else:
kwargs['threats'].append(line1)
else:
kwargs['exceptions'].append(line1)
elif kwargs['number'] % 3 == 1:
with open(kwargs['file_1'], 'r') as input2:
...
elif kwargs['number'] % 3 == 2:
with open(kwargs['file_2'], 'r') as input2:
...