見出し画像

Retro-gaming and so on

リスト内包表記

全部が全部そうではないが、いくつかのプログラミング言語に於いては、上達に対して「ツボ」と言えるモノが存在する言語がある。
Pythonに於いては、個人的にはそれは「リスト内包表記」と言う機能だ。

前にも指摘したけどまず前提から話をする。
プログラミングをやってるどんな人でも、心の奥底で一番大嫌いな事柄が何かと言えば、「繰り返し」だと思う。
繰り返しは初心者だけに限らず、どんなヤツでも大嫌いなのである。ホント。
何故か?まず「かならず間違える」からだ。人間は繰り返し、ひいては繰り返し構文が大っ嫌いなのだ。いやマジで。
C言語なんかでも配列相手に繰り返しを書いて、間違えて境界線を超えてsegmentation faultに襲われる、なんつーのは日常茶飯事だ。避けようがない。人間はエラーを起こすモノで、そしてとにかくエラーが頻出するのは間違いなく「繰り返し」である。

「俺は違う」ってヤツがいたら驚きだ。そいつは多分変態だろう。

じゃあ、初心者とプログラミングに慣れてるヤツの違いは何だろう?ぶっちゃけて言うと、プログラミングに慣れてるヤツはエラーにも慣れてる、のだ。だから初心者はエラーにビビるけど慣れてるヤツはビビらない。
それだけの話である。
ハッキリ言って、起きるエラーの量から考えると初心者より上級者の方が多いくらいかもしんない。繰り返しに限らず、「プログラミングに慣れれば慣れるほどエラーが増えていく」と言うのがむしろ現実なのだ。
いずれにせよ、「繰り返し」と言うのはそれだけ厄介なブツなのである。しかもプログラミングに必須だから始末におえない。
だからプログラミング言語が進化するに付随して、「何とか繰り返しを簡単に書けないか?」と構文レベルからの試行錯誤が行われてきた、のである。
ちなみに、厳密な定義ではないが、それらの「繰り返しを簡単に書けるような試行錯誤の様々の結果」を、通常イテレータと呼ぶ。

実の事を言うと、Pythonの場合、C言語で言うような「繰り返しの構文」は存在しない。for文はC言語のそれに見かけは似せてはいるが、実際のトコ、こいつはもっと高級な「イテレータ」を走らせる為のキーワードである。
ただ、Pythonは「もっと簡単に配列(リスト)相手に繰り返しを実行出来る機能を付けられないか?」と機能を他の言語から借りてこようとした。一つはLispで有名なmap。そしてもっと後になってHaskellと言う言語から持ってきた機能が「リスト内包表記」である。

ハッキリ言う。Pythonでは反復のほぼ8割はリスト内包表記絡みで解決可能だ(※1)。1割はreduceで解決出来る。forwhileのお世話にならなければならないケースは1割か、それに満たない。
つまり、個人的な意見では初心者でも何が何でも覚えなければならないのはまずはリスト内包表記なのだ。反復の8割にも適用出来るのなら既に最重要機能だろう。最重要機能である以上初心者必須である。forwhileより先に覚えるべきだ。

んでもう一つ言っておかなければならない。
「PythonはC言語の派生である」とか言うトンデモ論を打つヤツがたまに出てくるが。こういうトンデモ論が出てくる背景にはC言語を学ぶ事が全てのプログラミングの基礎である、と言う誤解があるのだ。
ハッキリ言う。大間違いも大間違いな誤解だ
前にも書いたけど、例えばPythonのforでリストのインデックスを使ってルーピングする、なんつー発想は大間違いなのだ。しかし、C言語でこれが正しいとすると、既にC言語はプログラミング言語教育の基礎になっていない、と言う意味になる。分かるか?
もう一回言おう。C言語で正しいやり方が別の言語だと「動くにせよ」正しくない場合、C言語はプログラミング言語教育の基礎からは乖離してる、って言う意味になるのだ。
要するに、PythonをC言語のように教えるのは間違いだし、百害あって一利もないのである。そして結果分かるのは「C言語がすべてのプログラミング言語教育の基礎に成りうる」なんつーのはトンデモ論でしかねぇ、ってこった。

