金貨と銅貨と空箱の「うそつき問題」(Ruby)

rsc.hatenablog.comまたまた rsc さんのブログから問題を拝借して、Ruby で解いてみました。
 

問題

コピペさせてもらいます。

 A~Eの五つの箱があり、これらの箱は、金貨の入った箱、銅貨の入った箱、空箱の3種類の場合がある。
 また、それぞれの箱にはラベルが付いているが、そのラベルの記述の内容は、金貨の入った箱のものは真(真実に一致している)であるが、銅貨の入った箱のものは偽(真実に反している)であり、空箱のものは真の場合も偽の場合もあるという。
 このとき、銅貨の入った箱が二つあるとすると、確実に銅貨の入った箱はどれか。
[ラベル]
A:「Bのラベルの記述の内容は真である。」
B:「Aが空箱ならば、この箱も空箱である。」
C:「この箱は、銅貨の入った箱である。」
D:「AかEの少なくとも一方は、銅貨の入った箱である。」
E:「この箱は、金貨の入った箱である。」

 

解答例

問題が少し曖昧で、空箱の場合、「真の場合も偽の場合もある」というのは「真でも偽でもどちらでも可」なのか、「真の場合も偽の場合も両方とも満たさないといけない」のかわかりませんが、まあ前者だろうと解釈して解きました。

コード。

N = 5
TBL = [*0...N]

box = Array.new(N)

imp = ->(p, q) {!p || q}

label = Array.new(N)
label[0] = ->{label[1].()}
label[1] = ->{imp.(!box[0], !box[1])}
label[2] = ->{box[2] == :copper}
label[3] = ->{box[0] == :copper || box[4] == :copper}
label[4] = ->{box[4] == :gold}

judgement = ->(i) {
  return true unless box[i]
  f = label[i].()
  (box[i] == :gold) ? f : !f
}

output = ->{
  puts (0...N).map {|i| "#{%W(A B C D E)[i]}=>#{box[i].inspect}"}.join(", ")
}

TBL.combination(2) do |cp2|
  cp2.each {|c| box[c] = :copper}
  (0..3).each do |n|
    (TBL - cp2).combination(n) do |golds|
      golds.each {|g| box[g] = :gold}
      (TBL - cp2 - golds).each {|e| box[e] = nil}
      output.() if (0...N).all? {|i| judgement.(i)}
    end
  end
end

 
結果。

A=>nil, B=>:copper, C=>nil, D=>:copper, E=>nil
A=>nil, B=>:copper, C=>nil, D=>:copper, E=>:gold
A=>nil, B=>:copper, C=>nil, D=>nil, E=>:copper
A=>nil, B=>:copper, C=>nil, D=>:gold, E=>:copper

つまり答えは B ということになります。
 

簡単な解説

箱は A ~ E ということですが、コードでは 0 ~ 4 に置き換えています。

基本的に「総当り法」で、まず「銅」の箱を2つ選び、残りは適当に「金」か「空」を割り当てます。全部「金」になったり、全部「空」になる場合もあります(と解釈して解いたのですが、それも問題では曖昧です)。箱は box[] で、:gold, :copper, nil が入るということです(nil は「空」)。

ラベルの条件は label[] に Proc オブジェクトで記述しています。「p ならば q」という論理包含(IMP)は、「p の否定と q との論理和」と必要十分なので、それを imp という Proc で表現しています。

箱の中身とラベルのマッチングを判断しないといけないので、それは judgement.() で記述しています。すべての箱で judgement.() が真になった場合に、その組み合わせを出力( output.() )します。


Proc (ここでは lambda ですが)のクロージャの機能を使いまくっているコードになっています。

Rubyでファイルのツリー構造を出力する

qiita.com
これを見て Ruby でもやってみたくなりました。
 

実行例

