第4章
文字列
Mojo の文字列は UTF-8 で保存されます。そのため len() も s[0] も Python のようには使えません。長さ・添字・スライス・メソッド・数値との変換・書式づくりを、実際のエラーを見ながら身につけます。
この章の内容
この章では、Mojo の文字列(String)の使い方を学びます。+ でつなぐ、メソッドを呼ぶ、数値と相互に変換するといった基本は Python とよく似ています。ところが、len(s) や s[0] はそのままでは書けません。理由は、Mojo の文字列が UTF-8 のバイト列として保存されているからです。その仕組みと、代わりに使う書き方を、日本語の文字列を使って確かめます。
この章を終えると、次のことができるようになります。
- 文字列リテラルと
Stringの違いを説明できる - 文字列を連結し、くり返し、比べられる
- バイト数・コードポイント数・文字数(書記素クラスタ数)を使い分けて長さを数えられる
s[byte=i]、s[codepoint=i]などで添字・スライスを書けるupper・split・find・replaceなどの主なメソッドを使える- 数値と文字列を相互に変換し、変換の失敗を
try/exceptで受け止められる String(...)とテンプレート文字列(t"...")、formatで文字列を組み立てられる- 複数行の文字列とエスケープシーケンスを書ける
4.1文字列リテラルと String
まず、文字列を作って表示する小さなプログラムから始めます。
def main():
var greeting = "Hello, Mojo!"
var name: String = "World"
print(greeting)
print("Hello, " + name + "!")~$ mojo run basics.mojo Hello, Mojo! Hello, World!
"Hello, Mojo!" のように引用符で囲んだ部分を 文字列リテラル と呼びます。リテラルそのものは、コンパイルのときに内容が決まっている特別な型(StringLiteral)です。ところが、var greeting = "..." のように変数に入れると、String 型になります。String は、実行中に長さを変えたり、メソッドで加工したりできる、ふつうの文字列の型です。
型が本当に String になっているか、わざと間違えて確かめてみましょう。String の変数を Int の変数に入れてみます。
def main():
var greeting = "Hello, Mojo!"
var n: Int = greeting
print(n)~$ mojo run type_check.mojo /home/user/type_check.mojo:3:18: error: cannot implicitly convert 'String' value to 'Int' var n: Int = greeting ^~~~~~~~ mojo: error: failed to parse the provided Mojo source module
メッセージに 'String' value とあるとおり、greeting は String です。普段はリテラルと String の違いを意識する必要はありません。意識するのは、リテラルをそのまま使うと、メソッドが足りない ことがあるときです。
def main():
print("hello".replace("l", "L"))~$ mojo run literal_method.mojo /home/user/literal_method.mojo:2:18: error: 'StringLiteral["hello"]' value has no attribute 'replace' print("hello".replace("l", "L")) ~~~~~~~^ mojo: error: failed to parse the provided Mojo source module
'StringLiteral["hello"]' value has no attribute 'replace'(リテラルには replace がない)と言われました。String(...) で包むか、いったん var に入れれば String になって使えます。
def main():
var word = "hello"
print(word.replace("l", "L"))
print(String("hello").replace("l", "L"))~$ mojo run literal_ok.mojo heLLo heLLo
4.2連結・くり返し・比較
Python と同じ演算子が使えます。+ で連結、* で整数回のくり返し、+= で追記、== や < で比較、in で部分文字列の検査です。
def main():
var a = String("abc")
print(a + "def")
print(a * 3)
print("-" * 10)
var b = String("ab")
b += "cd"
b += "ef"
print(b)
print("abc" == "abc", "abc" != "abd", "abc" < "abd")
print("ell" in String("hello"), "xyz" in String("hello"))~$ mojo run operators.mojo abcdef abcabcabc ---------- abcdef True True True True False
1行目の a + "def" は abcdef、2行目の a * 3 は abcabcabc です。"-" * 10 のようにリテラルに * を使う書き方もできます。b += "cd" のように += で追記できる点も Python と同じです。
比較は、文字の並びを辞書順で比べます。"abc" < "abd" が True なのは、3文字目が c と d で、c のほうが前だからです。
4.3長さを数える:バイトか、文字か
Python で len("こんにちは") と書くと 5 が返ります。Mojo で同じことをするとどうなるでしょうか。
def main():
var s = "こんにちは"
print(len(s))~$ mojo run len_error.mojo /home/user/len_error.mojo:3:11: error: `len(String/StringSlice)` is not supported because Mojo strings are UTF-8 encoded, so a single length is ambiguous: it could mean the number of UTF-8 bytes, the number of Unicode code points, or the number of user-visible characters (grapheme clusters). Use `s.byte_length()` or `len(s.bytes())` for the number of UTF-8 bytes, `len(s.codepoints())` for Unicode code points, or `len(s.graphemes())` for grapheme clusters. print(len(s)) ^~~ <unknown>:0: note: 'len' declared here mojo: error: failed to parse the provided Mojo source module
エラーです。メッセージを日本語にすると、次のとおりです。
> len(String/StringSlice) には対応していません。Mojo の文字列は UTF-8 なので、長さがひとつに決まらないからです。UTF-8 のバイト数、Unicode のコードポイント数、見た目の文字数(書記素クラスタ数)のどれのことなのか、はっきりしません。
そして、使うべき関数も教えてくれています。
| 数えたいもの | 書き方 |
|---|---|
| UTF-8 のバイト数 | s.byte_length() または len(s.bytes()) |
| Unicode のコードポイント数 | len(s.codepoints()) |
| 見た目の文字数(書記素クラスタ数) | len(s.graphemes()) |
実際に数えてみましょう。
def main():
var s = "こんにちは"
print("バイト数:", s.byte_length())
print("コードポイント数:", len(s.codepoints()))
print("書記素クラスタ数:", len(s.graphemes()))
var e = "e\u0301"
print(e, e.byte_length(), len(e.codepoints()), len(e.graphemes()))
var emoji = "😀"
print(emoji.byte_length(), len(emoji.codepoints()))~$ mojo run lengths.mojo バイト数: 15 コードポイント数: 5 書記素クラスタ数: 5 é 3 2 1 4 1
「こんにちは」は、バイト数が 15、コードポイント数が 5、見た目の文字数も 5 です。日本語のひらがなは、1文字が UTF-8 で3バイトになるので、バイト数は文字数の3倍になります。
図のように、こ は E3 81 93 という3バイトで表されます。len(s) がひとつの答えを返せないのは、「3バイトずつを1文字とみなす」のが日本語の場合に限った話だからです。英数字は1文字1バイトですし、絵文字 😀 は1つのコードポイントで4バイトになります(最後の行)。
2行目の e\u0301 は、e に「アクセント記号」(U+0301)を重ねた、見た目は é の1文字です。実際には2つのコードポイントでできているので、バイト数は 3、コードポイント数は 2 ですが、人が見て数える文字数(書記素クラスタ数)は 1 になります。
4.4添字とスライス
長さと同じ理由で、s[0] や s[1:3] もそのままでは書けません。
def main():
var s = "こんにちは"
print(s[0])~$ mojo run index_error.mojo /home/user/index_error.mojo:3:12: error: String does not support direct positional indexing like `s[i]` because Mojo strings are UTF-8 encoded, and the same position can mean three different things. Use one of: `s[byte=i]` for a raw UTF-8 byte, `s[codepoint=i]` for a Unicode code point, or `s[grapheme=i]` for a user-visible character (grapheme cluster). print(s[0]) ~^~~ <unknown>:0: note: '__getitem__' declared here mojo: error: failed to parse the provided Mojo source module
メッセージによると、位置の意味が3通りあるので、s[byte=i](UTF-8 のバイト位置)、s[codepoint=i](コードポイントの位置)、s[grapheme=i](見た目の文字の位置)のどれかを選ぶ必要があります。スライスも同じ形で、s[codepoint=1:3] のように書きます。
def main():
var s = "こんにちは"
print(s[codepoint=0])
print(s[codepoint=1])
print(s[grapheme=4])
print(s[codepoint=1:3])
print(s[byte=3])
print(s[byte=3:9])~$ mojo run indexing.mojo こ ん は んに ん んに
s[codepoint=1] は2文字目の ん、s[codepoint=1:3] は2〜3文字目の んに です。Python の s[1]、s[1:3] と同じ結果になります。
バイト位置のほうは、図のとおり1文字が3バイトなので、s[byte=3] が ん、s[byte=3:9] が んに になります。s[byte=3] は「3バイト目の1バイト」ではなく、3バイト目から始まる1文字 を返す点に注意してください。
文字の途中を指すとプログラムが止まる
バイト位置のほうは、文字の途中を指すことができてしまいます。s[byte=1] は、こ の2バイト目を指します。
def main():
var s = "こんにちは"
print(s[byte=1])~$ mojo run mid_char.mojo 2>&1 | head -2 At: /home/user/mid_char.mojo:3:12: Assert Error: String span index, 1 does not lie on a codepoint boundary. #0 0x00007437d77c4d1e (/home/user/mojo-book-env/.pixi/envs/default/lib/libKGENCompilerRTShared.so+0x112d1e)
ABORT や Assert Error は、プログラムが異常終了した ことを表します。文字の境界ではない位置を指したことを、Mojo が検出して止めたためです。try / except で受け止めることはできません。範囲外の位置でも同じです。
def main():
var s = "こんにちは"
print(s[codepoint=10])~$ mojo run out_of_range.mojo 2>&1 | head -2 ABORT: Mojo/stdlib/std/collections/string/string_span.mojo:438:18: codepoint index is out of bounds: 10 #0 0x000074dd63dc4d1e (/home/user/mojo-book-env/.pixi/envs/default/lib/libKGENCompilerRTShared.so+0x112d1e)
def main():
var s = "こんにちは"
var n = len(s.codepoints())
print(s[codepoint=n - 1])
print(s[codepoint=n - 2:n])~$ mojo run last_char.mojo は ちは
4.51文字ずつ取り出す
文字列の中身を順に処理したいときは、for で回します。文字列をそのまま回すと、1文字ずつの文字列が取り出せます。
def main():
var s = "こんにちは"
for c in s:
print(c, end=" ")
print()
for c in s.codepoints():
print(c.to_u32(), end=" ")
print()
print(ord("あ"), chr(12354))~$ mojo run iterate.mojo こ ん に ち は 12371 12435 12395 12385 12399 12354 あ
1つ目のループは こ ん に ち は と、文字がそのまま出ます。
2つ目の s.codepoints() は、文字を コードポイント(文字に割り当てられた番号)として取り出します。c.to_u32() で、その番号を整数に直しています。12371 は こ の番号(U+3053)です。
3行目の ord と chr は Python と同じで、ord("あ") は文字から番号を、chr(12354) は番号から文字を返します。
s.codepoint_slices() を使うと、s.codepoints() と違って、番号ではなく1文字分の文字列として取り出せます。ただ、for c in s と結果が同じなので、本書では for c in s を使います。
4.6よく使うメソッド
Python の文字列メソッドと同じ名前のものが、たくさんあります。
def main():
var s = String("Hello, World")
print(s.upper(), s.lower())
print("[" + String(" padded ").strip() + "]")
print(s.find("World"), s.find("xyz"), s.rfind("o"))
print(s.count("o"))
print(s.replace("World", "Mojo"))
print(s.startswith("Hello"), s.endswith("World"))
var parts = String("a,b,c").split(",")
print(len(parts), parts[0])
print(parts)
print("-".join(parts))
var words = String(" one two three ").split()
for w in words:
print("<" + w + ">")
print(String("l1\nl2\nl3").splitlines())~$ mojo run methods.mojo HELLO, WORLD hello, world [padded] 7 -1 8 2 Hello, Mojo True True 3 a [a, b, c] a-b-c <one> <two> <three> [l1, l2, l3]
行ごとに見ていきます。
upper()/lower():大文字・小文字にします。strip():前後の空白を取り除きます。lstrip()は左だけ、rstrip()は右だけです。find():最初に見つかった位置を返し、見つからなければ-1を返します。rfind()は後ろから探します。ここで返る位置は、バイト位置です(日本語を含む文字列では、s[byte=...]と組み合わせて使います)。count():部分文字列が何回出てくるかを数えます。replace():すべての出現を置き換えます。startswith()/endswith():先頭・末尾の検査です。split(","):区切り文字で分けて、文字列のリスト(List[String])を返します。printで表示するときは、[a, b, c]のように引用符が付きません。引数を省略したsplit()は、連続する空白をひとまとめにして区切ります。join():"-".join(parts)のように、区切りになる文字列に対して呼び、リストの要素をつなぎます。Python と同じ向きです。splitlines():改行で分けます。
リストの扱いは第5章で詳しく学びます。ここでは「split の結果は parts[0] のように取り出せる」と理解しておけば十分です。
4.7数値と文字列の変換
数値を文字列にするには String(...)、文字列を整数にするには Int(...)、小数にするには Float64(...) を使います。
def main() raises:
var n = 42
var text = String(n)
print(text + "!", text.byte_length())
print(String(3.14), String(True))
var a = Int("42")
var b = Float64("2.5")
print(a + 1, b + 1)
print(Int(" 42 "))~$ mojo run convert.mojo 42! 2 3.14 True 43 3.5 42
String(n) は、整数 42 を "42" にします。"n = " + n のように文字列と数値を直接 + でつなぐことはできず、String(n) で変換が必要です(次の節で実際のエラーを見ます)。
Int("42") は文字列を整数にします。前後の空白(" 42 ")は許されます。
def main() raises: と書いてあるのに注意してください。Int("...") は変換に失敗するとエラーを送り出す(raises)関数だからです。raises は第9章で詳しく学びます。
変換に失敗すると
数字ではない文字列を Int に渡すと、どうなるでしょうか。
def main() raises:
var x = Int("abc")
print(x)~$ mojo run parse_fail.mojo stack trace was not collected. Enable stack trace collection with environment variable `MODULAR_DEBUG=stack-trace-on-error` Unhandled exception caught during execution: String is not convertible to integer with base 10: 'abc' mojo: error: execution exited with a non-zero result: 1
Unhandled exception caught during execution: に続いて、String is not convertible to integer with base 10: 'abc'(文字列は10進数の整数に変換できません)と出て、プログラムが終了しました。Python の ValueError に当たります。
プログラムを止めたくないときは、try / except で受け止めます。
def parse(text: String) -> Int:
try:
return Int(text)
except e:
print("変換できません:", e)
return -1
def main():
print(parse("123"))
print(parse("12.5"))
print(parse("abc"))~$ mojo run parse_try.mojo 123 変換できません: String is not convertible to integer with base 10: '12.5' -1 変換できません: String is not convertible to integer with base 10: 'abc' -1
"12.5" も Int には変換できないことに注意してください。小数点を含む文字列は、Float64("12.5") で小数として読みます。
4.8文字列を組み立てる
+ でつなぐ以外に、文字列を組み立てる方法が3つあります。
String(...) に複数の値を渡す
String(...) は、複数の値を受け取って、1つの文字列にします。数値を String(n) で変換する手間もいりません。sep で区切りも指定できます。
def main():
var n = 7
print(String("n = ", n))
print(String("a", 1, "b", sep="-"))
print(String("x", "y", "z"))~$ mojo run build_string.mojo n = 7 a-1-b xyz
引数の間には、何も指定しなければ空白は入らず、そのままつながります(1行目と3行目)。最初の例で n = の後ろに空白を書いているのはそのためです。sep を指定すると、その文字列が間に入ります。
テンプレート文字列 t"..."
Python の f 文字列に当たるのは、引用符の前に t を付けた テンプレート文字列 です。{} の中に変数や式を書くと、その値が文字列として入ります。使うときは、String(...) で包んで文字列にします。
def main():
var name = "Mojo"
var n = 3
var t = t"hi {name}, n = {n}"
print(String(t))
print(String(t"{n} + 1 = {n + 1}"))~$ mojo run tstring.mojo hi Mojo, n = 3 3 + 1 = 4
Python の f 文字列(f"hi {name}")は、Mojo 1.2.0 では書けません。
def main():
var name = "Mojo"
print(f"hi {name}")~$ mojo run fstring.mojo /home/user/fstring.mojo:3:12: error: expected ')' in call argument list print(f"hi {name}") ^ mojo: error: failed to parse the provided Mojo source module
format
"{}" を使う format もあります。{} の部分が、引数で順に置き換わります。
def main():
print("{} + {} = {}".format(1, 2, 3))
print("name: {}".format("Mojo"))~$ mojo run format.mojo 1 + 2 = 3 name: Mojo
Python の "{:>5}".format(2) のように、{} の中に書式指定(右寄せなど)を書くと、Mojo 1.2.0 ではコンパイルエラーになります({:>5} を試すと Index :>5 not in kwargs と言われました)。書式を整えたいときは、String(...) や t"..." で組み立てたあと、自分で空白を足します。
def main():
var n = 1
var x = "n=" + n~$ mojo run plus_int.mojo 2>&1 | head -3 /home/user/plus_int.mojo:3:20: error: no matching method in call to '__add__' var x = "n=" + n ~~~~~~~^
エラーの先頭に no matching method in call to '__add__'(+ に当たる処理が見つからない)と出ます。+ が受け付けるのは、文字列どうしだけです。
4.9複数行の文字列とエスケープ
三重引用符 """...""" で、複数行にまたがる文字列を書けます。
def main():
var poem = """first line
second line
third line"""
print(poem)
print(len(poem.splitlines()))~$ mojo run multiline.mojo first line second line third line 3
エスケープシーケンスは、バックスラッシュ(\)で始めます。
| 書き方 | 意味 |
|---|---|
\n | 改行 |
\t | タブ |
\\ | バックスラッシュ |
\" | 二重引用符 |
\x41 | 16進数2桁で指定した文字(この例では A) |
\u3042 | 16進数4桁で指定した Unicode の文字(この例では あ) |
def main():
print("line1\nline2")
print("tab:\there")
print("back\\slash and \"quote\"")
print("\x41 \u3042")~$ mojo run escapes.mojo line1 line2 tab: here back\slash and "quote" A あ
\u のあとは、必ず4桁 の16進数です。それより大きい番号(絵文字など)は、文字そのものを書いてください。\u{1F600} のような書き方は、エラーになります。
def main():
print(r"C:\new\table")~$ mojo run raw.mojo C:\new\table
引用符の前に r を付けた raw 文字列 では、バックスラッシュがそのまま文字として扱われます。Windows のパスなどを書くときに便利です。
4.10Python との違いのまとめ
| やりたいこと | Python | Mojo 1.2.0 |
|---|---|---|
| 文字数 | len(s) | len(s.codepoints()) |
| バイト数 | len(s.encode()) | s.byte_length() |
| 見た目の文字数 | (標準にはない) | len(s.graphemes()) |
| 1文字取り出す | s[1] | s[codepoint=1] |
| スライス | s[1:3] | s[codepoint=1:3] |
| 最後の文字 | s[-1] | 位置を計算する(負の位置は使えない) |
| 1文字ずつ回す | for c in s: | for c in s: |
| 数値を文字列に | str(42) | String(42) |
| 文字列を整数に | int("42") | Int("42") |
| 文字列を組み立てる | f"n={n}" | t"n={n}"(String(...) で包む)、String("n=", n) |
| 変換の失敗 | ValueError | try / except で受け止める |
4.11まとめ
- 文字列リテラルは
StringLiteral、varに入れるとStringになります。メソッドを使いたいときはvarに入れるかString(...)で包みます。 +・*・+=・==・<・inは Python と同じように使えます。- Mojo の文字列は UTF-8 です。
len(s)とs[i]は書けません。バイト数はs.byte_length()、コードポイント数はlen(s.codepoints())、見た目の文字数はlen(s.graphemes())で数えます。 - 添字とスライスは
s[codepoint=i]・s[byte=i]・s[grapheme=i]のように、位置の意味を書いて選びます。文字の途中や範囲外を指すと、プログラムが異常終了します。負の位置は使えません。 for c in s:で1文字ずつ回せます。コードポイントの番号が欲しいときはs.codepoints()とto_u32()を使います。upper・strip・find・replace・split・joinなど、主なメソッドは Python と同じ名前です。Int("...")は、数字でない文字列では失敗します。try/exceptで受け止められます。- 文字列を組み立てるには、
String(...)、t"..."、formatが使えます。f 文字列は使えません。
確認クイズ
Mojo で
len("こんにちは")と書くと、どうなりますか。代わりに何を書きますか。解答を見る
コンパイルエラーになります。UTF-8 の文字列では長さがひとつに決まらないためです。バイト数は
s.byte_length()(15)、コードポイント数はlen(s.codepoints())(5)、見た目の文字数はlen(s.graphemes())(5)で数えます。"こんにちは"のバイト数が15になるのはなぜですか。解答を見る
UTF-8 では、ひらがな1文字が3バイトで表されるからです。5文字 × 3バイトで15バイトになります。
文字列
sの2文字目を取り出すには、どう書きますか。解答を見る
s[codepoint=1]です。バイト位置で指すs[byte=i]は、i が文字の境界でないとプログラムが止まるので、日本語を含む文字列ではcodepointを使うほうが安全です。Int("abc")はどうなりますか。止めずに扱うにはどうしますか。解答を見る
変換に失敗してエラーになり、そのままではプログラムが終了します。
try: ... except e: ...で受け止めれば、エラーメッセージeを表示するなどして続けられます。Python の f 文字列に当たる書き方は何ですか。
解答を見る
t"..."のテンプレート文字列です。String(t"n = {n}")のようにString(...)で包んで文字列にします。f"..."はそのままでは書けません。
練習問題 文字数と先頭の文字
"こんにちは世界" について、バイト数、コードポイント数、先頭の文字、最後の文字を表示するプログラムを書いてください。
解答例を見る
def main():
var s = "こんにちは世界"
var n = len(s.codepoints())
print(s.byte_length(), n)
print(s[codepoint=0], s[codepoint=n - 1])~$ mojo run ex1.mojo 21 7 こ 界
バイト数は 7 × 3 = 21、コードポイント数は 7 です。最後の文字は、位置を n - 1 と計算して取り出します。
練習問題 数字を足す
文字列 "10,20,30" を , で分けて、各要素を Int に変えて合計を表示してください。
解答例を見る
def main() raises:
var parts = String("10,20,30").split(",")
var total = 0
for p in parts:
total += Int(p)
print(total)~$ mojo run ex2.mojo 60
split の結果の各要素を Int(p) で整数にして、足しています。数字でない要素があれば Int が失敗するので、main に raises が付いています。
練習問題 文字を逆順にする
"mojo" の文字を逆順にして ojom と表示するプログラムを書いてください。(ヒント:for で1文字ずつ取り出し、新しい文字列の先頭にくっつけていく。)
解答例を見る
def main():
var s = "mojo"
var rev = String()
for c in s:
rev = String(c) + rev
print(rev)~$ mojo run ex3.mojo ojom
取り出した1文字を、これまでに作った文字列の 前 に付けることで、逆順になります。日本語の文章でも、for c in s はコードポイント単位で回るので同じように動きます。