104 lines
3.3 KiB
Plaintext
104 lines
3.3 KiB
Plaintext
This rule raises an issue when the default value of a function parameter is mutated.
|
|
|
|
== Why is this an issue?
|
|
|
|
In Python, function parameters can have default values.
|
|
|
|
These default values are expressions which are evalutated when the function is defined, i.e. only once. The same default value will be used every time the function is called. Therefore, modifying it will have an effect on every subsequent call. This can lead to confusing bugs.
|
|
|
|
[source,python]
|
|
----
|
|
def myfunction(param=foo()): # foo is called only once, when the function is defined.
|
|
...
|
|
----
|
|
|
|
For the same reason, it is also a bad idea to store mutable default values in another object (ex: as an attribute). Multiple instances will then share the same value and modifying one object will modify all of them.
|
|
|
|
|
|
This rule raises an issue when:
|
|
|
|
* a default value is either modified in the function or assigned to anything other than a variable and it has one of the following types:
|
|
** https://docs.python.org/3/library/collections.html[collections] module: deque, UserList, ChainMap, Counter, OrderedDict, defaultdict, UserDict.
|
|
* an attribute of a default value is assigned.
|
|
|
|
|
|
=== Exceptions
|
|
|
|
In some rare cases, modifying a default value is intentional. For example, default values can be used as a cache.
|
|
|
|
|
|
No issue will be raised when the parameter's name contains "cache" or "memo" (as in memoization).
|
|
|
|
== How to fix it
|
|
|
|
When a parameter default value is meant to be a mutable object, it is best to keep the parameter optional and instantiate the mutable object in the function's body directly.
|
|
|
|
=== Code examples
|
|
|
|
==== Noncompliant code example
|
|
|
|
In the following example, the parameter "param" has ``++list()++`` as a default value. This list is created only once and then reused in every call. Thus when appending ``++'a'++`` to this list in the body of the function, the next call will have ``++['a']++`` as a default value.
|
|
|
|
|
|
[source,python,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
def myfunction(param=list()): # Noncompliant: param is a list that gets mutated
|
|
param.append('a') # modification of the default value.
|
|
return param
|
|
|
|
print(myfunction()) # returns ['a']
|
|
print(myfunction()) # returns ['a', 'a']
|
|
print(myfunction()) # returns ['a', 'a', 'a']
|
|
----
|
|
|
|
|
|
==== Compliant solution
|
|
|
|
[source,python,diff-id=1,diff-type=compliant]
|
|
----
|
|
def myfunction(param=None):
|
|
if param is None:
|
|
param = list()
|
|
param.append('a')
|
|
return param
|
|
|
|
print(myfunction()) # returns ['a']
|
|
print(myfunction()) # returns ['a']
|
|
print(myfunction()) # returns ['a']
|
|
----
|
|
|
|
== Resources
|
|
|
|
=== Documentation
|
|
|
|
* Python documentation - https://docs.python.org/3/reference/compound_stmts.html#function-definitions[Function definitions]
|
|
|
|
=== External coding guidelines
|
|
|
|
* The Hitchhiker's Guide to Python - https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments[Common Gotchas]
|
|
|
|
|
|
ifdef::env-github,rspecator-view[]
|
|
|
|
'''
|
|
== Implementation Specification
|
|
(visible only on this page)
|
|
|
|
=== Message
|
|
|
|
Change this default value to "None" and initialize this parameter inside the function/method.
|
|
|
|
|
|
=== Highlighting
|
|
|
|
* Primary: the default value
|
|
* Secondaries:
|
|
** code modifying the parameter:
|
|
message: 'The parameter is modified.'
|
|
|
|
** code assigning the parameter to something else
|
|
message: 'The parameter is stored in another object.'
|
|
|
|
|
|
endif::env-github,rspecator-view[]
|