<kaki-utils>
  ├<lib>
  │  └<kaki>
  │     ├<utils>
  │     │  ├add_methods.rb
  │     │  ├all.rb
  │     │  ├check_fname_overlapping.rb
  │     │  ├es.rb
  │     │  ├imgsuffix.rb
  │     │  ├nest_loop.rb
  │     │  ├po.rb
  │     │  ├rec_decimal.rb
  │     │  ├retry.rb
  │     │  └safe_mkdir.rb
  │     └utils.rb
  ├README.md
  ├kaki-utils-0.0.1.gem
  ├kaki-utils-0.0.10.gem
  ├kaki-utils-0.0.2.gem
  ├kaki-utils-0.0.3.gem
  ├kaki-utils-0.0.4.gem
  ├kaki-utils-0.0.5.gem
  ├kaki-utils-0.0.6.gem
  ├kaki-utils-0.0.7.gem
  ├kaki-utils-0.0.8.gem
  ├kaki-utils-0.0.9.gem
  ├kaki-utils-0.1.0.gem
  ├kaki-utils-0.1.1.gem
  ├kaki-utils-0.1.10.gem
  ├kaki-utils-0.1.11.gem
  ├kaki-utils-0.1.12.gem
  ├kaki-utils-0.1.13.gem
  ├kaki-utils-0.1.14.gem
  ├kaki-utils-0.1.15.gem
  ├kaki-utils-0.1.2.gem
  ├kaki-utils-0.1.3.gem
  ├kaki-utils-0.1.4.gem
  ├kaki-utils-0.1.5.gem
  ├kaki-utils-0.1.6.gem
  ├kaki-utils-0.1.7.gem
  ├kaki-utils-0.1.8.gem
  ├kaki-utils-0.1.9.gem
  └kaki-utils.gemspec

 

コード

元の Python コードはまったく見ていません。簡単な再帰で実装しています。
print_dir_tree.rb

class Dir
  def self.print_tree(top_dir = Dir.pwd)
    unless File.directory?(top_dir)
      raise %("#{top_dir}") + " is not directory."
    end
    puts "<#{File.basename(top_dir)}>"
    
    walk_dir = ->(dir, pre) {
      Dir.chdir(dir) rescue return
      d_f = Dir.glob("*").sort
      dirs = d_f.select {|df| File.directory?(df)}
      files = d_f - dirs
      end_num = d_f.size
      line_num = 1
      
      dirs.each do |d|
        s0 = (line_num == end_num) ? "" : ""
        line_num += 1
        puts pre + s0 + "<#{d}>"
        s1 = pre + ((s0 == "") ? "   " : "")
        walk_dir.(d, s1)
      end
      
      files.each do |f|
        s = (line_num == end_num) ? "" : ""
        line_num += 1
        puts pre + s + f
      end
      
      Dir.chdir("..")
    }
    walk_dir.(top_dir, "  ")
  end
end


if __FILE__ == $0
  Dir.print_tree("./kaki-utils/")
end

Julia でじゃんけんゲームを書いてみる

20200306004319

いつものじゃんけんゲームを Julia のお勉強として書いてみました。Julia のバージョンは 1.0.4 です。
janken.jl

using Printf

mutable struct Person
    name::String
    wincount::Int
    hand::Int
end

function judge(player1::Person, player2::Person)
    show_hand(player::Person) = ["グー", "チョキ", "パー"][player.hand]
    
    @printf("%s 対 %s で ", show_hand(player1), show_hand(player2))
    
    winner = player1
    if player1.hand == player2.hand
        println("引き分けです。")
        return
    elseif (3 + player1.hand - player2.hand) % 3 == 1
        winner = player2
    end
    println("$(winner.name)の勝ちです。")
    
    winner.wincount += 1
end

function game(player1::Person, player2::Person, i::Int)
    println("*** $(i)回戦 ***")
    
    n = ""
    while true
        n = Base.prompt("$(player2.name)の手を入力して下さい(1:グー, 2:チョキ, 3:パー)")
        if n == "1" || n == "2" || n == "3" ; break end
    end
    player2.hand = parse(Int, n)
    
    player1.hand = rand(1:3)
    
    judge(player1, player2)
end

function winner(player1::Person, player2::Person)
    p1 = player1.wincount
    p2 = player2.wincount
    
    print("\n*** 最終結果 ***\n$(p1)$(p2) で ")
    
    final_winner = player1
    if p1 == p2
        println("引き分けです。")
        return
    elseif p1 < p2
        final_winner = player2
    end
    
    println("$(final_winner.name)の勝ちです。")
end

function main()
    pc = Person("Computer", 0, 0)
    
    name = Base.prompt("あなたの名前を入力して下さい")
    if name == "" ; name = "名無し" end
    player = Person(name, 0, 0)
    
    print("$(pc.name) 対 $(player.name) :じゃんけん開始\n\n")
    
    for i in 1:5 game(pc, player, i) end
    
    winner(pc, player)
end


main()

Julia はオブジェクト指向言語ではないですね。コードの見た目はすっきりした感じです。


Go言語版はこちらです。リンク先に他の言語での実装もまとめてあります。
Go言語でじゃんけんゲーム - Camera Obscura

Julia で 8 queens 問題

obelisk.hatenablog.com
ここで Go と Ruby でやっていることを、Julia でやってみました。

