LoginSignup
17
34

More than 3 years have passed since last update.

言語処理100本ノックでPythonに入門

Last updated at Posted at 2020-01-19

この記事は、C言語は少しわかるけどPythonはほぼ知らない状態で、Pythonによる自然言語処理を始めたい人向けです。自然言語処理の入門として名高い言語処理100本ノック2015の第1章を解けるようになる最短ルートを目指しています。
(4/8 追記 2020年版も第1章は同じです)

100本ノックの解答例自体はこのQiitaでも既に多くの記事がありますが、解説はさほど充実しておらずPython初学者には大変かと思い、本記事を執筆いたしました。

Pythonは公式のドキュメントがかなり親切で、チュートリアルを読めば自力で勉強できるとは思いますが、本記事では100本ノックを解くのに必要な事項だけ触っていきたいと思います。

インストール

頑張りましょう。MacOSなら$ brew install python3、Ubuntuなら$ sudo apt install python3.7 python3.7-dev

素のWindowsではPythonインストール(Win10)編などを参考にするのが簡単と思われます。

(Google Colaboratoryとかでも良いかもしれません。)

対話モードでコードを実行する

コマンドラインで$ python3とか$ python3.7などというコマンドを打ってPython3が起動できればOKです。これでPythonが対話モードで実行できる状態になります。このモードでは何か式を入力してEnterを押すと式の評価結果が返ってきます

>>> 1+2
3

Pythonの特徴の一つに「動的型付け」というものがあります。Cとは異なり、変数の型を宣言する必要は無いですし、整数(int)型が浮動小数点数(float)型になったりします。

>>> a = 1
>>> a = 5/2
>>> a
2.5

組み込み型

まずはPythonの標準で使える型(組み込み型)にはどういうものがあるのか見ていきましょう。上の例で出てきたintやfloatといった数値型もその一つです。100本ノック第一章では次に挙げる型だけ知っていれば解けます。
- 文字列(テキストシーケンス)
- リスト
- 集合
- 辞書(マッピング)

文字列(str)型

言語処理をするのでまず文字列型からやっていきます。Pythonで文字列を記述するには'"で囲うだけです!日本語もばっちりです。文字列同士は簡単に結合させることがきます。

>>> "ようこそ"
'ようこそ'
>>> 'hoge' + 'fuga'
'hogefuga'
  • Cの配列のように、添え字でアクセスできます
  • 負の添え字を使うと文字列の後ろからアクセスできます
>>> word = 'Python'
>>> word[0]
'P'
>>> word[-1]
'n'
  • 「スライス」を使うと部分文字列を簡単に取得できます!
    • word[i:j]i番目以上j番目未満の要素を取得
    • iまたはjを省略すると「端」という意味になる
>>> word[1:4]
'yth'
>>> word[2:-1]
'tho'
>>> word[:2]
'Py'
>>> word[2:]
'thon'
  • word[i:j:k]i番目以上j番目未満の要素をk個毎に取得できます
>>> word[1:5:2]
'yh'
>>> word[::2]
'Pto'
>>> word[::-2]
'nhy'

100本ノック00, 01

スライスを駆使してやってみましょう。

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

以下、解答例です。

nlp00.py
word = 'stressed'
word[::-1]
'desserts'

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

以下、解答例です。

nlp01.py
word = 'パタトクカシー'
word[::2]
'パトカー'

リスト(list)型

リスト型はCで習った配列のパワーアップ版だと思えば大丈夫です。次のように記述します。

squares = [1, 4, 9, 16, 25]

空のリストは次のように書きます。

empty = []

リスト型は文字列型と同じように添え字アクセスやスライスができます。このような組み込み型をまとめてシーケンス型と呼んだりします。

>>> squares[:3]
[1, 4, 9]

メソッド

Pythonでは、各々のデータ型専用の関数というものがあり、それらをメソッドと呼びます。
例えば、リスト型がもつappendメソッドはリストに要素を追加します。呼び出すには次のようにリスト.append()と書きます。

>>> squares.append(36)
>>> squares
[1, 4, 9, 16, 25, 36]

文字列型のメソッドもいくつか紹介します。
- x.split(sep):文字列xsepで区切ることでリストを作ります。
- sep.join(list): listの要素をsepで結合した文字列を作ります
- x.strip(chars):文字列の両端からcharsを削った文字列を返します
- x.rstrip(chars):文字列の右端からcharsを削った文字列を返します

split(), strip(), rstrip()の引数を省略したときは「あらゆる空白文字」という意味になります

