Pascal's triangle is a simple and effective way to expand a set of brackets in the form (a + b)n.
In my code the user is asked an input for what order (n) they want and it outputs Pascal's triangle.
The Code
I added in comments to help you understand my reasoning.
def get_super(x): # function to get superscript char.
normal = "0123456789"
super_s = "0123456789"
res = x.maketrans(''.join(normal), ''.join(super_s))
return x.translate(res)
# the history_variable will be the variable returned when function is called. It will contain each co-efficient of every row.
# the number of rows that the history_variable returns is provided by parameters(num)
# I created save_variable as a variable I can use to store the previous row because I need it to create the next row.
# current_variable is a variable that will contain the current row being made.
def basic_pascals(num):
history_variable = [[1], [1, 1]]
save_variable = [1, 1]
current_variable = []
amount = 0
# if the number given is 0 just return 1 as that is the first row.
if num == 0:
return([1])
# if the number given is 1 return [1, 1] as those are the coefficients of (a+b)
elif num == 1:
return([1, 1])
# otherwise we create a for loop that will loop through num-1 iterations - I of every loop here as the making of one row
# in that loop we create a for loop over the save_variable and save_variable[1:] which will let us loop through every possible pair.
# the reason I do this is because each co-efficient in the new row is equal to the addition of the two co_efficients directly above it in the previous row.
# I then add every sum of every pair to the current-variable.
# add 1 to the start and end and then I have the co-efficients of the row.
# equate save_variable to current_variable
# then append save_variable to history_variable
# it repeats itself and finally history_variable is a list of lists each list containing the co-efficients of every row.
for i in range(num-1):
for item in zip(save_variable, save_variable[1:]):
amount += sum(item)
current_variable.append(amount)
amount = 0
current_variable.append(1)
current_variable.insert(0, 1)
save_variable = current_variable
current_variable = []
history_variable.append(save_variable)
return history_variable
# this is essentially adding the a's and b's to the co-efficients
# specify the order you want and if the order == 0 or 1 then it just prints out 1 or (1a + 1b)
# otherwise we make variable co-efficients and call basic_pascals to it.
# power a will equal the highest power possible depending on the order of the row, i.
# power b will equal 0. As you move through every term in a row, the power in a decreases and b increases
# rest is forming f"string" to add it to the co-efficients
def pascals_triangle():
# the n value of (a+b)^n
order = int(input("Enter the order(n) you would like for (a+b)^n: "))
spacing = order*10
if order == 0:
print(1)
if order == 1:
print(f"a{get_super('1')} + b{get_super('1')}")
else:
co_efficients = basic_pascals(order)
for i, row in enumerate(co_efficients):
power_a = i
power_b = 0
result = f""
for item in row:
a = f"a{power_a}"
b = f"b{power_b}"
if i == 0:
result += f"{item}"
elif power_a == 0:
result += f"{item}{get_super(b)} + "
elif power_b == 0:
result += f"{item}{get_super(a)} + "
else:
result += f"{item}{get_super(a)}{get_super(b)} + "
power_a -= 1
power_b += 1
result = result.strip(" + ")
print(result.center(spacing))
pascals_triangle()
Output
Calling basic_pascals(8)
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1], [1, 5, 10, 10, 5, 1], [1, 6, 15, 20, 15, 6, 1], [1, 7, 21, 35, 35, 21, 7, 1], [1, 8, 28, 56, 70, 56, 28, 8, 1]]
Calling pascals_triangle(8)
1
1a1 + 1b1
1a2 + 2a1b1 + 1b2
1a3 + 3a2b1 + 3a1b2 + 1b3
1a4 + 4a3b1 + 6a2b2 + 4a1b3 + 1b4
1a5 + 5a4b1 + 10a3b2 + 10a2b3 + 5a1b4 + 1b5
1a6 + 6a5b1 + 15a4b2 + 20a3b3 + 15a2b4 + 6a1b5 + 1b6
1a7 + 7a6b1 + 21a5b2 + 35a4b3 + 35a3b4 + 21a2b5 + 7a1b6 + 1b7
1a8 + 8a7b1 + 28a6b2 + 56a5b3 + 70a4b4 + 56a3b5 + 28a2b6 + 8a1b7 + 1b8
Improvements
I want to make my code a bit more concise and easier to understand. I even have to think about why I do certain things sometimes.
3 Answers 3
0. Delete all the comments
These paragraph-length comments make the code very hard to read. Comments should be a last resort for understanding the code since programmers only read them when they can't figure out what the code is doing. Well-written code doesn't just make the computer do the correct thing, but is also easy to understand by humans. Let's get rid of the comments and simplify the code.
The fewer words written, the fewer chances for mistakes.
I should expand on this. The best advice I've heard is this: comments should only explain what the code cannot. Comments that describe what the code is doing are useless because a programmer can just read the code. Here are some examples of useful types of comments:
// This function implements <name of obscure algorithm> (<wikipedia link>)
// You would think that doing XXX would be the obvious solution, but that doesn't work because of YYY, which means we have to do ZZZ.
// This logic is required because of <business requirement> which is documented in <policy manual> on page 372.
From my own experience, if I'm reading code I wrote some time ago and it takes me more than a few seconds to understand what I wrote, that's a good place for a short comment to explain what I was trying to do. Either that or it's a good place to rewrite the code into something more comprehensible. One of the longest comments I've ever written was next to a continue
statement to explain why that loop iteration could be skipped.
1. Variable names
Picking accurate and specific variable names makes reasoning about the code easier. Let's look at basic_pascals()
:
def basic_pascals(num):
history_variable = [[1], [1, 1]]
save_variable = [1, 1]
current_variable = []
amount = 0
if num == 0:
return([1])
elif num == 1:
return([1, 1])
for i in range(num-1):
for item in zip(save_variable, save_variable[1:]):
amount += sum(item)
current_variable.append(amount)
amount = 0
current_variable.append(1)
current_variable.insert(0, 1)
save_variable = current_variable
current_variable = []
history_variable.append(save_variable)
return history_variable
The argument num
is the degree of the last polynomial in the triangle, so use degree
instead. The variable history_variable
is the Pascal's Triangle, so let's rename it triangle
. By tracking what happens to save_variable
through the function, we see that it is always equal to the current last row of history_variable
, so a natural name is last_row
. The variable current_variable
is the next row of the triangle being constructed, so let's call it next row
.
def basic_pascals(degree):
triangle = [[1], [1, 1]]
last_row = [1, 1]
next_row = []
amount = 0
if degree == 0:
return([1])
elif degree == 1:
return([1, 1])
for i in range(degree-1):
for item in zip(last_row, last_row[1:]):
amount += sum(item)
next_row.append(amount)
amount = 0
next_row.append(1)
next_row.insert(0, 1)
last_row = next_row
next_row = []
triangle.append(last_row)
return triangle
2. Create variables near where they are needed
Since you create the variables next_row
, last_row
, and amount
outside of the loops, you need to reset them at the end of the loops. If you move these to inside the loop, then they will be reset automatically at the beginning of each loop and you can delete the lines that reset the variables.
def basic_pascals(degree):
triangle = [[1], [1, 1]]
if degree == 0:
return([1])
elif degree == 1:
return([1, 1])
for i in range(degree-1):
next_row = []
last_row = triangle[-1]
for item in zip(last_row, last_row[1:]):
amount = 0
amount += sum(item)
next_row.append(amount)
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
Now we can see that amount
in the innermost loop is always equal to sum(item)
, so let's just use the latter expression.
def basic_pascals(degree):
triangle = [[1], [1, 1]]
if degree == 0:
return([1])
elif degree == 1:
return([1, 1])
for i in range(degree-1):
next_row = []
last_row = triangle[-1]
for item in zip(last_row, last_row[1:]):
next_row.append(sum(item))
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
3. Expressive loop conditions
How do we know we are done constructing the triangle? An n-th degree Pascal's Triangle has n+1 rows. This makes for a more expressive loop condition that lets the programmer know when the construction is complete. Most of the time, if a loop variable is not used, i
in this case, that is a good indication that there is a better way to write it.
def basic_pascals(degree):
triangle = [[1], [1, 1]]
if degree == 0:
return([1])
elif degree == 1:
return([1, 1])
while len(triangle) < degree + 1:
next_row = []
last_row = triangle[-1]
for item in zip(last_row, last_row[1:]):
next_row.append(sum(item))
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
4. Consistent return values
Right now, there are two possible return types: a list of lists [[]]
if degree >= 2
and a list []
otherwise. If you make the types of all possible return values the same, then any code that calls this function can be simpler because it only has to handle one data type. This function should construct the full triangle, so all return statements should return a list of lists representing the full triangle.
def basic_pascals(degree):
triangle = [[1], [1, 1]]
if degree == 0:
return [[1]]
elif degree == 1:
return [[1], [1, 1]]
while len(triangle) < degree + 1:
next_row = []
last_row = triangle[-1]
for item in zip(last_row, last_row[1:]):
next_row.append(sum(item))
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
Now, pascals_triangle()
doesn't need special cases for degrees 0 and 1.
5. Removing special cases.
Now that all return values return the same data type, do we even need the special cases for degree=0
and degree=1
? Let's delete the initial if
block and replace the initial value of triangle
with [[1]]
.
def basic_pascals(degree):
triangle = [[1]]
while len(triangle) < degree + 1:
next_row = []
last_row = triangle[-1]
for item in zip(last_row, last_row[1:]):
next_row.append(sum(item))
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
This still returns the correct answer.
6. Replace for ... append
with list comprehensions
The inner for loop can be expressed as a single line to make the full list. This is called a list comprehension.
def basic_pascals(degree):
triangle = [[1]]
while len(triangle) < degree + 1:
last_row = triangle[-1]
next_row = [sum(item) for item in zip(last_row, last_row[1:])]
next_row.append(1)
next_row.insert(0, 1)
triangle.append(next_row)
return triangle
We can even incorporate the ones on the start and end.
def basic_pascals(degree):
triangle = [[1]]
while len(triangle) < degree + 1:
last_row = triangle[-1]
next_row = [1] + [sum(item) for item in zip(last_row, last_row[1:])] + [1]
triangle.append(next_row)
return triangle
I added space before the return statement to emphasize the three steps: initialize triangle
, construct triangle
, return triangle
.
Other parts of the code
In pascals_triangle()
, in addition to making similar changes as described above, you can create a function that takes co_efficient
, power_a
, and power_b
as argument to make each term in the polynomial. This will simplify the loop and give you more freedom to get the notation correct.
Rather than trying to build the whole expression at once, it is simpler to build a list of terms (terms = ["1a3", "3a2b1", "3a1b2", "1b3"]
) and then use " + ".join(terms)
to create the full string. Then, you don't have to use strip()
at the end.
-
\$\begingroup\$ To be clear on #0: comments should explain tricky code or why a code does what it does, not just repeat what it does. \$\endgroup\$qwr– qwr2022年08月31日 19:31:08 +00:00Commented Aug 31, 2022 at 19:31
-
1\$\begingroup\$ @qwr I've expanded on the comment section. \$\endgroup\$Mark H– Mark H2022年09月01日 13:45:16 +00:00Commented Sep 1, 2022 at 13:45
-
\$\begingroup\$ #0 could use a line or two about docstrings, as an alternative to throwing out the comments completely. \$\endgroup\$2022年09月01日 20:09:07 +00:00Commented Sep 1, 2022 at 20:09
-
1\$\begingroup\$ Another popular way to explain your excellent point about comments: code explains how the code works, comments explain why the code works. (Sorry, I'm treading slightly on @qwr's toes.) +1. \$\endgroup\$J.G.– J.G.2022年09月01日 20:58:00 +00:00Commented Sep 1, 2022 at 20:58
Mark H has given good feedback about the triangle generation. I'll try not to repeat that feedback.
translate & maketrans
str.translate(table)
is my favourite Python function, so I hate seeing it used improperly.
def get_super(x): # function to get superscript char.
normal = "0123456789"
super_s = "0123456789"
res = x.maketrans(''.join(normal), ''.join(super_s))
return x.translate(res)
First, ''.join(normal)
is simply normal
, and ''.join(super_s)
is simply super_s
. The ''.join(...)
calls are busywork that does nothing but waste time.
def get_super(x): # function to get superscript char.
normal = "0123456789"
super_s = "0123456789"
res = x.maketrans(normal, super_s)
return x.translate(res)
Now, this function is called many, many times. Each time, it computes res = x.maketrans(normal, super_s)
, and normal
and super_s
are never different, so the result res
is always the same. Again, this is wasting time. We should compute this once, outside of the function.
SUPERSCRIPT_TRANS_TABLE = str.maketrans("0123456789", "0123456789")
def get_super(s: str) -> str:
"""
Replace the digits 0 through 9 in a string with superscript equivalents.
All other characters remain unchanged.
>>> get_super(' a1 b2 c3 ')
' a1 b2 c3 '
"""
return s.translate(SUPERSCRIPT_TRANS_TABLE)
I've additionally replaced # function to get superscript char.
with a docstring describing what the function does, and added type-hints to the function signature. This is much more useful, since a user can actually type help(get_super)
at an interactive prompt and get a description of what the function does, and how to use it, including an example of calling the function.
>>> help(get_super)
Help on function get_super in module __main__:
get_super(s: str) -> str
Replace the digits 0 through 9 in a string with superscript equivalents.
All other characters remain unchanged.
>>> get_super(' a1 b2 c3 ')
' a1 b2 c3 '
>>>
The doctest
module can even be used to read the "examples" from the docstrings, execute them, and verify the actual output matches the example output.
>>> import doctest
>>> doctest.testmod(verbose=True)
Trying:
get_super(' a1 b2 c3 ')
Expecting:
' a1 b2 c3 '
ok
1 items had no tests:
__main__
1 items passed all tests:
1 tests in __main__.get_super
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
TestResults(failed=0, attempted=1)
>>>
You'd usually put this in a main-guard, without the verbose flag, so that nothing is displayed when all tests pass.
if __name__ == '__main__':
import doctest
doctest.testmod()
I'd prefer a different function name, like digits_to_superscript
or to_superscript
or even simply superscript
instead of get_super
.
Bug
Enter the order(n) you would like for (a+b)^n: 1
a1 + b1
This is not a triangle, since 1
is not printed centered above it! It also lacks the coefficients in front of the variables (eg, 1a1 + 1b1
) of degree ≥ 2 output.
Centering
10*order
is an interesting guess at how much space is needed for the triangle to be centered in, but it will not work for the general case. For order 8, it is 10 characters too many. For orders higher than 10, your triangle can have terms like + 63205303218876a24b25
... which has significantly more than 10 character.
You've already computed the terms for the last row of the triangle before you start printing it. You could easily determine exactly how long that row is:
...
...
last_row = co_efficients[-1]
spacing = sum(map(len, map(str, last_row))) # Coefficients
spacings += 2 * sum(map(len, map(str, range(1, order+1)))) # Exponents
spacings += 2 * order # a's & b's
spacings += 3 * order # " + "
...
Alternately, without trying to compute the length from the cooefficients, you could generate and store all of the strings, then simply measure the length of the last one and print out all strings centered to that length.
Reworked code
Here is my rewrite of the code, in Python 3.10. It uses infinite generators for both Pascal's Triangle coefficients and polynomial generation. pairwise()
is now part of itertools
, and removes the need for the inefficient zip(save_variable, save_variable[1:])
construct, which makes an unnecessary copy of the elements of save_variable
for the [1:]
slicing operation. The match
statement is a new Python 3.10 construct ... and undoubtedly overkill for this usage, but it was fun to use. ;^)
from itertools import pairwise, islice
from typing import Any, Iterator
SUPERSCRIPT_TRANS_TABLE = str.maketrans("0123456789", "0123456789")
def superscript(obj: Any) -> str:
"""
Replace the digits 0 through 9 in an object's string representation with
superscript equivalents. All other characters remain unchanged.
>>> superscript(' a1 b2 c3 ')
' a1 b2 c3 '
"""
return str(obj).translate(SUPERSCRIPT_TRANS_TABLE)
def center(lines: Iterator[str]) -> Iterator[str]:
"""
Collect a sequence of strings, determine the maximum length,
and yield the sequence of strings centered within the width
of the longest string.
"""
lines = list(lines)
max_width = max(map(len, lines))
return (line.center(max_width) for line in lines)
def pascals_triangle_generator() -> Iterator[list[int]]:
"""
A pascal triangle coefficent row generator
Returns an infinite generator of Pascal's triangle rows
"""
row = [1]
while True:
yield row
row = [1] + [sum(terms) for terms in pairwise(row)] + [1]
def pascals_triangle_polynomials(var1: str, var2: str) -> Iterator[str]:
"""
A pascal triangle polynomial generator
Returns an infinite generator of polynomials generated using
Pascal's triangle.
"""
def term(args):
match args: # Note: Requires Python 3.10
case coeff, 0, 0:
return str(coeff)
case coeff, m, 0:
return f"{coeff}{var1}{superscript(m)}"
case coeff, 0, n:
return f"{coeff}{var2}{superscript(n)}"
case coeff, m, n:
return f"{coeff}{var1}{superscript(m)}{var2}{superscript(n)}"
for row, coeffs in enumerate(pascals_triangle_generator(), 1):
exponents = range(row)
yield " + ".join(map(term, zip(coeffs, exponents[::-1], exponents)))
def basic_pascals(degree: int) -> list[list[int]]:
"""
Return Pascal's triangle of the given degree
>>> basic_pascals(4)
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
"""
return list(islice(pascals_triangle_generator(), degree + 1))
def pascals_triangle(degree: int, var1: str, var2: str) -> None:
"""
Print Pascal's Triangle, of the given degree, with the given variables,
centered based on the length of the longest line
>>> pascals_triangle(4, "x", "y")
1
1x1 + 1y1
1x2 + 2x1y1 + 1y2
1x3 + 3x2y1 + 3x1y2 + 1y3
1x4 + 4x3y1 + 6x2y2 + 4x1y3 + 1y4
"""
print(*center(islice(pascals_triangle_polynomials(var1, var2), degree + 1)),
sep='\n')
if __name__ == '__main__':
import doctest
doctest.testmod()
print(basic_pascals(8))
pascals_triangle(8, "a", "b")
I'd already half-written this before the other answers rolled in, so I'll attempt to minimise overlap:
This is a good application for iterators, which you don't use.
Though he didn't describe doing so, AJNeufeld has correctly added PEP484 typehints and you should do the same.
With minimal effort, you can improve your formatting to omit redundant 1 coefficients and 1 exponents.
Prefer tuples over lists for immutable data.
Suggested
from string import digits
from typing import Iterator
EXPONENTS = str.maketrans(digits, "0123456789")
def pascal_coefficients(num: int) -> Iterator[tuple[int, ...]]:
row = 1,
for y in range(num+1):
yield row
row = (
1,
*(sum(row[x: x+2]) for x in range(y)),
1,
)
def format_term(letter: str, power: int) -> str:
if power == 0:
return ''
if power == 1:
return letter
exp_str = str(power).translate(EXPONENTS)
return letter + exp_str
def format_row(row: tuple[int, ...]) -> Iterator[str]:
y = len(row)
for x, n in enumerate(row):
result = format_term('a', y - x - 1) + format_term('b', x)
if n != 1 or not result:
result = f'{n}{result}'
yield result
def pascals_triangle_lines(order: int) -> Iterator[str]:
for row in pascal_coefficients(order):
yield ' + '.join(format_row(row))
def pascals_triangle(order: int) -> Iterator[str]:
*others, last = pascals_triangle_lines(order)
for other in others:
yield other.center(len(last))
yield last
def main() -> None:
order = int(input("Enter the order(n) you would like for (a+b)^n: "))
print('\n'.join(pascals_triangle(order)))
if __name__ == '__main__':
main()
Output
Enter the order(n) you would like for (a+b)^n: 11
1
a + b
a2 + 2ab + b2
a3 + 3a2b + 3ab2 + b3
a4 + 4a3b + 6a2b2 + 4ab3 + b4
a5 + 5a4b + 10a3b2 + 10a2b3 + 5ab4 + b5
a6 + 6a5b + 15a4b2 + 20a3b3 + 15a2b4 + 6ab5 + b6
a7 + 7a6b + 21a5b2 + 35a4b3 + 35a3b4 + 21a2b5 + 7ab6 + b7
a8 + 8a7b + 28a6b2 + 56a5b3 + 70a4b4 + 56a3b5 + 28a2b6 + 8ab7 + b8
a9 + 9a8b + 36a7b2 + 84a6b3 + 126a5b4 + 126a4b5 + 84a3b6 + 36a2b7 + 9ab8 + b9
a10 + 10a9b + 45a8b2 + 120a7b3 + 210a6b4 + 252a5b5 + 210a4b6 + 120a3b7 + 45a2b8 + 10ab9 + b10
a11 + 11a10b + 55a9b2 + 165a8b3 + 330a7b4 + 462a6b5 + 462a5b6 + 330a4b7 + 165a3b8 + 55a2b9 + 11ab10 + b11
-
2\$\begingroup\$
EXPONENTS[power]
doesn't work for powers greater than 9. \$\endgroup\$Mark H– Mark H2022年08月31日 01:51:44 +00:00Commented Aug 31, 2022 at 1:51 -
1\$\begingroup\$ You're welcome. I tried the same thing while working on my answer; "surely it's just a table lookup ..." \$\endgroup\$Mark H– Mark H2022年08月31日 12:12:13 +00:00Commented Aug 31, 2022 at 12:12
math.comb(n,k)
. \$\endgroup\$