結果はこんな感じ。

$ time julia eight_queens.jl
--------------
n = 1
@.......
....@...
.......@
.....@..
..@.....
......@.
.@......
...@....
--------------
n = 2
@.......
.....@..
.......@
..@.....
......@.
...@....
.@......
....@...
--------------

....

--------------
n = 92
.......@
...@....
@.......
..@.....
.....@..
.@......
......@.
....@...

real	0m0.367s
user	0m0.474s
sys	0m0.211s

 
コード。
eight_queens.jl

N = 8
count = 0

function get_space(field, n)
    result = fill(true, N)
    for i in 1:n
        queen = field[i]
        result[queen] = false
        dst = n - i + 1
        if queen - dst >= 1 ; result[queen - dst] = false end
        if queen + dst <= N ; result[queen + dst] = false end
    end
    result
end

function solve(field, n)
    if n == N
        global count += 1
        println("--------------")
        println("n = $count")
        for queen in field
            println(repeat(".", queen - 1) * "@" * repeat(".", N - queen))
        end
    else
        tmp = get_space(field, n)
        for i in 1:N
            if tmp[i]
                field[n + 1] = i
                solve(field, n + 1)
            end
        end
    end
end

solve(repeat([0], N), 0)

配列のインデックスが 1 から始まるのには戸惑いました。あと、Rubyeach_with_index に相当する文法がないのかな? それ以外はかなり書きやすいですね。型の記述はしませんでした。このあたり、柔軟である。
 
N = 13 にしてかつ最終的な解の個数だけ出力するようにしてみると、

$ time julia eight_queens.jl
73712

real	0m12.339s
user	0m12.401s
sys	0m0.276s

という感じ。

Linux Mint に Julia をインストールする

julialang.org
Linux Mint 19.3 に Julia をインストールしてみます。

インストールはここを参考にしました。まず、Snap をインストールします。

$ sudo apt update
$ sudo apt install snapd

Julia をインストールします。

$ sudo snap install julia --classic
2020-03-05T12:14:16+09:00 INFO Waiting for restart...
julia 1.0.4 from The Julia Language (julialang✓) installed
$ /snap/bin/julia -version
julia version 1.0.4

これでインストールされましたが、/snap/bin にPATHが通っていないので、面倒です。PATHを通すか、自分は .bashrc に alias julia='/snap/bin/julia' としておきました。

これで、

$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.0.4 (2019-05-16)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> 2020*3
6060

julia> exit()
$ 

こんな感じで REPL が立ち上がります。
 

ちょっとREPLで遊んでみた

julia> a = 10
10

julia> 2a
20

julia> 4.5a
45.0

julia> typeof(a)
Int32

julia> typeof(4.5a)
Float64

julia> cube(x) = x^3
cube (generic function with 1 method)

julia> cube(a)
1000

julia> f(x) = 3x - a
f (generic function with 1 method)

julia> f(20)
50

julia> sphere(x) = 4/3 * pi * cube(x)
sphere (generic function with 1 method)

julia> sphere(10)
4188.790204786391

julia> b = [x for x = 1:4]
4-element Array{Int32,1}:
 1
 2
 3
 4

julia> c = b * 3
4-element Array{Int32,1}:
  3
  6
  9
 12

julia> b + c
4-element Array{Int32,1}:
  4
  8
 12
 16

julia> d = vcat(b, c)
8-element Array{Int32,1}:
  1
  2
  3
  4
  3
  6
  9
 12

julia> filter(iseven, d)
4-element Array{Int32,1}:
  2
  4
  6
 12

julia> str = map(x -> repeat("*", x), b)
4-element Array{String,1}:
 "*"   
 "**"  
 "***" 
 "****"

julia> join(str, " ")
"* ** *** ****"

julia> a = 45
45

julia> f(20)
15

julia> g(x) = 2x - j
g (generic function with 1 method)

julia> g(3)
ERROR: UndefVarError: j not defined
Stacktrace:
 [1] g(::Int32) at ./REPL[22]:1
 [2] top-level scope at none:0

julia> j = 100
100

julia> g(3)
-94

julia> j = 10
10

julia> g(3)
-4

関数はクロージャなのだな。それから、関数内に未定義変数があっても、実行するまでは怒られないのか。
 

Ruby は「痒いところに手が届く」か

qiita.comいや、Python すばらしいですね。どこまで Ruby でできるか、試してみました。なお、使った Ruby のバージョンは 2.7.0 です。
 

3値以上の比較

1 == 2 == 3  # -> False
1 < 2 < 3 < 4  # -> True