>>> 'I have a pen.'.split(' ')
['I', 'have', 'a', 'pen.']
>>> ' '.join(['I', 'have', 'a', 'pen.'])
'I have a pen.'

join()がリスト型ではなく文字列型のメソッドなのは若干覚えにくいかもしれません。stack overflowを要約すると、リストの各要素が文字列になっていないと適用不可能だから、とのことです。

>>> 'ehoge'.strip('e')
'hog'
>>> 'ehoge'.rstrip('e')
'ehog'
>>> 'ehoge'.rstrip('eg') # 右端からeまたはgをできるだけ削る、という意味
'eho'

for文

リストを理解したところで、何かの処理を繰り返すために使うfor文を扱います。
例えばCで配列の要素の総和を求めるときは次のように書いていました。

int i;
int squares[6] = {1, 4, 6, 16, 25, 36};
int total = 0;
for(i = 0; i < 6; i++) {
    total += squares[i];
}

Pythonのfor文はこうなります。

total = 0
for square in squares:
    total += square

各ループでsquaresの要素が1個ずつ取り出されてsquareに代入されます。つまり、square=1, square=4,,,という具合です。
for文を形式化するとこんな感じになります。

for 要素を表す変数 in リストとか:
TAB 処理内容

inの直後を「リストとか」というように誤魔化していますが、文字列型でもOKです(各文字が文字列の要素とみなせるからです)。for文のinの直後に置けるものの総称はイテラブル(オブジェクト)(iterable)といいます。

なお、インデントはCでは任意でしたがPythonでは強制です!

組み込み関数

print()

実際にforループ中の変数の値を見てみましょう。対話モードではforブロック中の変数などを評価して値を表示してくれたりしないので、print()関数を使います。引数は文字列型でなくなても標準出力してくれます。Cでいうところの#includeみたいなことをせずに使うことができます。このような関数は組み込み関数といいます。

for square in squares:
    print(square)

1
4
9
16
25
36

このように、Pythonのprint()関数は(デフォルトでは)自動で改行してくれます。
改行を防ぐときは次のようにオプション引数endを指定します。

for square in squares:
    print(square, end=' ')

1 4 9 16 25 36 

len()

便利な組み込み関数を紹介していきます。len()はリストや文字列などの長さを返します。

>>> len(squares)
6
>>> len('あいうえお')
5

range()

range()は一般にfor文をn回まわしたいときに使います。

for i in range(3):
    print(i)
0
1
2

※ range()の戻り値はrange型と呼ばれるもので、シーケンス型の一種です。しかし使い道はfor文かリスト型などに変換して使うくらいです。

>>> range(3)
range(0, 3)
>>> list(range(4))
[0, 1, 2, 3]

100本ノック02

このあたりでソースコードをファイルに保存して実行する方法もやってみましょう。
例えばprint('Hello World')とだけ書いたファイルをhello.pyという名前で保存し、コマンドラインで$ python3 hello.pyと打つと'Hello World'と出力されるはずです。
といったところで100本ノックの続きをやってみましょう。

「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ

これまで学んだ範囲でやってみましょう。以下、解答例です。

nlp02.py
str_a = 'パトカー'
str_b = 'タクシー'
for i in range(len(str_a)):
    print(str_a[i]+str_b[i], end='')

パタトクカシーー

こんなとき、組み込み関数zip()を使えばもっと簡単に書けます。この関数は引数のイテラブルオブジェクトからi番目の要素を組にしてくれます。

>>> list(zip(str_a, str_b))
[('パ', 'タ'), ('ト', 'ク'), ('カ', 'シ'), ('ー', 'ー')]

よって上のコードは次のように書き換えられます。とてもよく使う関数です。

for a, b in zip(str_a, str_b):
    print(a+b, end='')
パタトクカシーー