なんでこんな誤解が蔓延したんだろ?一つの理由として、例えば、Javaしか知らないプログラマとC言語からJavaに来たプログラマが書いたコードを見比べると後者の方が綺麗だ、と言うのは良く言われる事だ。ただしそれは近視眼的な結論であり、Cの構文と機能を元にデザインされた言語に於いてCのエキスパートが書けばそれはその言語に於いてのみは綺麗なコードにはなるだろう。当たり前、である。
でもその延長線上で、どんな言語でもC言語のエキスパートが綺麗なコードを書ける、って保証にはなんないわな。それとこれたぁ全然別の話、である。
なのになんでそんな近視眼的な結論に陥ったのか、っつーと、殆どのプログラマは長い間、C言語しか知らなかった、からだ。最初に覚えた、ないしは最初に「覚えろ」って言われた言語だけしか見てなかったし使わなかったし、自発的に他のプログラミング言語を学ぼう、とさえ思わなかったから、である。だから反例があるのを知らんかったわけだ。そしてその反例の一つが上のPythonのループにおけるリストのインデックス使用、と言うとんでもなく汚い方法論である。
いずれにせよ、無知である事、これが大多数のプログラマの行動だった、って事に他ならない。色々言うけど、連中の殆どはそんなに自発的に勉強する奴らじゃあねぇんだよ。だからC言語とその周辺しか知らない。だからトンデモねぇ事(「C言語はプログラミングの基本」と言うウソ)を言い出す。
でもプログラミングの基本、とか言う割にはC言語を学んだから、っつって、ハードウェアと密接してるアセンブリ言語や機械語ををいきなり「理解する」なんつー事にはならない。ならないんだけど、彼らは「基本だ」って言い張るんだ。多分みんながそう言うからそう言ってるだけだろうし、「基本」とはどういう意味なのか、彼らは知らんのだろう。
もう一回言うと、Pythonを使う以上、あるいは覚える以上C言語は忘れろC言語での「方法論」はPythonでは「悪習」でしかないし、Pythonプログラミングでの基本には成りえないのだ。

1. Cの反復の練習問題を敢えてリスト内包表記で解く(※2)

まず最初にちょっとした実験をしようと思う。C言語に於ける反復の問題を引っ張ってきて、Pythonでのリスト内包表記を中心として駆使すれば、如何に簡単に解けるか見てみようと思う。
これはリスト内包表記を使う為の「練習問題」に成り得るし、言い方を変えると、C言語が如何に無駄な「抽象度の足りない」考え方をユーザーに押し付けてるか分かると思う。
Pythonは要するに、C言語より遥かに抽象度が高いプログラミング言語なんだ、と言う事だ。

練習問題 1:
“SPAM”という単語を 10 回表示するプログラムを作成しなさい。
解:

def one(n = 10):
 [print("SPAM") for i in range(n)]

>>> one()
SPAM
SPAM
SPAM
SPAM
SPAM
SPAM
SPAM
SPAM
SPAM
SPAM
>>>

練習問題 2
九九、三の段( 3 ~ 27 の 3 の倍数)を表示するプログラムを作成しなさい。

解:
def two(n = 3):
 [print(n * i) for i in range(1, 10)]

>>> two()
3
6
9
12
15
18
21
24
27
>>>

練習問題 3
2 の 1 乗から 8 乗までを計算し表示するプログラムを作成しなさい。

解:

def three(base = 2, power = 8):
 [print(base ** i) for i in range(1, power + 1)]

>>> three()
2
4
8
16
32
64
128
256
>>>

練習問題 4
7 の階乗を計算し、表示するプログラムを作成しなさい。
※ 階乗:1 から n までの積。1 × 2 × 3 × … ×( n - 1 )× n

解: 
def four(n = 7):
 ini = 1
 [ini := ini * i for i in range(1, n + 1)]
 print(ini)

>>> four()
5040
>>>

解説: セイウチ構文が飛び出す辺り、実の事言うとかなりムリクリである。
実際のトコはこのケースでは、functools.reduceを使う方がしっくり来るだろうし、そうすべきである。
あくまで練習問題の一環として、こういう形式で書いた。