いやー、これは Ruby ではできないですね。なお、Ruby でもこれができるバージョンがあったそうですが、まつもとさんがふつうの(?)比較に戻したそうです。
 

時間(datetime/date)の比較

Python

from datetime import date

feb1 = date(2020, 2, 1)
feb2 = date(2020, 2, 2)
feb3 = date(2020, 2, 3)

feb1 < feb2 <= feb2 < feb3  # -> True

Rubyrequire "date" します。

irb(main):001:0> require "date"
=> true
irb(main):002:0> feb1 = Date.new(2020, 2, 1)
irb(main):003:0> feb2 = Date.new(2020, 2, 2)
irb(main):004:0> feb3 = Date.new(2020, 2, 3)
irb(main):005:0> feb1 < feb2 && feb2 < feb3
=> true

だいたい同じですかね。
 

時間の最大/最小

Pyhton。

from datetime import date

# 意図的に逆順にしてます
dates = [
    date(2020, 2, 3),
    date(2020, 2, 2),
    date(2020, 2, 1),
]

min(dates)  # -> datetime.date(2020, 2, 1)
max(dates)  # -> datetime.date(2020, 2, 3)

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:0> dates = 3.downto(1).map {|i| Date.new(2020, 2, i)}
irb(main):003:0> dates.min
=> #<Date: 2020-02-01 ((2458881j,0s,0n),+0s,2299161j)>
irb(main):004:0> dates.max
=> #<Date: 2020-02-03 ((2458883j,0s,0n),+0s,2299161j)>

これもだいたい同じですかね。
 

時間の計算

Python

# Input
from datetime import datetime

start = datetime(2020, 2, 1, 10)
goal = datetime(2020, 2, 3, 12)

t = goal - start
print(f'あなたの記録は{t.days}日と{t.seconds}秒です')

# Output
'あなたの記録は2日と7200秒です'

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:0> start = DateTime.new(2020, 2, 1, 10)
irb(main):003:0> goal = DateTime.new(2020, 2, 3, 12)
irb(main):004:0> t = goal - start
irb(main):005:0> "あなたの記録は#{t.to_i}日と#{(t % 1 * 86400).to_i}秒です"
=> "あなたの記録は2日と7200秒です"

これはちょっと面倒ですねえ。もっとうまいやり方はないかな。
 

時間を連想配列のキーにする

Python

# Input
from datetime import date

counts = {
    date(2020, 2, 1): 0,
    date(2020, 3, 1): 0,
}
counts[date(2020, 2, 1)] += 1
counts[date(2020, 2, 1)] += 1
counts[date(2020, 3, 1)] += 1

print(counts)

# Output
{datetime.date(2020, 2, 1): 2, datetime.date(2020, 3, 1): 1}

Ruby

irb(main):001:0> require "date"
=> true
irb(main):002:1* counts = {
irb(main):003:1*   Date.new(2020, 2, 1) => 0,
irb(main):004:1*   Date.new(2020, 3, 1) => 0,
irb(main):005:0> }
irb(main):006:0> counts[Date.new(2020, 2, 1)] += 1
irb(main):007:0> counts[Date.new(2020, 2, 1)] += 1
irb(main):008:0> counts[Date.new(2020, 3, 1)] += 1
irb(main):009:0> counts
=> {#<Date: 2020-02-01 ((2458881j,0s,0n),+0s,2299161j)>=>2,
    #<Date: 2020-03-01 ((2458910j,0s,0n),+0s,2299161j)>=>1}

ほぼ同じ。
 

キーが連想配列に含まれるか

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

print('foo' in d)  # -> True

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.has_key?(:foo)
=> true

わかりやすいですね。
 

連想配列からキーを抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d))  # -> ['foo', 'bar', 'baz']

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.keys
=> [:foo, :bar, :baz]

これもわかりやすいですね。
 

連想配列から値を抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d.values()))  # -> [1, 2, 3]

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> h.values
=> [1, 2, 3]

これもわかりやすいですね。
 

連想配列からキーと値のペアを抽出

Python

d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

for key, value in d.items():
    print(key, value)

# Output
foo 1
bar 2
baz 3

Ruby

irb(main):001:0> h = {foo: 1, bar: 2, baz: 3}
irb(main):002:0> puts h.map {_1.join(" ")}
foo 1
bar 2
baz 3

map を使うのは Ruby らしいのでは? もちろんふつうに each あるいは each_pair で取り出すこともできます。
 

2重配列を連想配列へ変換

Python

