TIL

23.04.25 TIL - defaultdict, lambda, callable

best_spear_man 2023. 4. 25. 21:28

요약: 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