reduceを用いた別解:

def four(n = 7):
 from functools import reduce
 print(reduce(lambda acc, x: acc * x, range(1, n + 1), 1))

なお、本当の事を言うとBattery IncludedのPythonではmathにfactorialが含まれている

>>> from math import factorial
>>> factorial(7)
5040

練習問題 5
整数を 10 回入力し、平均値を求めるプログラムを作成しなさい。
※ 計算は整数で行い、小数点以下は切り捨ててよい。

解:
def five(n = 10):
 from statistics import mean
 from math import floor
 return floor(mean([int(input()) for i in range(n)]))

練習問題 6
整数、0 か 1 を 10 回入力する。これを対戦成績と考え、0 を負け、1 を勝ちとして、勝ちの総数、負けの総数を表示するプログラムを作成しなさい。

解:

def six(n = 10):
 match_results = [False if int(input()) == 0 else True for i in range(n)]
 print(match_results.count(True), match_results.count(False))

練習問題 7
次のプログラムを作成しなさい。
  • 巨人、阪神戦で毎回の得点を入力する。(1 回 ~ 9 回)
  • 入力が終わったら、それぞれの得点とどちらが勝ったか、引き分けかを表示する。
※ 試合は巨人の先行とする。
1回表、巨人の得点は? 0
1回裏、阪神の得点は? 0
2回表、巨人の得点は? 0
2回裏、阪神の得点は? 1
9回表、巨人の得点は? 0
9回裏、阪神の得点は? 1

巨人:5点, 阪神:6点
   阪神の勝ち

解:

def seven(n = 9):
 games = [(int(input("{}回表、巨人の得点は?".format(i + 1))), \
      int(input("{}回裏、阪神の得点は?".format(i + 1))))\
      for i in range(9)]
 giants, tigers = [sum(i) for i in zip(*games)]
 print("巨人: {0}点, 阪神: {1}点\n\t{2}の勝ち".format(giants, tigers,\
                    "阪神" if tigers > giants\
                    else "巨人"))

解説: 若干テクニックを要する問題である。
ポイントはzip関数。「zip() に続けて * 演算子を使うと、zip したリストを元に戻せます」と言う事はunzip可能だ、と言う事なので、それを利用する辺りが若干トリッキーな解である。
三項演算子的な記述をしてたりもするが、この問題は見方を変えると「色んなPythonなりのテクニック総動員」なんで、この一問だけで色々な勉強が可能な良問でもある。

練習問題 8
自然数(正の整数)を 10 回入力し、最大値を求めるプログラムを作成しなさい。

解:
def eight(n = 10):
 return max([int(input()) for i in range(n)])

練習問題 9
整数を 10 回入力し、最大値と最小値を求めるプログラムを作成しなさい。

解:

def nine(n = 10):
 lst = [int(input()) for i in range(n)]
 return max(lst), min(lst)

練習問題 10
個数を示す数値を入力し、その個数分だけ‘*’を表示するプログラムを作成しなさい。

解:

def ten(n):
 print("".join(["*" for i in range(n)]))

練習問題 11
個数を示す数値を入力し、その個数分だけ 0 ~ 9 の数字を表示するプログラムを作成しなさい。数字は 0 , 1 , 2 , 3 , , の順に表示し、9  の次は 0 に戻るものとします。
  例:     14
  01234567890123 

解:
def eleven(n):
 s = "0123456789"
 m, d = n // len(s), n % len(s)
 print(s * m + s[:d])

解説: ここでは初の「リスト内包表記で解けない」問題、である。
かと言って、じゃあforやwhileの出番か、と言うと全然違って、文字列の基本操作とちょっとしたアイディアで解ける「むしろ簡単な」問題なのである。
C言語だとこの程度の事をやるにも「複雑過ぎる手順を要する」に過ぎないのだ。

練習問題 12
10000 より小さい 3 の累乗( 3, 9, 27, , , )をすべて表示するプログラムを作成しなさい。

解:

def twelve(n = 10000, d = 3):
 from math import log
 [print(i) for i in range(d, n + 1) if (log(i)/log(d)).is_integer()]