# Input
l = [
    ['Yamada', 'baseball'],
    ['Tanaka', 'soccer'],
    ['Sato', 'tennis'],
]
dict(l)

# Output
{'Yamada': 'baseball', 'Tanaka': 'soccer', 'Sato': 'tennis'}

Ruby

irb(main):001:1* ary = [
irb(main):002:1*   [:Yamada, :baseball],
irb(main):003:1*   [:Tanaka, :soccer],
irb(main):004:1*   [:Sato, :tennis],
irb(main):005:0> ]
irb(main):006:0> ary.to_h
=> {:Yamada=>:baseball, :Tanaka=>:soccer, :Sato=>:tennis}

ほぼ同じですね。
 

多重配列のループ

Python

# Input
rows = [
    ['yamada', 20],
    ['tanala', 18],
    ['sato', 18],
]

for name, age in rows:
    print(f'{name}さんは{age}歳です')
else:
    print('紹介終わり')

# Output
'yamadaさんは20歳です'
'tanalaさんは18歳です'
'satoさんは18歳です'
'紹介終わり'

Ruby

irb(main):001:1* rows = [
irb(main):002:1*   [:yamada, 20],
irb(main):003:1*   [:tanala, 18],
irb(main):004:1*   [:sato, 18],
irb(main):005:0> ]
irb(main):006:0* puts rows.map {|name, age| "#{name}さんは#{age}歳です"} + ["紹介終わり"]
yamadaさんは20歳です
tanalaさんは18歳です
satoさんは18歳です
紹介終わり

いいんじゃないでしょうか。
 

多重配列から必要な要素だけ取り出しつつループ

Python

# Input
l = [
    ['Yamada', 'Taro', 20, 'baseball'],
    ['Tanaka', 'Jiro', 18, 'circle'],
]

# 先頭を取り出す
for last, *others in l:
    print(last, others)
print()

# 末尾を取り出す
for *others, circle in l:
    print(circle, others)
print()

# 最初の2要素を取り出す
# (他の要素が要らない場合はダブルアンダースコアを指定するのがPython流)
for last, first, *__ in l:
    print(last, first)

# Output
Yamada ['Taro', 20, 'baseball']
Tanaka ['Jiro', 18, 'circle']

baseball ['Yamada', 'Taro', 20]
circle ['Tanaka', 'Jiro', 18]

Yamada Taro
Tanaka Jiro

Ruby

irb(main):001:1* ary = [
irb(main):002:1*   [:Yamada, :Taro, 20, :baseball],
irb(main):003:1*   [:Tanaka, :Jiro, 18, :circle],
irb(main):004:0> ]
irb(main):005:0> #先頭を取り出す
irb(main):006:0> puts ary.map {|last, *others| "#{last} #{others.inspect}"}
Yamada [:Taro, 20, :baseball]
Tanaka [:Jiro, 18, :circle]
irb(main):007:0> #末尾を取り出す
irb(main):008:0> puts ary.map {|*others, circle| "#{circle} #{others.inspect}"} 
baseball [:Yamada, :Taro, 20]
circle [:Tanaka, :Jiro, 18]
irb(main):009:0> #最初の2要素を取り出す
irb(main):010:0> puts ary.map {|last, first, *_| "#{last} #{first}"}
Yamada Taro
Tanaka Jiro

ほとんど同じだなあ。
では Python

# Input
l = [
    ['a', 'b', 'c', ['d', 'e', 'f']],
]

for one, *__, (*__, two) in l:
    print(one, two)

# Output
a f

Ruby では? これはせっかくなので、Ruby 2.7.0 のパターンマッチを使ってみましょう。

irb(main):001:0> ary = [:a, :b, :c, [:d, :e, :f]]
irb(main):002:1* case ary
irb(main):003:1*   in [one, _, _, [_, _, two]]
irb(main):004:0> end
irb(main):005:0> "#{one} #{two}"
=> "a f"

おお、いいですね。
 

カウンター付きループ

Python

# Input
rows = [
    ['Yamada', 20],
    ['Tanaka', 18],
    ['Sato', 16],
]

for i, (name, age) in enumerate(rows, start=1):
    print(f'{i}行目 : 氏名={name}, 年齢={age}')

# Output
'1行目 : 氏名=Yamada, 年齢=20'
'2行目 : 氏名=Tanaka, 年齢=18'
'3行目 : 氏名=Sato, 年齢=16'

Ruby

