23.04.25 TIL - defaultdict, lambda, callable
요약: defaultdict, callable, __call__()
페어프로그래밍: 주차요금계산을 풀면서 다른사람의 정답코드를 보고 궁금했던 것들을 찾아 정리했다.
1. 주차요금계산
문제상황: 시간순으로 입/출차 기록과 요금표가 입력으로 주어질 때, 각 차량의 주차요금을 구하여 차량번호 기준 오름차순으로 정렬한 요금의 리스트를 반환해야한다.
시도-공통: 시간 처리, 반복문 사용, 남은 차 처리
def str_to_min(t):
h, m = t.split(":")
return int(h) * 60 + int(m)
먼저 split을 이용해 입/출차 시간을 분단위 정수로 바꾸어주는 함수를 정의한다.
def solution(fees, records):
parking = {}
minuites = {}
for record in records:
...
# del parking[차량번호]
#끝까지 출차안한 차 처리
for car, in_ in parking.items():
...
return answer
기록은 시간순으로 주어지므로, 처음부터 순차적으로 확인하여 입차한 차는 딕셔너리에 저장하고 출차한 차는 딕셔너리에서 빼면서 시간/요금을 계산한다. 그리고 모든 기록을 확인했음에도 출차하지 않고 남아있는 차를 23:59에 출차시키고 시간/요금을 계산한다.
시도 1: 출차시 바로 요금계산
def solution(fees, records):
parking = {}
resume = {}
for record in records:
t, car, io_ = record.split(" ")
t = str_to_min(t)
if io_ == "IN": # 입차 시
parking[car] = t # 주차장에 입차시간과 함께 저장
else: # 출차 시
in_ = parking.pop(car) # 주차장에서 제거, 입차시간 확인
time_ = t - in_
resume.setdefault(car, 0)
# 요금계산
if time_ <= fees[0]:
resume[car] = fees[1]
else:
resume[car] = fees[1] + fees[3] * (
(time_ - fees[0] + fees[2] - 1) // fees[2]
)
# 주차장에 남은 차량 처리
for car, in_ in parking.items():
time_ = 24 * 60 - 1 - in_
resume.setdefault(car, 0)
# 요금계산
...
return [time_ for _, time_ in sorted(resume.items(), key=lambda x: x[0])]
# 차량번호로 정렬해 반환
출차시 이전 입차 시간을 조회해 주차 시간을 계산하고 이에 따른 요금을 계산하는 방식으로 구현하였다.
올림 연산: 정수이므로 math 모듈을쓰지 않고 '단위시간 - 1' 을 더해 내림하는 것으로 구현하였다.
차량번호 정렬: 같은 길이(자릿수)의 차량번호 문자열을 사전식으로 비교해 정렬했다.
위 방식의 오류:
입력 1의 첫번째 차량(0000)의 경우 34분 주차 + 300분 주차를 하였다. 이 방식으로 계산하면 기본요금 5000원+ 기본요금 5000원 + 120/10*600=12200원의 요금이 나오는데 이는 정답과 다르다.
해결 1. 시간계산과 요금 게산 분리
출차시엔 얼마나 오래 주차했는지만 분 단위로 기록하고, 모든 차량의 주차시간 정산이 끝난 뒤 요금을 계산한다.
def solution(fees, records):
parking = {}
minuites = {}
for record in records:
t, car, io_ = record.split(" ")
t = str_to_min(t)
if io_ == "IN": # 입차
parking[car] = t
else: # 출차
in_ = parking.pop(car)
minuites.setdefault(car, 0)
minuites[car] += t - in_ # 주차 시간 누적
for car, in_ in parking.items(): # 끝까지 출차안한 차 처리
...
answer = []
# 차량 번호순서대로 정렬후 요금 계산
for _, time_ in sorted(minuites.items(), key=lambda x: x[0]):
if time_ < fees[0]:
answer.append(fees[1])
else:
answer.append(
fees[1] + fees[3] * ((time_ - fees[0] + fees[2] - 1) // fees[2])
)
return answer
다른 사람의 흥미로운 풀이: 클래스, defaultdict, lambda 이용하기
from math import ceil
class Parking:
def __init__(self, fees):
self.fees = fees
self.in_flag = False
self.in_time = 0
self.total = 0
def update(self, t, inout):
self.in_flag = True if inout=='IN' else False
if self.in_flag:
self.in_time = str2int(t)
else:
self.total += (str2int(t)-self.in_time)
def calc_fee(self):
if self.in_flag:
self.update('23:59', 'out')
add_t = self.total - self.fees[0]
return self.fees[1] + ceil(add_t/self.fees[2]) * self.fees[3] if add_t >= 0 else self.fees[1]
먼저 위와같은 클래스 하나를 만든다. 이 클래스는 각 차량별 주차여부(in_flag), 입차 시간(in_time), 총 주차시간(total)을 속성으로 가진다. update 메서드로 입차시 입차 시간을 기록하고출차시 총 주차시간을 갱신한다. calc_fee 메서드를 이용해 총 주차시간에 대한 주차비용을 계산한다.
from collections import defaultdict
def solution(fees, records):
recordsDict = defaultdict(lambda:Parking(fees))
for rcd in records:
t, car, inout = rcd.split()
recordsDict[car].update(t, inout)
return [v.calc_fee() for k, v in sorted(recordsDict.items())]
solution 함수에서는 defaultdict 클래스와 lambda를 이용해 차량번호를 키값으로 가지고 Parking객체를 값으로 갖는 유사 딕셔너리 recordsDict 를 만든다. 이후 입출차 기록을 순차적으로 조회하며 update를 실행하고 최종적으로 calc_fee() 메서드를 이용해 각 차량별 요금을 계산한다.
의문 1. defaultdict는 무엇인가?
한줄 요약: 호출가능한 함수/클래스 등을 인자로 받아 그 반환을 기본값으로 사용하는 유사 딕셔너리.
collections 모듈에서 제공하는 dictionary의 서브 클래스이다. 매개변수로 default_factory를 받는데, 여기에는 함수나 클래스 등 callable한 인자를 주어야한다. __missing__(key) 메소드가 새로 정의되는데, 이 메소드는 dict의 __getitem__() 메서드가 실행될 때(조회할 때) 키를 찾을 수 없을 경우 실행된다.
default_factory는 기본값이 None이며, 아무것도 지정하지 않을 경우엔 조회할 키가 존재하지 않으면 __missing__()이 에러를 낸다. (즉 기본 딕셔너리와 똑같이 동작한다)
만약 default_factory가 None이 아니면, default_factory가 인자 없이 호출되어 반환된 값이나 객체가 반환+딕셔너리에 저장된다.
__missing__()은 오직 __getitem__()에만 호출되므로 get() 메서드를 사용할 때는 default_factory가 호출되지 않는다.
예시 1: 간단한 예시
from collections import defaultdict
def foo():
return 10
a = defaultdict(foo)
a[1] = 5
print(a["a"], a[1], a[0]) # 10, 5, 10
def bar(x): # 인자를 받아야하는 경우는 안된다!
return 100
b = defaultdict(bar)
print(b[0]) # TypeError: bar() missing 1 required positional argument: 'x'
예시 2: 리스트의 딕셔너리로 그룹화하기
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
d[k].append(v)
print(sorted(d.items()))
# [('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
예시 3: 이러한 성질을 이용해 .setdefault()를 사용하거나 if문을 사용해 count 하는 것을 대체할 수 있다.
def foo():
return 0
s = "aasdfasdfasdf"
d = defaultdict(foo)
for k in s:
d[k] += 1
print(dict(sorted(d.items())))
# {'a': 4, 'd': 3, 'f': 3, 's': 3}
응용: lambda 사용하기
위 문제에서 우리가 원하는 것은 어떤 딕셔너리에 기본값으로 Parking 객체가 들어가는 것이다. 그렇다면 풀이는 왜 그렇게 하지 않고 lambda를 사용하는가? 그것은 다음과 같이 간략화하여 설명할 수 있다.
간략화된 문제: 나는 defaultdict를 이용하여 기본값이 어떤 문자열을 정수형으로 바꾼 값으로 지정된 딕셔너리를 만들고 싶다.
string='1234'
my_dict = defaultdict(int(string))
print(my_dict[10])
# TypeError: first argument must be callable or None
에러가 뜨는 것을 볼 수 있다. 왜냐하면 default_factory에는 callable한 객체가 와야하기 때문이다.
그러므로 간단하게 lambda를 사용하여 해결할 수 있다.
string='1234'
my_dict = defaultdict(lambda:int(string))
print(my_dict[10])
# 1234
응용 2: 람다를 이용한 추상화
class Double:
def run(self,x):
return x*2
class Triple:
def run(self,x):
return x*3
class Multiply:
def __init__(self,mult):
self.mult = mult
def run(self,x):
return x*self.mult
class Library:
def __init__(self,c):
self.c = c()
def Op(self,val):
return self.c.run(val)
op1 = Double
op2 = Triple
op3 = Multiply(5)
lib1 = Library(op1)
lib2 = Library(op2)
lib3 = Library(op3)
print(lib1.Op(2)) # 4
print(lib2.Op(2)) # 6
print(lib3.Op(2)) # TypeError: 'Multiply' object is not callable
위 코드는 Library객체를 통해 다른 세 클래스의 연산을 추상화하려다가 실패하였다. 그 이유는 Multiply(5) 로 이미 한번 호출되어 생성된 객체는 호출할 수 없는 객체이기 때문이다. lambda를 이용해 다음과 같이 바꾸면 해결가능하다.
op3 = lambda: Multiply(5)
의문 2: callable한 객체란?
괄호를 이용해 호출하고 실행가능한 클래스 인스턴스, 함수, 메서드 등의 객체를 말한다. 모든 함수,메서드는 callable하다. enumerate, int, list 클래스 등 도 callable하다.
어떠한 객체를 callable하게 만들고 싶을땐, __call__() 메소드를 정의해주면 된다.
class Person:
def __init__(self, name):
self.name = name
def __call__(self, opponent):
print(f"Hi, {opponent}! My name is {self.name}")
tommy = Person("Tommy")
tommy("Nami")
# Hi, Nami! My name is Tommy
이를 이용해 위의 문제를 다음과 같이 해결할 수 있다.
class Multiply:
def __init__(self, mult):
self.mult = mult
def __call__(self):
return self
def run(self, x):
return x * self.mult
어떤 객체가 callable한지 알고 싶을 땐 내장함수 callable을 사용해보면 된다.
tommy = Person("Tommy")
callable(tommy) # True
배운 점
defaultdict의 정의와 활용법을 알게되었다.
callable의 명확한 뜻을 알게 되었다.
lambda의 새로운 용도를 알게되었다.
레퍼런스
https://etloveguitar.tistory.com/142
[python] callable이란?
python에서 callable이란 호출가능한 클래스 인스턴스, 함수, 메서드 등 객체를 의미한다. 참고로 파이썬에서는 모든 것이 객체기 때문에, 함수도 하나의 객체다. 함수 안에 data variable, 또 다른 함수
etloveguitar.tistory.com
https://www.pythonmorsels.com/callables/
The meaning of "callable" in Python
A callable is a function-like object, meaning it's something that behaves like a function. The primary types of callables in Python are functions and classes, though other callable objects do exist.
www.pythonmorsels.com
https://docs.python.org/ko/3/library/collections.html#defaultdict-objects
collections — Container datatypes
Source code: Lib/collections/__init__.py This module implements specialized container datatypes providing alternatives to Python’s general purpose built-in containers, dict, list, set, and tuple.,,...
docs.python.org
https://stackoverflow.com/questions/360368/lambda-function-for-classes-in-python
Lambda function for classes in python?
There must be an easy way to do this, but somehow I can wrap my head around it. The best way I can describe what I want is a lambda function for a class. I have a library that expects as an argumen...
stackoverflow.com