「Pythonプログラミング入門」は、東大・京大のプログラミングの授業でも使用されており、無料で読める、東大/京大の「Python教科書」電子書籍で公開されています。
この記事では、CHAPTER 6「文字列(string)」の問題について説明します。
この連載シリーズでは、教科書に載っている模範解答を解説し、さらに良い解法がある場合には、別解としていくつか紹介しています。練習問題を解いてみて、「自分の回答が最適解なのか不安だ」という方にも参考になると思います。
教科書のHTML版やPDF版のリンクも紹介していますので、ぜひご活用ください。(この記事ではPDF版の章立てを基準にしています)
教材リンクはこちら↓
目次
6.8.2 練習 remove_punctuations
問題
英語の文章からなる文字列 str_engsentences が引数として与えられたとき、str_engsentences 中に含まれる全ての句読点(., ,, :, ;, !, ?)を削除した文字列を返す関数 remove_punctuations を作成してください。 (練習の解答はこのノートブックの一番最後にあります。)
次のセルの … のところを書き換えて remove_punctuations(str_engsentences) を作成してください。
6.8.2 解答
Pythonの文字列メソッド str.replace を使って順番に文字を削除していくオーソドックスな手段です。
削除対象の文字が増えるたびにソースコードの行数が増えていくデメリットがありますが、単純なケースであればこの方法でも問題ないと思います。
def remove_punctuations(str_engsentences):
str1 = str_engsentences.replace('.', '') # 指定の文字を空文字に置換
str1 = str1.replace(',', '')
str1 = str1.replace(':', '')
str1 = str1.replace(';', '')
str1 = str1.replace('!', '')
str1 = str1.replace('?', '')
return str1
別解1
やっていることは模範解答と同じですが、同じ処理の繰り返しをループにしているので、少しコードがきれいになっていると思います。
def remove_punctuations(str_engsentences):
remove_chars = ".,:!?"
for remove_c in remove_chars:
str_engsentences = str_engsentences.replace(remove_c, '')
return str_engsentences
少し解説すると、remove_chars には削除したいすべての文字が含まれていて、
for ループでその文字列の文字を一つずつ順番に取得してreplaceする処理になっています。
別解2
str.maketrans と str.translate を組み合わせた回答です。
maketrans で変換テーブルを作成して、translate で一度にすべて変換します。
やっている処理の意図が伝わりやすく良い方法だと思います。
def remove_punctuations(str_engsentences):
replace_table = str.maketrans({
".": None,
",": None,
":": None,
"!": None,
"?": None
})
return str_engsentences.translate(replace_table)
別解3
正規表現操作モジュールである reモジュールのimportが必要になりますが、以下のような方法もあります。
削除対象が正規表現で表せる場合には、ソースコードがシンプルになって良いと思います。
import re
def remove_punctuations(str_engsentences):
# 正規表現を使って変換
return re.sub("[.,:!?]", '', str_engsentences)
メモ
正規表現とは、プログラミングをする時にはよく出てきますが「文字列の集合を一つの文字列で表現する方法」のことです。
色々な記法がありますが(Wikipedia)、例えば
[0-9] と書けば、[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
の集合を表しますし、
[A-Z] と書けば、[A, B, C ... X, Y, Z]
の集合のことを表します。
6.8.3 練習 atgc_bppair
問題
ATGC の 4 種類の文字から成る文字列 str_atgc が引数として与えられたとき、文字列 str_pair を返す関数 atgc_bppair を作成してください。
ただし、str_pair は、str_atgc 中の各文字列に対して、 A を T に、T を A に、G を C に、C を G に置き換えたものです。
次のセルの … のところを書き換えて atgc_bppair(str_atgc) を作成してください。
6.8.3 解答
模範解答では、一度変換対象の小文字に変換してから、最後に str.upper で大文字に変換しています。
最も自然なアプローチで、実際にこのような問題が出たら私も同様のやり方を使うと思います。
自然で合理的なアプローチである方が、読み手にも意図が伝わりやすく良いコードになる場合が多いです。
def atgc_bppair(str_atgc):
str_pair = str_atgc.replace('A', 't')# 指定の文字に置換。ただし全て小文字
str_pair = str_pair.replace('T', 'a')
str_pair = str_pair.replace('G', 'c')
str_pair = str_pair.replace('C', 'g')
str_pair = str_pair.upper() # 置換済みの小文字の列を大文字に変換
return str_pair
この問題の落とし穴は、A → T の様に直接大文字に変換してしまうと、次の処理の T → A の変換処理で再度変換した文字が変換されてしまうことです。
変換済みの文字が以降の処理で変換対象にならないように、小文字に変換してから最後に大文字にするのは常用のテクニックです。
別解1
もちろん、模範解答のように小文字に変換しなくても変換する手段はあります。
以下のコードのように、関係ない文字に一旦変換してから対象の文字に戻すという方法でも問題ありません。
ただし、やや冗長なソースコードとなります。
def atgc_bppair(str_atgc):
str_pair = str_atgc
str_pair = str_pair.replace('A', '@')
str_pair = str_pair.replace('T', '!')
str_pair = str_pair.replace('G', '$')
str_pair = str_pair.replace('C', '#')
str_pair = str_pair.replace('@', 'T')
str_pair = str_pair.replace('!', 'A')
str_pair = str_pair.replace('$', 'C')
str_pair = str_pair.replace('#', 'G')
return str_pair
別解2
ここでも6.8.2の問題と同様に、str.maketrans と str.translate を使うことができます。
やっていることの意図が読み手に伝わりやすいので、非常に分かりやすいと思います。
Pythonでやるなら、この方法が一番良いのではないかと思います。
def atgc_bppair(str_atgc):
replace_table = str.maketrans({
"A": "T",
"T": "A",
"G": "C",
"C": "G"
})
return str_atgc.translate(replace_table)
6.8.5 練習 swap_colon
問題
コロン (:) を 1 つだけ含む文字列 str1 を引数として与えると、コロンの左右に存在する文字列を入れ替えた文字列を返す関数 swap_colon(str1) を作成してください。
次のセルの … のところを書き換えて swap_colon(str1) を作成してください。
6.8.5 解答
模範解答は、str.index でコロンの位置(index)を特定して、その位置を使って文字列をスライスしています。
特定の文字の位置を探し出して、部分文字列に分割する処理はよく出てくる定番の処理ですが、このような場合にスライスを使うのはオススメ出来ません。
indexが表している位置が分かりにくく、しばしばバグの温床となり得ます。
この辺りは弊ブログの以下の記事でも詳しく解説しています。(Javaの例ですが、全く同じ問題を抱えています)
-
substring を使わない方が良い3つの理由 [Java]
この記事では、Javaにおけるsubstringの闇を暴きます。 非常に罠が多いにも関わらず、初心者用の解説サイトでも出現頻度が高い有名なメソッドです。しかし、この難しいメソッドの使い方をわざわざ覚え ...
続きを見る
実践では別解のような処理を書くことをオススメします。
def swap_colon(str1):
#コロンの位置を取得する # find でも OK
col_index = str1.index(':')
#コロンの位置を基準に前半と後半の部分文字列を取得する
str2, str3 = str1[:col_index], str1[col_index+1:]
#部分文字列の順序を入れ替えて結合する
str4 = str3 + ':' + str2
return str4
別解1
コロンで分割するのに、str.split を使います。
indexの位置を考慮しなくて良くなるため、格段に可読性が良くなると思います。
str.splitの第2引数は最大分割回数で、今回の問題では指定しなくても問題ありませんが、コロンが複数個含まれる文字列の場合に意図しない動作をさせないために指定しています。
splitについては以下の記事もご覧ください。
Pythonで文字列を分割(区切り文字、改行、正規表現、文字数) | note.nkmk.me
def swap_colon(str1):
first, second = str1.split(':', 2)
return second + ':' + first
別解2
入力str1をsplit(分割)して、reverse(反転)して、join(連結)するという、とても宣言的な書き方で、処理内容の理解がしやすいコードです。
def swap_colon(str1):
split_str = str1.split(':')
split_str.reverse()
return ':'.join(split_str)
6.8.7 練習 atgc_count
問題
ATGC の 4 種類の文字から成る文字列 str_atgc と塩基名(A, T, G, C のいずれか)を指定する文字列 str_bpname が引数として与えられたとき、str_atgc 中に含まれる塩基 str_bpname の数を返す関数 atgc_count を作成してください。
次のセルの … のところを書き換えて atgc_count(str_atgc, str_bpname) を作成してください。
6.8.7 解答
模範解答は完璧です。
これ以上簡単には出来ないのでこのコードが最善です。
def atgc_count(str_atgc, str_bpname):
return str_atgc.count(str_bpname)
別解
str.count と同じ処理を敢えて自分で書くと以下の様になります。
文字列を文字ごとに分割して、str_bpnameと一致した数をカウントして返します。
def atgc_count(str_atgc, str_bpname):
count = 0
for c in str_atgc:
if (c == str_bpname):
count += 1
return count
6.10 練習 check_lower
問題
英語の文字列 str_engsentences が引数として与えられたとき、それが全て小文字である場合、True を返し、そうでない場合、 False を返す関数 check_lower を作成してください。
次のセルの … のところを書き換えて check_lower(str_engsentences) を作成してください。
6.10 解答
模範解答はほぼ完璧だと思います。
元の文字列と str.lower した文字列が一致して入れば True、一致しなければ Falseとしています。
元の文字がすべて小文字であれば lower() した文字列は元の文字列から変化がないという性質を利用しています。
def check_lower(str_engsentences):
if str_engsentences == str_engsentences.lower():#元の文字列と小文字に変換した文字列を比較する
return True
return False
別解1
模範解答をもっと短くすることも出来ます。
意味はほぼ同じですが、if文が無い分だけよりシンプルになっています。
def check_lower(str_engsentences):
return str_engsentences == str_engsentences.lower()
別解2
あまりオススメはしませんが、以下の様に処理する方法もあります。
文字列を文字ごとに分割して、その文字がA〜Zの間の文字(=英大文字)であるかを判定しています。
大文字が見つかれば Falseを返します。
def check_lower(str_engsentences):
for c in str_engsentences:
if ('A' <= c and c <= 'Z'):
# 大文字があればFalseにする
return False
return True
6.12 練習 remove_clause
問題
コンマ (,) を含む英語の文章からなる文字列 str_engsentences が引数として与えられたとき、str_engsentences 中の一番最初のコンマより後の文章のみかならなる文字列 str_res を返す関数 remove_clause を作成してください。ただし、 str_res の先頭は大文字のアルファベットとしてください。
次のセルの … のところを書き換えて remove_clause(str_engsentences) を作成してください。
6.12 解答
この模範解答は明確に間違っています。
int_indexに+2していることから分かるように、コロンの後にスペースが含まれていることが前提のコードになっていますが、スペースが含まれていることは問題文に記載がないため誤った解答です。
プログラムの中に 2 などの意味が明確でない数値(マジックナンバー)が出てきた時は要注意です。
書いた本人しか意味が分からない可能性があり、コードを読む人が非常に苦労します。
#6.8.5 練習 swap_colon の項目でも書きましたが、そもそも index や find をなるべく使わないようにするのが、分かりやすいコードを書くためのコツです。
def remove_clause(str_engsentences):
int_index = str_engsentences.find(',')
str1 = str_engsentences[int_index+2:]
return str1.capitalize()
別解
find や 文字列のスライスに頼らなくても処理をすることが可能です。
分割した文字列の先頭にスペースが含まれていることがあり得るのなら、str.lstrip を使って文字を除去した方が良いです。
lstripなら、文字列の先頭にスペースがある場合は除去され、ない場合は何も起きません。
文字列の先頭文字を大文字にするのは str.capitalize を使います。
def remove_clause(str_engsentences):
str = str_engsentences.split(',', 2)[-1] # splitの最後の要素のみ取得
return str.lstrip().capitalize() # lstripで文字列先頭の空白を削除 (left strip)
参考
- Pythonプログラミング入門 — Pythonプログラミング入門 documentation (utokyo-ipp.github.io)
- PDF版 Pythonプログラミング入門 (utokyo-ipp.github.io)
- Colaboratory へようこそ - Colaboratory (google.com)
- 無料で読める、東大/京大の「Python教科書」電子書籍:AI・機械学習の無料電子書籍 - @IT (itmedia.co.jp)
- 組み込み型 — Python 3.11.3 ドキュメント
前のCHAPTERはこちら
-
【教科書解説】Pythonプログラミング入門 (CHAPTER 4 論理・比較演算と条件分岐の基礎) 練習問題
東大/京大のプログラミングの授業でも使われている(無料で読める、東大/京大の「Python教科書」電子書籍)、「Pythonプログラミング入門」の練習問題の解説を連載しています。 この記事では、CHA ...
続きを見る
次のCHAPTERはこちら
-
【教科書解説】Pythonプログラミング入門 (CHAPTER 7 リスト (list)) 練習問題
「Pythonプログラミング入門」は、東大・京大のプログラミングの授業でも使用されており、無料で読める、東大/京大の「Python教科書」電子書籍で公開されています。 この記事では、CHAPTER 7 ...
続きを見る