irb(main):001:1* rows = [
irb(main):002:1*   [:Yamada, 20],
irb(main):003:1*   [:Tanaka, 18],
irb(main):004:1*   [:Sato, 16],
irb(main):005:0> ]
irb(main):006:1* puts rows.map.with_index(1) {|(name, age), i|
irb(main):007:1*   "#{i}行目:氏名=#{name}, 年齢=#{age}"
irb(main):008:0> }
1行目:氏名=Yamada, 年齢=20
2行目:氏名=Tanaka, 年齢=18
3行目:氏名=Sato, 年齢=16

Ruby らしいですね。
 

連想配列のキー無しエラーを防ぐ

Python

d = {
    'Yamada': 20,
    'Tanaka': 18
}
d.get('Yamada')  # -> 20
d.get('Sato')  # -> None
d.get('Sato', '年齢なし')  # -> 年齢なし

Ruby。これは Python とは挙動がちがいますね。どちらがよいでしょうか。

irb(main):001:0> h = Hash.new("年齢なし")
irb(main):002:0> h.merge!({Yamada: 20, Tanaka: 18})
=> {:Yamada=>20, :Tanaka=>18}
irb(main):003:0> h[:Yamada]
=> 20
irb(main):004:0> h[:Sato]
=> "年齢なし"

 

配列/連想配列を展開して関数へ渡す

Python

# Input
def func(a, b, c=None, d=None):
    print(a)
    print(b)
    print(c)
    print(d)


l = ['aaa', 'bbb']
d = {'c': 'ccc', 'd': 'ddd'}

func(*l, **d)

# Output
aaa
bbb
ccc
ddd

Ruby。キーワード引数は Ruby 2.7 で整理されました。

irb(main):001:1* def func(a, b, c: nil, d: nil)
irb(main):002:1*   puts a
irb(main):003:1*   puts b
irb(main):004:1*   puts c
irb(main):005:1*   puts d
irb(main):006:0> end
=> :func
irb(main):007:0> ary = [:aaa, :bbb]
irb(main):008:0> h = {c: :ccc, d: :ddd}
irb(main):009:0> func(*ary, **h)
aaa
bbb
ccc
ddd

ほぼ一緒ですね。
 

all関数

Python

l = [
    True,
    1,
    "foo",
]
all(l)  # -> True

l = [
    True,
    1,
    "",
]
all(l)  # -> False

Ruby。Enumerable#all? メソッドがあります。

irb(main):001:0> ary = [true, 1, "foo"]
irb(main):002:0> ary.all?
=> true
irb(main):003:0> ary = [true, 1, nil]
irb(main):004:0> ary.all?
=> false

ほぼ同じなのだけれど、Ruby では空文字列は true なので、少し変えました。
 

any関数

all関数の場合とほぼ同じなので、省略します。Ruby では Enumerable#any? を使います。one? や none? メソッドもあります。
 

集合演算(Set型)

Ruby では require "set" をすれば Python とほぼ同じなので省略。
 

collections / itertools パッケージ

collections.Counter

Python。同一要素の数を数えます。

# Input
import collections

l = ['a', 'b', 'c', 'a', 'a', 'c']
c = collections.Counter(l)
print(c.most_common())

# Output
[('a', 3), ('c', 2), ('b', 1)]

RubyRuby 2.7 から Enumerable#tally が入りました。

irb(main):001:0> ary = [:a, :b, :c, :a, :a, :c]
irb(main):002:0> ary.tally
=> {:a=>3, :b=>1, :c=>2}

 

collections.defaultdict

Python

# Input
import json
import collections

# defaultdict()の引数には関数(callable)ならなんでも渡せる
groups = collections.defaultdict(list)

# 普通の連想配列だと "baseballというキーは存在しない" といったエラーが発生する
groups['baseball'].append('yamada')
groups['tennis'].append('tanaka')
groups['baseball'].append('sato')

print(json.dumps(groups))

# Output
{"baseball": ["yamada", "sato"], "tennis": ["tanaka"]}

Ruby。Hash の default_proc を使います。

irb(main):001:0> groups = Hash.new {|h, k| h[k] = []}
irb(main):002:0> groups[:baseball] << :yamada
irb(main):003:0> groups[:tennis] << :tanaka
irb(main):004:0> groups[:baseball] << :sato
irb(main):005:0> groups
=> {:baseball=>[:yamada, :sato], :tennis=>[:tanaka]}

また、Python

# Input
import json
from collections import defaultdict

nested = defaultdict(lambda: defaultdict(int))

nested['a']['a'] += 1
nested['a']['a'] += 1
nested['a']['b'] += 1
nested['b']['c'] += 1

print(json.dumps(nested))

# Output
{"a": {"a": 2, "b": 1}, "b": {"c": 1}}

