Rubyist가 Python을 쓸 때 — 같은 듯 다른 두 언어의 속사정
Ruby의 블록, 오픈 클래스, 암묵적 반환이 Python에서는 어떻게 되는가
Ruby와 Python은 같은 시대에 태어난 동적 타입 언어다. 둘 다 Perl의 영향을 받았고, 둘 다 "개발자의 행복"을 중시한다고 한다. 근데 쓰다 보면 철학이 완전히 다르다는 걸 체감한다.
블록이 없다
Rubyist가 Python에서 가장 먼저 부딪히는 벽이 이거다.
# Ruby — 블록으로 리소스 관리
File.open('data.txt') do |f|
puts f.read
end
# 여기서 f는 이미 닫혀 있다
# Python — with문(context manager)
with open('data.txt') as f:
print(f.read())
# 여기서 f는 이미 닫혀 있다
결과는 같다. 근데 메커니즘이 다르다. Ruby는 블록이라는 범용 구조가 있어서, File.open이 블록을 받아 yield 하고, ensure로 정리한다. Python은 __enter__/__exit__ 프로토콜이라는 전용 메커니즘을 만들었다.
Ruby 블록의 강력한 점은 아무 메서드에나 블록을 넘길 수 있다는 것이다. 3.times { puts 'hello' } 같은 게 가능한 이유. Python에서 이런 패턴은 lambda나 함수를 인자로 넘기는 걸로 대체한다. 가능하긴 한데, 블록만큼 자연스럽지 않다.
모든 것이 표현식(expression) vs 문(statement)
Ruby에서는 거의 모든 게 값을 반환한다.
# Ruby — if는 표현식이다
status = if score >= 90 then 'A' else 'B' end
# 메서드의 마지막 줄이 암묵적 반환
def greet(name)
"Hello, #{name}"
end
# Python — if는 문(statement)이다
# 한 줄 삼항 연산자는 있다
status = 'A' if score >= 90 else 'B'
# return을 반드시 써야 한다
def greet(name):
return f"Hello, {name}"
Python에서 return을 빼먹으면 None이 반환된다. Ruby 습관으로 마지막 줄만 쓰고 return을 안 넣으면 디버깅에 시간을 쏟게 된다. 이거 한 번 겪으면 잊히지 않는다.
오픈 클래스 vs 닫힌 클래스
Ruby의 오픈 클래스(monkey patching)는 양날의 검이다.
# Ruby — 기존 클래스를 아무 데서나 열 수 있다
class String
def shout
upcase + '!!!'
end
end
'hello'.shout # => 'HELLO!!!'
Python에서는 이게 안 된다. 내장 타입(str, int, list)은 C로 구현돼 있어서 수정 자체가 불가능하다.
# Python — 이러면 AttributeError
str.shout = lambda self: self.upper() + '!!!'
# TypeError: cannot set 'shout' attribute of immutable type 'str'
Python에서 비슷한 걸 하려면 상속하거나, 독립 함수를 만든다. "기존 클래스를 건드리지 않는다"가 Python의 철학이다. Rails의 ActiveSupport가 3.days.ago를 가능하게 하는 마법 — Python 세계에서는 이런 걸 안 한다. 좋게 말하면 예측 가능하고, 아쉽게 말하면 표현력이 좀 떨어진다.
Enumerable vs itertools + comprehension
Ruby의 Enumerable은 정말 편하다.
# Ruby
users.select { |u| u.active? }.map(&:name).first(5)
# Python — list comprehension이 더 Pythonic
[u.name for u in users if u.active][:5]
Ruby는 메서드 체이닝이 자연스럽다. .select.map.first가 왼쪽에서 오른쪽으로 읽힌다. Python은 list comprehension이라는 독자적인 문법을 쓴다. 처음에는 어색한데, 익숙해지면 오히려 간결하다.
근데 복잡한 변환이 3단계 이상 들어가면 comprehension이 읽기 힘들어진다. 그때 Ruby의 체이닝이 그리워진다.
# 이건 좀... 읽기 힘들다
result = {k: v for k, v in sorted(
((name, sum(scores)) for name, scores in data.items()),
key=lambda x: -x[1]
)[:10]}
# Ruby라면
data.transform_values(&:sum).sort_by { |_, v| -v }.first(10).to_h
명시적 self vs 암묵적 self
Python 클래스에서 메서드의 첫 번째 인자가 self인 거, Rubyist한테는 꽤 거슬린다.
# Python — self를 매번 써야 한다
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
# Ruby — self는 암묵적
class User
def initialize(name)
@name = name
end
def greet
"Hello, #{@name}"
end
end
Python의 "Explicit is better than implicit" (PEP 20, The Zen of Python). Ruby의 "프로그래머의 행복을 위해 타이핑을 줄인다". 이 차이가 self 하나에도 드러난다.
결국 어느 쪽이 좋은가?
질문이 잘못됐다. Ruby는 "개발자가 쓰기 좋은 코드"를 추구하고, Python은 "누구나 읽기 좋은 코드"를 추구한다. Ruby는 DSL을 만들기 좋고, Python은 팀 프로젝트에서 일관성을 유지하기 좋다.
Rubyist가 Python을 쓸 때 가장 힘든 건 기능의 부재가 아니라 미학의 차이다. 할 수 있는 건 거의 비슷한데, "어떻게 하는 게 좋은 코드인가"의 답이 다르다.
핵심 포인트
Ruby 블록 → Python context manager(with) + lambda — 범용 vs 전용 메커니즘
Ruby는 암묵적 반환, Python은 명시적 return 필수 — 빼먹으면 None
Ruby 오픈 클래스 vs Python 닫힌 내장 타입 — monkey patching 불가
Enumerable 체이닝 vs list comprehension — 3단계 넘으면 체이닝이 그립다
명시적 self vs 암묵적 self — "Explicit is better than implicit" vs "개발자의 행복"