ちなみに、ファイルフォーマットのZIPとは関係なく、zipper`(ファスナー)を締めるという意味だと思われます。

タプル(tuple)型

上の説明を見て「このカンマはなんだ?」と思わなかった人はこの節を飛ばして問03を解きましょう。

このzip()が各ループで取り出しているオブジェクトはstr_a[i]str_b[i]を要素とするリストみたいなものです。正しくは、タプルというもので、リストの変更不能版です。

タプルはシーケンスの1種なので添え字アクセスはできますがappend()などで要素を追加することはできません。Pythonでは変更可能なものをミュータブル、変更不能なものをイミュータブルといいます。これまでスルーしてきましたが、リストはミュータブルで、文字列やタプルはイミュータブルです。

>>> a = 'abc'
>>> a[1] = 'a'
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-56-ae17b2fd35d6> in <module>
      1 a = 'abc'
----> 2 a[1] = 'a'


TypeError: 'str' object does not support item assignment
>>> a = [0, 9, 2]
>>> a[1] = 1
>>> a
[0, 1, 2]

タプルは()で囲んで記述しますが、通常は外側の()は不要です(関数の引数を書く()の内側などでは省略不可です)。

>>> 1, 2, 3
(1, 2, 3)

タプルのパックとシーケンスのアンパックという代入方法

タプルの()を省略して記述できる仕様を利用して、,区切りの複数の値をまとめて1つの変数に代入することをタプルのパックといいます。対照的に、1つのシーケンスを複数の変数にまとめて代入することをシーケンスのアンパックといいます。

>>> a = 'a'
>>> b = 'b'
>>> t = a, b
>>> t
('a', 'b')
>>> x, y = t
>>> print(x)
>>> print(y)
a
b

横道が長くなってしまいましたが、これでfor a, b in zip(str_a, str_b):の正体がわかりました。各ループでzip関数が返すタプルをa,bという変数にアンパックしているというわけです。

100本ノック03

やってみましょう。

"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.

この問題、ピリオドとカンマはアルファベットではないので除去しないと円周率になりません。

以下、解答例です。

nlp03.py
sent = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
words = sent.split()
ans = []
for word in words:
    ans.append(len(word.rstrip('.,')))
print(ans)
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

もっと良い書き方が実はあるのですが(リスト内包表記)、現段階では見なかったことにして大丈夫です。


ans = [len(word.rstrip('.,')) for word in words]

辞書(dict)型

文字列やリストなどでは、要素にアクセスするのに添え字、すなわちある範囲の整数を使っていましたが、辞書型は自分が定義した「キー」でアクセスすることができます。例えば、ある単語の出現回数を記憶させたいので、キーを単語、値を出現回数とした辞書を作る、という用途があります。辞書オブジェクトは次のように定義して値を取り出します。また作った辞書にキーと値のペアを簡単に追加できます。

>>> dic = {'I':141, 'you':112}
>>> dic
{'I': 141, 'you': 112}
>>> dic['I']
141
>>> dic['have'] = 256
>>> dic
{'I': 141, 'you': 112, 'have': 256}

※ キーにできるのは「ハッシュ可能」な型と言われますが、純粋なイミュータブルオブジェクト(文字列とかタプルとか)であればなんでも大丈夫です。

ブール(bool)型

等式や不等式などが評価されるとTrueかFalseが返ってきます。

>>> 1 == 1
True
>>> 1 == 2
False
>>> 1 < 2 <= 3
True

notでbool値を反転することができます。

in演算

forを伴わないinは所属関係を判定します。

>>> 1 in [1, 2, 3]
True

in演算が定義されているものはコンテナ型と呼ばれます。文字列もリストもタプルも辞書も全てコンテナ型の一種です。

if文

Pythonのif文はCとそっくりですが、else ifではなくelifであることに注意しましょう。

if 1==2:
    print('hoge')
elif 1==3:
    print('fuga')
else:
    print('bar')
bar

100本ノック04

ここまで学んだことを駆使してやってみましょう。

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

以下、解答例です。

nlp04.py
sent = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
positions = [1, 5, 6, 7, 8, 9, 15, 16, 19]
words = sent.split()

i = 1
ans = {}
for word in words:
    if i in positions:
        key = word[0]
    else:
        key = word[:2]
    ans[key] = i
    i += 1

print(ans)
{'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}

(何、Miなんて元素記号は無いですって?そもそも水平リーベ僕の船の英語版はMgが鬼門のようで…)

組み込み関数のenumerate()を使うとループ回数をくっつけてくれるので、もっと簡単に書けます。普通に使うと0から始まるので、オプション引数start=1を指定しています。

ans = {}
for i, word in enumerate(words, start=1):
    if i in where:
        key = word[0]
    else:
        key = word[:2]
    ans[key] = i

enumerate()はこのようにとても便利でよく使うので是非知っておきましょう。実は03番でもenumerate()を使ってwordsの要素を変更していくという方法があります(新たなリストansを生成する手間が省けます)。

関数定義、100本ノック05

次は100本ノック05をやってみましょう。

与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

pythonで自作の関数を定義したいときは次のように書けばOKです。

def function_name(argument):
TAB 処理

出力形式は指定されていないですが、例えば次のような出力をするコードを書いてみましょう。
[['I', 'am'], ['am', 'an], ['an', 'NLPer']]
シーケンスとnを引数とする関数を作れば大丈夫そうです。

以下、解答例です。

def ngram(seq, n):
    lis = []
    for i in range(len(seq) - n + 1):
        lis.append(seq[i:i+n])
    return lis

ここまでできた人は、nlp05.pyという名前のファイルに次のようなこと書いて保存し、コマンドラインで$ python3 nlp05.pyと実行しましょう。

nlp05.py
def ngram(seq, n):
    lis = []
    for i in range(len(seq) - n + 1):
        lis.append(seq[i:i+n])
    return lis


if __name__ == '__main__':
    sent = 'I am an NLPer'
    words = sent.split(' ')
    lis = ngram(words, 2)
    print(lis)
    lis = ngram(sent, 2)
    print(lis)

if __name__ == '__main__':の部分は次の問題でインポートをするので必要です。疑問に思う人はいったんこの行を書かないでおくと良いと思います。

また、ngram関数は次のような別解があり、この方が高速なのですが説明が少々長くなるので省略します。

def ngram(seq, n):
    return list(zip(*(seq[i:] for i in range(n))))

集合(set)型、100本ノック06

次は100本ノック06です。

"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

とりあえず次のようなコードを動かしてみましょう。

from nlp05 import ngram

x = set(ngram('paraparaparadise', 2))
print(x)
{'is', 'se', 'di', 'ap', 'ad', 'ar', 'pa', 'ra'}

from nlp05 import ngramの部分で先ほど作ったngram関数を「インポート」しています。

set型は手でx = {'pa', 'ar'}と定義することもできますが、他のイテラブルオブジェクトからset()を使ってつくることもあります。

和集合は|、積集合は&、差集合は-で求めることができます。

以下、解答例です。

nlp06.py
from nlp05 import ngram

x = set(ngram('paraparaparadise', 2))
y = set(ngram('paragraph', 2))
print(x | y)
print(x & y)
print(x - y)
print(y - x)
print('se' in x)
print('se' in y)
{'is', 'se', 'di', 'ph', 'ap', 'ag', 'ad', 'ar', 'pa', 'gr', 'ra'}
{'ra', 'ap', 'pa', 'ar'}
{'is', 'se', 'ad', 'di'}
{'ag', 'gr', 'ph'}
True
False

このin演算、問題04のようにリストに適用するとO(n)かかるので、できれば集合型に使いましょう。

文字列書式設定、100本ノック07

100本ノック07を解きます。

引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

これを文字列同士を+で結合しまくる方法でやるのは大変です。Pythonでは3つの方法があります。

printf 形式

>>> x = 12
>>> y = '気温'
>>> '%d時の%s' % (x, y)
'12時の気温'

str.format() (Python2.6以降)

>>> '{}時の{}'.format(x, y)
'12時の気温'

f-string (Python3.6以降)

>>> f'{x}時の{y}'
'12時の気温'

どれを使えばええねん

基本的にはf-stringが一番楽。ただし{}の中でバックスラッシュは使えない仕様です。また、桁数を指定する場合はprintf形式が高速らしいです(参考
これで07番は特に迷うことなくできるでしょう。

100本ノック08, 09

08は文字とコードポイントの変換をやってくれる組み込み関数ord()chr()を使えばできます。小文字の判定はコードポイントを使っても良いですしstr.islower()でも良いでしょう。コードポイントとはコンピュータ上における文字のインデックスみたいなもので、バイト列をコードポイントに変換することをデコード、その逆をエンコードと言います、それらの対応関係を定めたものを文字コードと言います、みたいな話は知っておいて良いかもしれません。

09はrandomモジュールのshuffle()sample()を使えばできます。shuffle()は元データを破壊するメソッドであり戻り値は無い、ということに気をつけましょう。もちろんインポートは忘れずに。

おわりに

これでPythonの基本を押さえることができたはずです。本当は内包表記、イテレータ、ジェネレータ、ラムダ式、引数リストのアンパック、map()あたりの内容も扱いたかったのですが、断腸の思いで割愛しました。興味のある人は調べてみて下さい。

(4/25追記)
【第2章】言語処理100本ノックでPythonに入門を公開しました!第2章の問題を使ってファイル入出力、Unixコマンドに加え、上記の扱いたかった内容の一部について解説してます。

17
34
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
34