に対しては、Ruby なら

irb(main):001:0> nested = Hash.new {|h, k| h[k] = Hash.new(0)}
irb(main):002:0> nested[:a][:a] += 1
irb(main):003:0> nested[:a][:a] += 1
irb(main):004:0> nested[:a][:b] += 1
irb(main):005:0> nested[:b][:c] += 1
irb(main):006:0> nested
=> {:a=>{:a=>2, :b=>1}, :b=>{:c=>1}}

とすればよいです。
 

itertools.product

Python

# Input
import itertools

a = ['a1', 'a2']
b = ['b1', 'b2', 'b3']
c = ['c1']

list(itertools.product(a, b, c))

# Output
[('a1', 'b1', 'c1'),
 ('a1', 'b2', 'c1'),
 ('a1', 'b3', 'c1'),
 ('a2', 'b1', 'c1'),
 ('a2', 'b2', 'c1'),
 ('a2', 'b3', 'c1')]

Ruby には Array#product があります。

irb(main):001:0> a = [:a1, :a2]
irb(main):002:0> b = [:b1, :b2, :b3]
irb(main):003:0> c = [:c1]
irb(main):004:0> a.product(b, c)
=> [[:a1, :b1, :c1], [:a1, :b2, :c1], [:a1, :b3, :c1],
    [:a2, :b1, :c1], [:a2, :b2, :c1], [:a2, :b3, :c1]]

 

itertools.chain.from_iterable

Python

# Input
import itertools

l = [
    ['a1', 'a2'],
    ['b1', 'b2', 'b3'],
]

list(itertools.chain.from_iterable(l))

# Output
['a1', 'a2', 'b1', 'b2', 'b3']

これは Ruby では Array#flatten でおしまいです。

irb(main):001:1* ary = [
irb(main):002:1*   [:a1, :a2],
irb(main):003:1*   [:b1, :b2, :b3],
irb(main):004:0> ]
irb(main):005:0> ary.flatten
=> [:a1, :a2, :b1, :b2, :b3]

 

ioパッケージ

Python

# Input
import io


def writer(f, text):
    f.write(text)


def printer(f):
    print(f.read())


sio = io.StringIO()

writer(sio, 'foo\n')
writer(sio, 'bar\n')
writer(sio, 'baz\n')

sio.seek(0)
printer(sio)

# Output
foo
bar
baz

Ruby では StringIO を使います。

irb(main):001:0> require "stringio"
=> true
irb(main):002:1* def writer(f, text)
irb(main):003:1*   f.write(text)
irb(main):004:0> end
=> :writer
irb(main):005:1* def printer(f)
irb(main):006:1*   print f.read
irb(main):007:0> end
=> :printer
irb(main):008:0> sio = StringIO.new("tmp", "w+")
irb(main):009:0> writer(sio, "foo\n")
irb(main):010:0> writer(sio, "bar\n")
irb(main):011:0> writer(sio, "baz\n")
irb(main):012:0> sio.rewind
irb(main):013:0> printer(sio)
foo
bar
baz

 

タプル(tuple)

Ruby には immutable なタプルはありません。では、Python

# Input
from collections import defaultdict

data = [
    {'circle': 'baseball', 'name': 'yamada', 'age': 10},
    {'circle': 'baseball', 'name': 'sato', 'age': 10},
    {'circle': 'baseball', 'name': 'suzuki', 'age': 11},
    {'circle': 'tennis', 'name': 'tanaka', 'age': 10},
]

per_circle_age = defaultdict(list)

for v in data:
    k = (v['circle'], v['age'])  # (サークル名, 年齢) というタプルを生成
    per_circle_age[k].append(v['name'])  # タプルをキーにして集計

for (circle, age), members in per_circle_age.items():
    print(f'{circle}に所属している{age}歳のメンバー:{members}')

# Output
"baseballに所属している10歳のメンバー:['yamada', 'sato']"
"baseballに所属している11歳のメンバー:['suzuki']"
"tennisに所属している10歳のメンバー:['tanaka']"

これを Ruby でどうするかというと、ふつうに Array と Hash でいけます。