解説: この問題はリスト内包表記がどーの、と言うより、単に累乗数の判定をどうするのか、それだけ、が鍵で、少々数学を要する。
要するに例えばy = b^xだった場合、両辺対数を取るとln|y| = x*ln|b|故、xはln|y|/ln|b|となり、yがbの累乗数だった場合、xは整数でなければならない、と言うそれだけの理屈である。
なお、Pythonでfloatがきっかり整数になってるか否かはis_integer()で判定する。

練習問題 13
数値を繰り返し入力し、合計が 100 を超えたら入力を止めて合計を表示するプログラムを作成しなさい。

解:

def thirteen(n = 100, acc = 0):
 class Thirteen(object):
  def __init__(self, n, acc):
   self.n = n
   self.acc = acc
  def __iter__(self):
   return self
  def __next__(self):
   if self.acc > self.n:
    raise StopIteration
   val = float(input())
   self.acc += val
   return val
 print(sum([i for i in Thirteen(n, acc)]))

解説: 久々にリスト内包表記では素直に解けない問題である。
結局、この問題の本質は、実はリストが関係ないのでリスト内包表記に向かない、のである。
そういった背景だが、敢えてイテレータを設計して無理矢理リスト内包表記で解いている。
真似しなくて良い。「出来る」と言う証明の為だけにこうしている。
なお、あまり知られてないが、Pythonではこのように、ローカル関数ならぬローカルクラス、なんつーC++/Javaでは考えられないようなモノを作る事が出来る。

練習問題 14
ストライク・カウントを数えるプログラムを作成しなさい。
  • 1球ごとにストライクかボールかを入力する。
  • 3ストライクまたは4ボールになったら入力を止め、ストライクとボールのカウントを表示する。
※ ストライクの場合は 1、ボールの場合は 2 を入力する。
ストライク=1 or ボール=2 ?
1
ストライク=1 or ボール=2 ?
2
ストライク=1 or ボール=2 ?
1
ストライク=1 or ボール=2 ?
1
1ボール,3ストライク

解:

def fourteen(strike = 0, ball = 0):
 class Fourteen(object):
  def __init__(self, strike, ball):
   self.strike = strike
   self.ball = ball
  def __iter__(self):
   return self
  def __next__(self):
   if (self.strike == 3) or (self.ball == 4):
    raise StopIteration
   val = int(input("ストライク = 1 or ボール = 2 ?\n"))
   if val == 1:
    self.strike += 1
    return 0, 1
   elif val == 2:
    self.ball += 1
    return 1, 0
 b, s = [sum(j) for j in zip(*[i for i in Fourteen(strike, ball)])]
 print("{0}ボール, {1}ストライク".format(b, s))

解説: 練習問題13に続き、リスト内包表記では解けない問題。いや、この通り、頑張れば可能だが、別にそこまで頑張る必要もない。
と言うのもリスト内包表記はあくまでラクだから使うのであって、ラクじゃなきゃ使わなくて良いのだ。
そしてラクじゃないケースはいつなんだ、ってのは知らなきゃいけない。
練習問題13に準じて、本質的にこの問題はリスト絡みじゃないから使いづらいのだ。
徐々に「いつ使うべき」で「いつ使わざるべきか」これらの練習問題を通して分かっていくだろう。

練習問題 15
前の問題に次の修正を加えなさい。
  • 1球ごとにストライク、ボール、ファウルの何れかを入力する。(残念ながらヒットにはなりません)
  • ファウルの場合、2ストライクまではストライクにカウントするが、3ストライクにはならない。
  • 3ストライクまたは4ボールになったら入力を止め、ストライクとボールのカウントを表示する。
解:

