Position or name
Arguments can be passed by position (matched to the parameters in order) or by name (quantity=200). Names make a call readable when a function has several inputs of the same type, and they let you pass the arguments in any order. Positional arguments must come first; once you use a name, the rest must be named too.
def pnl(entry, exit_price, quantity):
return (exit_price - entry) * quantity
print(pnl(184.0, 185.5, 200)) # 300.0
print(pnl(quantity=200, entry=184.0, exit_price=185.5)) # 300.0Default values make parameters optional
Write name=value in the def line and the caller may leave that argument out; the default is used. Parameters with defaults come after those without. Defaults suit settings that are usually the same, like a fee rate or a minimum, and any of them can still be overridden by name.
# Parameters with a default are optional
def fee(notional, rate=0.001, minimum=1.0):
return max(minimum, notional * rate)
print(fee(36850.0)) # 36.85
print(fee(36850.0, rate=0.0005)) # 18.425
print(fee(100.0, minimum=0.5)) # 0.5Any number of arguments: *args
A parameter written *prices collects every extra positional argument into a tuple, so average(1, 2, 3) and average(1, 2, 3, 4, 5) both work. The mirror image works at the call: average(*quotes) unpacks a list into separate arguments.
# *prices collects the extra arguments into a tuple
def average(*prices):
return sum(prices) / len(prices)
print(average(184.0, 185.0, 186.0)) # 185.0
quotes = [184.0, 185.0]
print(average(*quotes)) # 184.5Any named options: **options
In the same way **options collects extra named arguments into a dictionary of names and values (dictionaries get a topic of their own soon). It suits functions that accept optional extras, such as order settings. sorted(options) gives the names in alphabetical order, so the output is predictable.
# **options collects extra named arguments into a dictionary
def describe(symbol, **options):
text = symbol
for name in sorted(options):
text += f" {name}={options[name]}"
return text
print(describe("AAPL", tif="day", limit=184.5)) # AAPL limit=184.5 tif=dayThe mutable default trap
A default value is created once, when the function is defined, not on every call. With a number or text that is harmless. With a list, every call that uses the default shares and changes the same list. The standard fix is to default to None and build a fresh list inside the function.
# A default list is created once and shared by every call
def add_fill(fill, fills=[]):
fills.append(fill)
return fills
print(add_fill(100)) # [100]
print(add_fill(200)) # [100, 200] <- surprise: the same list
# The fix: default to None and build a new list inside
def add_fill_fixed(fill, fills=None):
if fills is None:
fills = []
fills.append(fill)
return fills
print(add_fill_fixed(100)) # [100]
print(add_fill_fixed(200)) # [200]- Use names in calls with several inputs of the same type.
- Defaults for settings that are usually the same; never a list or dictionary.
*argsfor any number of values,**optionsfor named extras.
Next up: scope and closures, to see which names a function can see and how it can remember values.
Lab · what's graded
- Make quantity and side optional with
position_pnl25 pts - Accept any number of prices with
average25 pts - Take named order options with
describe_order25 pts - Avoid the shared-list trap with
add_fill25 pts