irb(main):001:1* data = [
irb(main):002:1*   {circle: :baseball, name: :yamada, age: 10},
irb(main):003:1*   {circle: :baseball, name: :sato, age: 10},
irb(main):004:1*   {circle: :baseball, name: :suzuki, age: 11},
irb(main):005:1*   {circle: :teniss, name: :tanaka, age: 10},
irb(main):006:0> ]
irb(main):007:0> per_circle_age = Hash.new {|h, k| h[k] = []}
irb(main):008:1* data.each do |h|
irb(main):009:1*   k = [h[:circle], h[:age]]
irb(main):010:1*   per_circle_age[k] << h[:name]
irb(main):011:0> end
irb(main):012:1* puts per_circle_age.map {|(circle, age), members|
irb(main):013:1*   "#{circle}に所属している#{age}歳のメンバー:#{members.inspect}"
irb(main):014:0> }
baseballに所属している10歳のメンバー:[:yamada, :sato]
baseballに所属している11歳のメンバー:[:suzuki]
tenissに所属している10歳のメンバー:[:tanaka]

 

dataclassesパッケージ

Python

import dataclasses


# frozen=True とすると不変(immutable)なオブジェクトとして扱える
@dataclasses.dataclass(frozen=True)
class User:
    last_name: str
    first_name: str

    def full_name(self):
        return f'{self.last_name} {self.first_name}'


yamada = User(last_name='Yamada', first_name='Taro')
tanaka = User(last_name='Tanaka', first_name='Jiro')

yamada.full_name()  # -> Yamada Taro

# 簡単に連想配列へ変換出来ます
dataclasses.asdict(yamada)  # -> {'last_name': 'Yamada', 'first_name': 'Taro'}

# 比較が可能です
yamada2 = User(last_name='Yamada', first_name='Taro')
yamada == yamada2  # -> True
yamada == tanaka  # -> False
yamada in [yamada2]  # -> True

# "frozen=True" とした場合は値の再代入は出来ません
yamada.last_name = 'Sato'  # -> FrozenInstanceError: cannot assign to field 'last_name'

# immutableなので連想配列のキーとして使えます
d = {yamada: 'foo', tanaka: 'bar'}

# 集合演算(Set型)も可能です
{yamada, tanaka} & {yamada2}  # -> {User(last_name='Yamada', first_name='Taro')}

Ruby なら、Struct クラスを使うとよいのかな。

irb(main):001:1* User = Struct.new(:last_name, :first_name) do
irb(main):002:2*   def full_name
irb(main):003:2*     "#{last_name} #{first_name}"
irb(main):004:1*   end
irb(main):005:0> end
irb(main):006:0> yamada = User.new(:Yamada, :Taro)
irb(main):007:0> tanaka = User.new(:Tanaka, :Jiro)
irb(main):008:0> yamada.full_name
=> "Yamada Taro"
irb(main):009:0> yamada.to_h
=> {:last_name=>:Yamada, :first_name=>:Taro}
irb(main):010:0> yamada2 = User.new(:Yamada, :Taro)
irb(main):011:0> yamada == yamada2
=> true
irb(main):012:0> yamada == tanaka
=> false
irb(main):013:0> h = {yamada => :foo, tanaka => :bar}
irb(main):014:0> [yamada, tanaka] & [yamada2]
=> [#<struct User last_name=:Yamada, first_name=:Taro>]

だいたいいけますね。


どうでしょうか。Ruby もなかなかいけるでしょう?

古い Ruby の define_method

AtCoder の過去問をやっていて、手元では通るコードがことごとく RE になる理由が全然わからなかった。いろいろ考えてみたが、コードはどう考えても正しい気がする。


ふと、自分は横着して何も考えずに Ruby 2.7.0 を使っていたが、AtCoderRuby は 2.3.3 なことにハッと気づいた。たまたま rbenv で Ruby 2.3.3 を既にインストールしてあったので、$ rbenv local で切り替えて Ruby 2.3.3 で実行してみたところ、何とエラーが出る。これだったのか。

詳しくは書かないけれど、2.7.0 のコードは Module#define_method をこんな風に使っていた。

class Hoge
end

a = 1

Hoge.define_method(:output) do
  puts a
end

Hoge.new.output    #=>1

ローカル変数 a をふつうはスコープの外であるメソッド内と共有しようという意図である。まあ、この手のコード自体がよいものかは別だが、とにかくこれは 2.7.0 で動く。


しかしこれ、2.3.3 ではエラーが出る。何故かというと、2.3.3 では Module#define_method がプライベート・メソッドだからである。つまり、レシーバーを付けて呼び出せないようになっている。
解決方法はある。Object#send を使えばよい。これを使えば、御存知のとおりプライベート・メソッドまで呼び出せてしまう。

Hoge.send(:define_method, :output) do
  puts a
end

これで AtCoder でも動いた。

なお、Module#defile_method がパブリック・メソッドになったのは、たぶん 2.5.0 からである。るりまで調べてみた。