def fifteen(strike = 0, ball = 0):
 class Fifteen(object):
  def __init__(self, strike, ball):
   self.strike = strike
   self.ball = ball
  def __iter__(self):
   return self
  def __next__(self):
   if (self.strike == 3) or (self.ball == 4):
    raise StopIteration
   val = int(input("ストライク = 1, ボール = 2, or ファウル = 3 ?\n"))
   if val == 1:
    self.strike += 1
    return 0, 1
   elif val == 2:
    self.ball += 1
    return 1, 0
   elif val == 3:
    if self.strike
     self.strike += 1
     return 0, 1
    else:
     return 0, 0
 b, s = [sum(j) for j in zip(*[i for i in Fifteen(strike, ball)])]
 print("{0}ボール, {1}ストライク".format(b, s))

練習問題 16
入力された数が素数かどうかを判定するプログラムを作成しなさい。
※ 判定する数は 4 以上としてよい。

解:

def sixteen(n):
 return len([n if i*i > n else i for i in range(2, n + 1) if n % i == 0]) == 1

解説: これもリスト内包表記で解くのは難問。従ってフツーに解いて良い。
ただし、アタマの体操としては上の関数は悪い例ではない。結局素数であるか否か、と言うのは命題になりえる計算の解が唯一なのか、複数あるのか、と言う事である。
結果、リストの長さが1であれば素数、そうじゃなければ素数じゃなくなる。
そして計算自体は試し割り法である。

練習問題 17
2 以上の数値を入力し、素因数分解した結果を表示しなさい。
例:
 20100
 2 2 3 5 5 6

解:

def seventeen(n):
 from math import sqrt, ceil
 def foo(x, y):
  acc = []
  while True:
   x /= y
   if x.is_integer():
    acc += [y]
   else:
    return acc
 [print("{} ".format(j), end="")\
 for j in sum([foo(n, i) for i in range(2, ceil(sqrt(n))) \
      if sixteen(i)], [])]

解説: もちろん、while構文とリスト内包表記の「合わせ技」を使っても構わない。
ここでは、基本的には「素数列」を生成するのにリスト内包表記を使い、素因数分解を試すローカル関数でwhileを用いている。同じ素数で何回割れるか?と言うのを試すには、単純なループ構文の方が当然向いてるから、である。
なお、[[a, b], [c, d]]のようなリストを[a, b, c, d]にするのを通常flattenと呼ぶ。
Pythonではsum関数の第二引数に空リストを指定すると二次元リスト限定でflattenしてくれる。

練習問題 18
九九表(一の段~九の段)を表示するプログラムを作成しなさい。
※ printf(" %2d", x ); のように、%2d と記述すると表示が 2 桁に揃う。

def eighteen():
 [[print("{0:2} × {1:2} = {2:2} ".format(x[0], x[1], x[2]), end = "")\
  for x in k] and print() for k in [[(i, j, i * j) for i in range(1, 10)]\
               for j in range(1, 10)]]

解説: ちょっとしたパズル。まぁ、実際はここまでしつこくリスト内包表記を使わなくても良いが、練習問題としては丁度良いだろう。

練習問題 19
数値を繰り返して入力し、0 が入力されたら入力を止め、それまでの合計を表示するプログラムを作成しなさい。

解:
def nineteen():
 class Nineteen(object):
  def __init__(self):
   pass
  def __iter__(self):
   return self
  def __next__(self):
   val = float(input())
   if val == 0:
    raise StopIteration
   else:
    return val
 lst = [i for i in Nineteen()]
 print(sum(lst))

解説: 割に軽いしょーもない問題。
なお、クラス設計で、初期値で特に何も与える必要がないけど、構文的には何か置かなきゃならない、と言う場合、上で見るようにpassが使える。

練習問題 20
数値を繰り返して入力し、0 が入力されたら入力を止め、平均値を表示するプログラムを作成しなさい。
※ 計算は整数で行い、小数点以下は切り捨ててよい。
※ 最後に入力された 0 は平均に含めない。
※ 少なくとも 1 回は入力が行われるものとする。(最初に 0 を入力してはいけない)

解:

def twenty():
 from statistics import mean
 class Twenty(object):
  def __init__(self):
   pass
  def __iter__(self):
   return self
  def __next__(self):
   val = float(input())
   if val == 0:
    raise StopIteration
   else:
    return val
 lst = [i for i in Twenty()]
 print(mean(lst))

解説: ネタ的には19と同じ。

