RubyistがPythonの勉強をしてみて印象的だった違い

# 最初に

自分のことをRubyistっていうのはなんか気恥ずかしいのですが、
競プロでよくRubyを書いてたし、最近Ruby技術者認定試験Goldに受かったし、
Rubyistって言ってもいいんじゃないかなって思います。

そんな自分が、最近Pythonに再び興味が湧いて、Pythonの基礎力も検定などを通して確かめてみたいと思い、「Python 3 エンジニア認定基礎試験」というのを見つけ、勉強しました。
RubyほどではないですがPythonも齧ったことがあり、試験演習を通じてすぐ合格できそうでした。

実際に試験を受けることはない気がしましたが、RubyとPythonの違い等について気になったことがいくつかありますので、何番煎じかわからないですが、記事として残しておこうと思います。

特にメソッド周りでスコープや文法の違いが、特に印象に残りました。

# 文字列

Pythonの文字列は破壊的な変更不可(イミュータブル)で、やはりRubyとの違いを感じます。

JavaScriptやCrystal等の言語も文字列は変更不可で、
トレンド的にはRubyの方がざっくりしてて珍しいんじゃないかと思ってますけど、
Rubyに慣れてるとPythonで1字の代入変更が出来ず地味に嵌りそうです。

```python
s = "bar"
s[2] = "z"  # TypeError: 'str' object does not support item assignment
```


また、Rubyでは範囲外の添字で`nil`を返しますが、
Pythonでは範囲外の添字だとエラーです。

```python
s = "bar"
s[3] # IndexError: string index out of range
```

配列・リストでも同じ挙動ですが、Rubyの方がざっくりしてる感じです。

ただし、Pythonで`:`を使った指定方法だと、エラーにならない。
ここはちょっと意外な感じがしました。使い分けが出来るので便利そう。

```python
s = "bar"
print(s[-100:100:1]) # bar
```

# 演算子

Rubyの特徴ですが、`*`のような演算子は、メソッドです。
左側がレシーバで`文字列 * 整数`で、`String#*`が呼ばれます。
反対に`整数 * 文字列`で、`Integer#*`が呼ばれ、
`Integer#*`は引数に文字列が対応してないのでエラーになります。

Rubyを学習し始めた最初はなんで順番があるのか理解に苦しんだ記憶が薄っすらあるのですが、
今ではPythonの順番が関係ない点に抵抗を感じてしまいます。

```python
print(3 * "x") # "xxx"
```

数値どうしは交換できるけど、文字列と整数は等価じゃないし交換できるのは変だよねという気持ちです。

# メソッド周辺での違い

## メソッド呼び出し時の`=`の違い

Rubyの場合、メソッド呼び出し時の`=`は普通の代入と同じ。 
(普通、Rubyではメソッド呼び出し時には`=`で代入はしないでしょう)
しかし、Pythonの呼び出し時の`=`は、キーワード引数の意味。

```python
def foo(x, y):
  print(x, y)

foo("X", "Y") # X Y
foo(y = "Y", x = "X") # X Y
```

そして、位置引数(positional argument)でも、キーワード引数でも、どちらの形でも呼ぶことができる。
こういうの欲しいかもと思ったことがあるので、便利かもしれない。

ただし、次のような制約・注意点があります。

- 文法として、位置引数はキーワード引数よりも先に書く必要あり。
- 代入でなく、キーワード引数なので、仮引数で存在しない変数名を実引数で書くとエラー。

```python
def foo(x, y):
  print(x, y)

foo(x = "X", "Y") # SyntaxError: positional argument follows keyword argument
foo("X", z = "Y") # TypeError: foo() got an unexpected keyword argument 'z'
foo("Y", x = "X") # TypeError: foo() got multiple values for argument 'x'
```

## 定義時の`=`はデフォルト引数という意味では同じ

また、定義時における`=`はデフォルト引数の意味で、
こちらも｢デフォルト引数は非デフォルト引数よりも後でならなければならない｣といったルールがあります。

```python
def foo(z = "Z", x, y):
  print(x, y)
# SyntaxError: non-default argument follows default argument
```

なお、定義時における`=`はRubyも同じデフォルト引数の意味です。
しかし、Rubyの場合は通常の引数(非デフォルト引数)の前後どちらか1つなら、どちらでもデフォルト引数を設定することができます(できてしまいます)。

```ruby
def foo(z = "Z", x, y)
  p [x, y, z]
end

foo("X", "Y") #=> ["X", "Y", "Z"]
```

それから、重要なこととして後述するスコープの違いがあるので、
デフォルト引数という意味では同じですが挙動が大きく変わる可能性があるので注意が必要です。

それから、Pythonのデフォルト引数はキーワード引数を兼ねてるところも面白いです。
良い機能な気がします。

## スコープの違い

Rubyと大きく異なると思う点で、メソッドのスコープがあります。
Rubyでは、メソッド内におけるローカル変数の参照で、メソッド外での定義を探しにいきません。
対して、Pythonは、メソッド内における参照で、メソッド外での定義も探します。

```python
x = "X"
y = "Y"

def foo(x = x):
  print(x, y)
  
foo() # X Y
```

Rubyだと、メソッド外の定義を探しにいくのは、定数やグローバル変数です。
このあたりのPythonの挙動は、きっちりめなイメージのPythonに似つかわしくない感じがしました。

## デフォルト引数の挙動の違い

1番驚いた違いとして、メソッドのデフォルト引数の挙動があります。

Pythonでは、デフォルト引数で与えれらたオブジェクトが、引数がないときに常に同一になります。
次の例で空配列が生成されるのは、1度きり。

```python
def foo(i, L = []):
  L.append(i)
  return L
  
print(foo(1)) # [1]
print(foo(2)) # [1, 2]
print(foo(3)) # [1, 2, 3]
print(foo(10, [])) # [10]
```

対して、Rubyでは、次のコード例のように、デフォルト引数で同じく空配列を与えていますが、引数がなかったときに毎回新しく空配列のオブジェクトが生成されて代入されていることがわかります。

```ruby
def foo(i, a = [])
  a.append(i)
end
  
p foo(1) # [1]
p foo(2) # [2]
p foo(3) # [3]
p foo(10, []) # [10]
```

このあたりの挙動が全く異なるので、注意が必要そうです。

# 最後に

以上です。読んでいただき、ありがとうございました。