練習問題 21
サイズを示す数値を入力し、何等かの文字で例のような三角形を表示するプログラムを作成しなさい。
サイズ 4 の例
$
$$
$$$
$$$$

解:

def twentyOne(n):
 [print("$" * (i + 1)) for i in range(n)]

練習問題 22
サイズを示す数値を入力し、何等かの文字で、そのサイズの×印を表示するプログラムを作成しなさい。
サイズ 3 の例
X X
 X
X X
サイズ 4 の例
X  X
 XX
 XX
X  X
サイズ 5 の例
X   X
 X X
  X
 X X
X   X 

解:

def twentyTwo(n):
 [[print("{}".format("X" if i == j or n - i - 1 == j else " "), end = "")\
  for j in range(n)] and print() for i in range(n)]

解説: 単なるパズル、と言って良い。パズルを解く手間としてはforを使おうがリスト内包表記を使おうが大して変わらない。

練習問題 23
フィボナッチ数列を表示するプログラムを作成しなさい。
最初の2つの項を 0、1 とし、1000 まで( 1000 以下の項)を表示するものとします。
※          フィボナッチ数列:
 それぞれの項がその直前の2つの項の和になっている数列のこと。
         例:0, 1, 1, 2, 3, 5, 8, 13, 21, ...

解:

def twentyThree(a = 0, b = 1, n = 1000):
 class TwentyThree(object):
  def __init__(self, a, b, n):
   self.a = a
   self.b = b
   self.n = n
  def __iter__(self):
   return self
  def __next__(self):
   tmp = self.a
   self.a, self.b = self.b, self.a + self.b
   if tmp > self.n:
    raise StopIteration
   else:
    return tmp
     [print("{} ".format(i), end = "") for i in TwentyThree(a, b, n)]

さて、どうだろうか。23問もリスト内包表記で「反復」の練習問題を解けばいい加減慣れるとは思う。まずは形式には慣れるだろう。
23問中、リスト内包表記で書けなかった問題はたった1問、whileを使った解答も1問、であり、リスト内包表記は96%の「反復」で使えた、と言う事だ。whileをどうしても使わなければならなかったのは4%ってトコである。
ただし、かなりインチキがある。何故ならローカルクラスとしてイテレータを仕込んだ解答は正直なトコ、本来だったらリスト内包表記では解けない問題だから、だ。
そこで正確にカウントすると、23問中、リスト内包表記で簡単に解ける問題は16問で70%、反復じゃないと解けないのが35%ってトコだ。足して100%になってないのは「両方使った」解答があるから、だ。最初に宣言した8割には届かなかったが、まぁまぁの結果ではなかろうか。

ところで以前は「rangeを使うな」と言ったが結構使ってる。使わざるを得なかった理由というのは、これらが元々C言語用に作られた問題だから、である。つまりインデックスを多用せざるを得ないような問題構成になってる、って事がまずある。
そう、C言語のようにPythonを使う、と言うのは間違いだ、と言ったが、この辺で端的に現れてるだろう。もう問題自体がC言語前提のアレコレを含んでるのである。と言う事は、思考の道筋もプログラミング言語によって実は違う、と言う事が示唆されてるわけだ。
もっと言っちゃえばプログラミング言語こそが思考の道具なわけで、「どの言語を使うのか」が「考え方」に影響を与える、って事実がまずあるのだ。だからこそ「C言語が全ての基本」なんつーのは大間違いの大勘違いで、PythonとC言語だとプログラミングを組み立てる際の思考方法が丸っきり違うんだ、と言う事実をまずは認めないとならない。それが問題の形式に影を落としているんだ。
特に、C言語の問題は形式的に入力と出力を伴うモノが多く、実はそれはPythonに於いては物凄く無駄なのだ。Pythonはreturnすればいいし、inputprintも開発中は通常要らない。それはインタプリタを伴った開発だと結果はすぐ見れるし、特にprintなんて全然必要ない、と言う事なのだが、そういう便利な開発環境が無いC言語だとどうしてもprintfを使わないと開発中は暗中模索って事になってしまうんだな。この辺の「開発スタイル」と言うのもプログラミング言語の影響を受ける、と言うのが厳然たる事実なのだ。
今回は「表示せよ」と言うC言語の問題に合わせたが、繰り返すが通常はreturnしとけば良い。わざわざ表示させる必要は全くないのだ。
(だからこそ、以前言ったが、IDEを導入してShellしか出せないような設定にしてると開発に難がある、って事になるわけだ。Pythonインタプリタを走らせるのに手間がかかるようなら、IDLEでプログラミングした方が色々と初心者向けにはラクになる、と言うのはそういう事である。)

さて、もう一回言うが7割は反復はリスト内包表記が使える、と言う事は分かった。じゃあ、どういう時にリスト内包表記が使えない、あるいは使いづらいのだろうか。
まず、

  • リスト内包表記はシーケンスやイテラブルを受け取りリストを返す

と言う前提がある。もっと言うと、シーケンスやイテラブルを受け取り「加工する」と言う大前提がある、と言う事だ。
言い換えると

  • リスト内包表記はリスト自体を無から生成するには向かない
と言うのがポイント。上の23問見ても、ローカルなイテレータを使ってるのは何らかのシーケンス/イテラブルを「内部生成」する前提になってる。要するにリスト内包表記が受け取るべきシーケンス/イテラブルが「元々存在しない」と言う事だな。このテの問題はリスト内包表記で解くには向かないわけだ。
もっと言っちゃうと、上の23問は全部ある意味受け取るべきシーケンス/イテラブルが存在しない。存在しないが、range関数で簡単に作れる場合とそうじゃない場合に二分されてた、って事だ。
そして問題構成として、「受け取るべきシーケンス/イテラブルが存在しない」のはC言語だと当然だ、って事になる。なんせ可変長配列を自在に扱えない、そもそもそんなモノが存在しない、と言うのが前提だからだ(※3)。
逆に言うと、PythonだとCに比べると配列(と言うかリスト)をバンバン使って構わない環境になってる、と言う事だ。従って、Pythonに慣れれば慣れる程「Cで扱うのが億劫に思える」可変長配列を日常的に使うのが当たり前になるんで、上のCの問題が抱えるような事柄は忘却の彼方へと去っていくだろう。一々添字で考えるよりもリストの要素「まるごとそのまま」を対象にして考える事がフツーとなるから、だ。
いずれにせよ、ここではまず「Pythonに向かないような問題設定でも」高確率でリスト内包表記が使え、その幾分「無理矢理な解法」を試みる事によって強制的に「リスト内包表記に慣れさせる」事を主眼としている。

2. マッピング関数代わり

リスト内包表記の重要な側面が、そのマッピング関数とフィルタリング関数を統一的に扱う為の構文になってる、と言う事だ。
1. の練習問題はともかくとして、多分一番分かりやすい使い方と言えるだろう。

>>> [sum(i) for i in zip([1, 2, 3], [4, 5, 6])] # [1, 2, 3] と [4, 5, 6] の各要素を足し合わせる。
[5, 7, 9]
>>> [i * i for i in [1, 2, 3]] # [1, 2, 3] の各要素を2乗したリストを返す。
[1, 4, 9]
>>>

Python3.8以降からセイウチ構文と言うモノが導入された。それまではリスト内包表記で外部の変数の破壊的変更等は行えなかったが、セイウチ構文を使えばそう言った副作用目的のリスト内包表記が書けるようになった(つまり、この場合、リスト内包表記の返り値は重要じゃない、と言う事になる)。

# リストの要素の総和をsumを使わずに計算してみる試み
>>> s = 0
>>> [s := s + x for x in [1, 2, 3, 4]]
[1, 3, 6, 10]
>>> s
10
>>>

もっとも、このセイウチ構文はPythonista内でも賛否両論で、この激論に疲れ果てたPythonの生みの親(グイド・ヴァン・ロッサム)は自ら作ったPythonコミュニティを去ってしまった。
親殺しのセイウチ、と覚えておこう(謎)。

3. フィルタリング関数代わり

これもこのブログではある意味既にお馴染みなのだが、一応紹介しておこう。

# リストから負の整数をフィルタリングする
>>> [i for i in [1, 2, -3, -4, 5] if i > 0]
[1, 2, 5]
>>>

リスト内包表記と条件式の組み合わせについては色々なトコ・・・主にQiitaか(笑)、で指南されてたりするんで、適宜検索してみよう。この辺のトピックに関してはWebページは事欠かない。

4. FizzBuzz問題

最後に10年前くらいに流行ったFizzBuzz問題を見てみよう。

 FizzBuzz問題では、1から何らかの値までを数え上げて(画面などに出力して)いく。ただし、その際には以下の条件がある。
  • その数が3で割り切れる場合には、その数の代わりに「Fizz」を出力する
  • その数が5で割り切れる場合には、その数の代わりに「Buzz」を出力する
  • その数が3でも5でも割り切れる場合には、その数の代わりに「FizzBuzz」を出力する

 1から15の数え上げであれば、出力は「1→2→Fizz→4→Buzz→Fizz→7→8→Fizz→Buzz→11→Fizz→13→14→FizzBuzz」のようになる

「データ(数値のリスト)を加工する」と言う意味だと、リスト内包表記の為にあるような問題である。1〜100の整数列にFizzBuzzを適用するには次のようにして書けば良い。

>>> ["FizzBuzz" if i % 15 == 0 else "Buzz" if i % 5 == 0 else "Fizz" if i % 3 == 0 else i for i in range(1, 101)]
[1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19, 'Buzz', 'Fizz', 22, 23, 'Fizz', 'Buzz', 26, 'Fizz', 28, 29, 'FizzBuzz', 31, 32, 'Fizz', 34, 'Buzz', 'Fizz', 37, 38, 'Fizz', 'Buzz', 41, 'Fizz', 43, 44, 'FizzBuzz', 46, 47, 'Fizz', 49, 'Buzz', 'Fizz', 52, 53, 'Fizz', 'Buzz', 56, 'Fizz', 58, 59, 'FizzBuzz', 61, 62, 'Fizz', 64, 'Buzz', 'Fizz', 67, 68, 'Fizz', 'Buzz', 71, 'Fizz', 73, 74, 'FizzBuzz', 76, 77, 'Fizz', 79, 'Buzz', 'Fizz', 82, 83, 'Fizz', 'Buzz', 86, 'Fizz', 88, 89, 'FizzBuzz', 91, 92, 'Fizz', 94, 'Buzz', 'Fizz', 97, 98, 'Fizz', 'Buzz']
>>>

これもこのように、C言語と違って、Pythonだったら出力(print)は考えなくって良い。リストの返り値さえあればあとでどのようにも出力可能だし、加えると、C言語だと配列丸ごと出力する仕組みがないが、Pythonはリスト丸ごと印字が可能である。
どうしても、ってぇのなら以下のようにしてもいいが、どの道Pythonでは本質的な問題にはならないのだ。

>>> [print("{}".format("FizzBuzz" if i % 15 == 0 else "Buzz" if i % 5 == 0 else "Fizz" if i % 3 == 0 else i)) for i in range(1, 101)]

返り値がNoneだらけのリストになるが、それはprintの返り値がNoneだからだ。
従って、返り値を利用して何かしよう、って場合、「出力命令が邪魔になる」と言う事が分かるだろう。C言語的なプログラミング書法はPythonだと邪魔になる、と言う好例だろう。

と言うわけで、一応、リスト内包表記の話は終わり、である。

※1: Python3.0以降だと他に辞書内包表記や集合内包表記まで出てきたんで、「内包表記」の適用範囲は広がる一方だ。
とは言っても、いずれにせよ、基本は「リスト内包表記」である。
※2: この練習問題はこのページから借りてきた。
※3: 2021年時点でのCの国際規格では可変長配列は「ある」がオプショナル扱いになってて、gcc/clangでは扱えるがマイクロソフトのコンパイラでは使えない。JIS規格では可変長配列は完全に「ある」が、一方、マイクロソフトのコンパイラは国際最新仕様には準じてるがJIS規格には準じてない、と言うややこしい事になってて、いずれにせよ、Cでの可変長配列は扱いづらいのが現状である。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事