2. pythonic 코드

hyuckhoon.ko·2023년 10월 21일
0
post-thumbnail

이 장에서는 파이썬스럽게 표현하는 방법을 배운다.

우리는 왜 파이썬스러운 코드를 작성해야 할까?

  • 일반적으로 성능이 더 좋다.
  • 코드가 더 작고 이해하기 쉽다.
  • 전체 개발팀이 동일한 패턴 구조에 익숙해지면 실수가 줄어들고 문제의 본질에 더 집중할 수 있게 된다.

목표

  • 인덱스와 슬라이스를 이해하기
  • 시퀀스와 이터러블 구현하기
  • 컨텍스트 관리자 이해하기
  • 매직 메서드 활용하여 파이썬스러운 코드 구현하기
  • 파이썬에서 부작용 유발하는 실수 피하기

1️⃣ 인덱스와 슬라이스

[1] 하나의 요소를 얻기

1) 마지막 인덱스 접근

>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[-1]
9

2) 음의 정수 인덱스로 요소 접근

>>> my_numbers[-4]
4

3) 에러 발생하는 케이스

>>> my_numbers[-5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range

4) 그림으로 확인하기

요소4539
인덱스0123
인덱스-4-3-2-1


[2] 특정 구간의 요소 구하기

1) 시작과 끝 인덱스 명시

>>> my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
>>> my_numbers[2:5]
(2, 3, 5)

[시작:끝] 단, 끝 인덱스는 제외한 구간의 값을 가져온다.
따라서, 결괏값이 (2, 3, 5, 8) 이 아닌 (2, 3, 5)가 된다.

2) 시작 인덱스 제외

>>> my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
>>> my_numbers[:3]
(1, 1, 2)

3) 끝 인덱스 제외

>>> my_numbers = (1, 1, 2, 3, 5, 8, 13, 21)
>>> my_numbers[3:]
(3, 5, 8, 13, 21)

4) 시작, 끝 인덱스 제외

>>> my_numbers[:]
(1, 1, 2, 3, 5, 8, 13, 21)

이 경우 my_numbers의 복사본을 만든다.

5) 시작, 끝 인덱스 그리고 간격 파라미터 제외

>>> my_numbers[::]
(1, 1, 2, 3, 5, 8, 13, 21)

마찬가지로 이 경우에도 my_numbers의 복사본을 만든다.

복사본이라고 표현하길래, 설마 복사본이라는게 깊은 복사를 말하는건가? 하고 궁금해졌다.
id 값을 확인해보고 싶었다.

>>> id(my_numbers)
4338114848
>>> id(my_numbers[:])
4338114848
>>> id(my_numbers[::])
4338114848

역시나 얕은 복사다.

6) 간격 파라미터 사용

>>> my_numbers[1:7:2]
(1, 3, 8)

# 내장 객체 slice 사용
>>> interval = slice(1, 7, 2)
>>> my_numbers[interval]
(1, 3, 8)
>>> interval = slice(None, 3)
>>> interval
slice(None, 3, None)
>>> my_numbers[interval]
(1, 1, 2)

slice(시작, 중지, 간격) 중 지정하지 않은 파라미터는 None으로 설정되고 있다.

TIP 튜플, 문자열, 리스트의 특정 요소를 가져오려고 한다면 for루프를 돌며 수작업으로 요소를 선택하지 말고 이와 같은 방법을 사용하자.

💫 자체 시퀀스 생성

🧐 인덱스 및 슬라이스는 어떻게 가능했나?

시퀀스는 __getitem____len__을 모두 구현한 객체다.

그렇다면 표준 라이브러리 객체를 감싸는 사용자 정의 클래스를 만들어 보자.
예를 들어, 만들려고 하는 클래스가 리스트의 래퍼라면,
리스트의 동일한 메서드를 호출하여 호환성을 유지할 수 있다.

from collections.abc import Sequence


class Items:
    def __init__(self, *values):
    	# 이로써, self._valus는 리스트다.
        # Items 클래스는 리스트의 래퍼 클래스가 됐다.
        self._values = list(values)

    def __len__(self):
        return len(self._values)

    def __getitem__(self, item):
        return self._values.__getitem__(item)

사용자 정의 시퀀스 구현 시, 주의사항

  • 1) 범위 인덱싱 결과는 해당 클래스와 같은 타입의 인스턴스여야 한다.

    • 리스트의 일부를 가져오면 그 결과는 리스트여야 한다.
      - 튜플의 특정 범위를 요청하면 그 결과는 튜플이어야 한다.
    • substring의 결과는 문자열이다.
      -> 각각의 경우 원본 객체와 동일한 타입임을 확인할 수 있다.

    ex) range가 대표적인 좋은 예

>>> range(1, 100)[25: 50]
range(26, 51)
  • 2) slice 범위는 파이썬이 하는 것처럼 마지막 요소는 제외해야 한다.
    파이선 개발자는 이미 slice, range가 작동하는 방식에 익숙하다.
    사용자 정의 클래스에서 예외를 만들지 말자.
>> list(range(1, 10))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

2️⃣ 컨텍스트 관리자

어떤 중요한 작업 전후에 실행해야하는 코드가 있을 때 유요한 기능이다.
일반적으로 리소스 관리와 관련하여 자주 볼 수 있다.

  • 파일을 열면 파일 디스크립터 누수를 막기 위해 작업이 끝나면 적절히 닫히길 기대한다.
  • 서비스나 소켓에 대한 연결을 열었을 때도 적절하게 닫거나 임시 파일을 제거하는 등의 작업을 해야 한다.
# 우아하지 않은 방법
fd = open(file_name)
try:
    process_file(fd)
finally:
    fd.close()

# 우아한 방법
with open(fine_name) as fd:
    process_file(fd)

with문은 컨텍스트 관리자로 진입하게 한다.
예외 발생 시에도 블록이 완료되며 파일이 닫힌다.

컨텍스트 관리자는 __enter____exit__ 두 개의 매직 메서드로 구현된다.

def stop_database():
    run("systemctl stop postgresql.service")


def start_database():
    run("system start postgresql.service")


def db_backup():
    run("pg_dump database")


class DBHandler:
	# 이 케이스의 경우 __enter__의 리턴값은 쓸모가 없다. 
    # 하지만 일반적으로 __enter__가 무언가를 반환하도록 하는 것이 좋다.
    def __enter__(self):
        stop_database()
        return self
	# __exit__ 메서드 실행 시, 발생한 에러를 파라미터로 받을 수 있다.
    # 블록에 예외가 없다면 모두 None이 할당된다.
    def __exit__(self, exc_type, exc_val, exc_tb):
        start_database()


def main():
    with DBHandler():
        db_backup()

__exit__의 리턴값에 유의해야 한다.
특별한 작업을 할 필요가 없다면 아무것도 반환하지 않아도 된다.
만약 return True를 하게되면, 발생한 에러를 호출자에게 전파하지 않겠다는 것을 의미한다. 절대로 오류를 조용히 무시하면 안 된다.

[1] 컨텍스트 관리자 구현

contextlib 모듈을 사용하여 보다 쉽게 컨텍스트 관리자를 구현할 수 있다.

1) @contextlib - 데코레이터 사용

import contextlib


@contextlib.contextmanager
def db_handler():
    try:
        stop_database()
        yield
    finally:
        start_database()


with db_handler():
    db_backup()
  • yield문 앞까지 선언된 모든 코드는 __enter__메서드의 일부처럼 취급된다.
  • yield문에서 아무것도 리턴하고 있지 않으므로 None이 반환되고 있다.

db_backup 로직이 실행이 완료되면, yield 다음의 코드가 실행된다.
즉, yield 문 이후의 코드는 __exit__의 로직으로 볼 수 있다.

yield 문을 기준으로 __enter____exit__이 나뉜다.

db_backup 함수를 수정하기 편리하다.


2) contextlib.ContextDecorator 클래스

컨텍스트 관리자 안에서 실행될 함수에 데코레이터를 적용하기 위한 로직을 제공하는 믹스인 클래스(유틸리티 클래스)다. 컨텍스트 관리자 자체의 로직은 직접 매직 메서드를 구현해야 한다.

class dbhandler_decorator(contextlib.ContextDecorator):
    def __enter__(self):
        stop_database()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        start_database()


@dbhandler_decorator
def offline_backup():
    run("pg_dump database")

위 두 가지 방법의 차이점은 무엇일까?

  • with 문의 유무
  • 독립성
    - 데코레이터는 offline_backup이 무슨 일을 하는지 모른다?!

3) contextlib.suppress

import contextlib


with contextlib.suppress(DataConversionException):
    parse_data(input_json_or_dict)

안전하다고 확신하는 경우 특정 예외를 무시하는 기능이다.

import contextlib


def render_data():
    print(1 + "1")


with contextlib.suppress(ValueError):
    render_data()

결과

print(1 +"1")

TypeError: unsupported operand type(s) for +: 'int' and 'str'

TypeError를 무시하도록 수정해보자.

with contextlib.suppress(TypeError):
    render_data()

결과

Process finished with exit code 0

컨텍스트 관리자는 파이썬을 차별화하는 상당히 독특한 기능이다.
따라서 컨텍스트 관리자를 자주 사용하자.



3️⃣ 컴프리핸션과 할당 표현식

컴프리핸션을 사용하면 코드를 보다 간결하게 작성할 수 있고, 가독성도 높아진다.

하지만 컴프리핸션을 적용하면 오히려 코드가 더 복잡해질 수 있다.
즉, for문을 사용하는 것이 나은 선택일 수 있다는 말이다.

이때 할당 표현식도 함께 고려해보자.

numbers = []
for i in range(10):
	# list.appned를 반복적으로 호출(성능 좋지 않다.)
    numbers.append(i)

numbers = [run_calculation(i) for i in range(10)]

[할당 표현식 사용 전]

from typing import Iterable, Set


def collect_account_ids_from_arns(arns: Iterable[str]) -> Set[str]:
    """
    arn:partition:service:region:account-id:resource-id 형태의 ARN들이 주어진 경우
    고유한 계정 ID (account_id)를 찾아서 반환
    """

    collected_account_ids = set()
    for arn in arns:
        matched = re.match(ARN_REGEX, arn)
        if matched:
            account_id = matched.groupdict()["account_id"]
            collected_account_ids.add(account_id)

    return collected_account_ids

[할당 표현식 적용 후]

def collect_account_ids_from_arns(arns):
	# None이 아닌 것들만 남긴다.
    matched_arns = filter(None, (re.match(ARN_REGEX, arn) for arn in arns))
    return {m.groupdict()["account_id"] for m in matched_arns}

[할당 표현식(python 3.8, PEP-572) 도입 이후]

def collect_account_ids_from_arns(arns):
    return {
        matched.groupdict()["account_id"]
        for arn in arns
        if (matched := re.match(ARN_REGEX, arn)) is not None
    }

더 간결한 코드가 항상 더 나은 코드를 의미하는 것은 아니다.

TIP 컴프리핸션의 가독성을 생각해야 하며, 정말로 이해하기 쉬운 코드가 되는 것이 아니라면, 한 줄 코드를 만들기 위해 노력하지 않도록 유의하자.



4️⃣ 프로퍼니, 속성(Attribute)과 객체 메서드의 다른 타입들

파이썬에서는 변수명 지정과 관련하여 몇 가지 네이밍 컨벤션이 있다.
밑줄로 시작하는 속성은 private 속성으로써, 외부에서 호출되지 않기를 기대한다는 의미다.

1) 파이썬에서의 밑줄

>> conn = Connector("postgresql://localhost")
>> conn.source
'postgresql://localhost'
>> conn._timeout
60
>> conn.__dict__
{'source': 'postgresql://localhost', '_timeout': 60}

TIP 클래스는 외부 호출 객체와 관련된 속성과 메서드만 노출해야 한다. 즉 객체의 인터페이스로 공개하는 용도가 아니라면 모든 멤버는 접두사로 하나의 밑줄을 사용하는 것이 좋다.

private 속성과 private 메서드를 만들 수 있다는 오해가 있다.

class Connector:
    def __init__(self, source):
       self.source = source
       self.__timeout = 60
        
    def connect(self):
       print("connecting with {0}s".format(self.__timeout))
       
>> conn = Connector("mysql://localhost")
>> conn.connect()
connecting with 60s

>> conn.__timeout
Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 364, in runcode
    coro = func()
  File "<input>", line 1, in <module>
AttributeError: 'Connector' object has no attribute '__timeout'

"이것은 private이다" 또는 "이것은 접근할 수 없다" 식의 에러가 아니다.
AttributeError로 단지 존재하지 않는다고만 하고 있다.

밑줄 두 개를 사용하면 파이썬은 다른 이름을 만든다.
이름 맹글링(name mangling)으로, 이중 밑줄을 사용한 변수의 이름을 _<class_name>__<attribute-name>형태로 변경해 버린다.
따라서, _Connector__timeout이라는 속성이 생기고 접근이 가능하다.

>> vars(conn)
{'source': 'mysql://localhost', '_Connector__timeout': 60}

>> conn._Connector__timeout = 15
>> conn.connect()
connecting with 15s

파이썬에서 이중 밑줄을 사용하는 것은 완전히 다른 경우를 위한 것이다.
여러 번 확장되는 클래스의 메서드를 이름 충돌 없이 오버라이드하기 위해서 만들어졌다.

프로퍼티

일반적으로 객체 지향 설계에서는 도메인 엔터티를 추상화하는 객체를 만든다.
이러한 객체는 어떤 동작이나 데이터를 캡슐화할 수 있다.
그리고 종종 데이터의 정확성이 객체를 생성할 수 있는지 여부를 결정한다.
다시 말하면, 일부 엔터티는 데이터가 특정 값을 가질 경우에만 존재할 수 있고, 잘못된 값을 가진 경우에는 존재할 수 없다.

이것이(일반적으로 setter 작업에 사용되는) 유효성 검사 메서드를 만드는 이유이다.
그러나 파이썬에서는 프로퍼티를 사용하여 setter와 getter 메서드를 더 간결하게 캡슐화할 수 있다.

ex) 좌표 값을 처리하는 지리 시스템

class Coordinate:
    def __init__(self, lat: float, long: float) -> None:
        self._latitude = self._longitude = None
        self.latitude = lat
        self.longitude = long
        
    @property
    def latitude(self) -> float:
        return self._latitude
    
    @latitude.setter
    def latitude(self, lat_value: float) -> None:
        if lat_value not in range(-90, 90 + 1):
            raise ValueError(f"유효하지 않은 위도 값: {lat_value}")
        self._latitude = lat_value
        
    @property
    def longitude(self) -> float:
        return self._longitude
    
    @longitude.setter
    def longitude(self, long_value: float) -> None:

여기에서 프로퍼티는 latitude와 longitude를 정의하기 위해 사용됐다.
변수에 저장된 값을 반환하는 별도의 속성을 만들었다.
더 중요한 것은 사용자가 다음과 같은 방법으로 이러한 속성 중 하나를 수정하려는 경우이다.

coordinate.latitude = <new-latitude-value> 
coordinate.longitude = <new-longitude-value>

@latitude.setter, @longitude.setter 데코레이터로 선언된 유효성 검사 로직이 자동 호출된다.

TIP 객체의 모든 속성에 대해 get, set 메서드를 작성할 필요는 없다.
대부분의 경우 일반 속성을 사용하는 것으로 충분하다.
속성 값을 가져오거나 수정할 때 특별한 로직이 필요한 경우에만 프로퍼티를 사용하자.

프로퍼티는 명령-쿼리 분리 원칙을 따르기 위한 좋은 방법이다.
명령-쿼리 분리 원칙은 객체의 메서드가 무언가의 상태를 변경하는 커맨드이거나 무언가의 값을 반환하는 쿼리이거나 둘 중 하나만 수행해야 한다는 것이다.

ex) set_email 메서드에 대한 고찰

if self.set_email("a@j.com")

이 코드는 무엇을 의미하는 것일까?
a@j.com으로 이메일을 설정하려는 것인가?
이미 이메일이 해당 값으로 설정 돼 있는지 확인하는 걸까?
아니면 동시에 이메일 값을 설정하고 유효한지 체크하려는 걸까?

프로퍼티를 사용하면 이러 혼동을 피할 수 있다.

@property 데코레이터는 무언가에 응답하기 위한 쿼리이고,
@<property_name>.setter는 무언가를 하기 위한 커맨드이다.

3) 보다 간결한 구문으로 클래스 만들기

파이썬에서는 객체의 값을 초기화하는
일반적인 보일러 플레이트(boilerplate code: 모든 프로젝트에서 공통적으로 반복해서 사용되는 코드) 코드가 있다.

def __init__(self, x, y):
	self.x = x
    self.y = y

파이썬 3.7부터는 dataclasses 모듈을 사용하여 위의 코드를 훨씬 단순화할 수 있게 됐다.

  • @dataclass 데코레이터를 사용하면 __init__ 메서드를 자동으로 생성한다.
  • @datablass 데코레이터는 field 객체도 제공한다. 해당 속성에 특별한 특징이 있음을 표시한다.
@dataclass
class Foo:
    bar: List = []
    
# raise ValueError(f'mutable default {type(f.default)} for field '
# ValueError: mutable default <class 'list'> for field bar is not allowed: use default_factory
@dataclass
class Foo:
    x: list = field(default_factory=list)


assert Foo().x is not Foo().x
from typing import List
from dataclasses import dataclass, field

R = 26


@dataclass
class RTrieNode:
    size = R
    value: int
    next_: List["RTrieNode"] = field(default_factory=lambda: [None] * R)

    def __post_init__(self):
        if len(self.next_) != self.size:
            raise ValueError(f"리스트(next_)의 길이가 유효하지 않음")

4) 이터러블 객체

파이썬의 반복은 이터러블 프로토콜이라는 자체 프로토콜을 사용해 동작한다.
파이썬은 고수준에서 다음 두 가지를 차례로 검사한다.

  • 객체가 __next____iter__ 메서드 중 하나를 포함하는지 여부
  • 객체가 시퀀스이고 __len____getitem__ 모두 가졌는지 여부

(1) 이터러블 객체 만들기

객체를 반복하려고 하면 파이썬은 해당 객체의 iter()함수를 호출한다.
이 함수가 처음으로 하는 것을 해당 객체에 __iter__ 메서드가 있는지 확인하는 것이다.

from datetime import timedelta


class DateRangeIterable:
    """자체 이터레이터 메서드를 가지고 있는 이터러블"""
    
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date
        
    def __iter__(self):
        return self 
    
    def __next__(self):  
        if self._present_day >= self.end_date:
            raise StopIteration()
        today = self._present_day
        self._present_day += timedelta(days=1)
        return today
@dataclass
class MyIterable:
    start: int
    end: int

    def __post_init__(self):
        if self.start > self.end:
            raise ValueError("시작값이 더 큽니다")

    def __iter__(self):
        return self

    def __next__(self):
        if self.start > self.end:
            raise StopIteration()
        current = self.start
        self.start += 1
        return current


for i in MyIterable(1, 3):
    print(i)

개선

@dataclass
class MyIterable:
    start: int
    end: int

    def __post_init__(self):
        if self.start > self.end:
            raise ValueError("시작값이 더 큽니다")

    def __iter__(self):
        current = self.start
        while current <= self.end:
            yield current
            current += 1


r1 = MyIterable(1, 3)
print(r1)
print(", ".join(map(str, r1)))
print(max(r1))
>>> from datetime import date
>>> for day in DateRangeIterable(date(2023, 1, 1), 	date(2023, 1, 5)):
    	print(day)
    
2023-01-01
2023-01-02
2023-01-03
2023-01-04

for 루프가 작동하는 원리는 StopIteration 예외가 발생할 때까지 next()를 호출하는 것과 같다.

r = DateRangeIterable(date(2023, 1, 1), date(2023, 1, 5))
next(r)
datetime.date(2023, 1, 1)
next(r)
datetime.date(2023, 1, 2)
next(r)
datetime.date(2023, 1, 3)
next(r)
datetime.date(2023, 1, 4)
next(r)
Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 364, in runcode
    coro = func()
  File "<input>", line 1, in <module>
  File "<input>", line 14, in __next__
StopIteration
class DateRangeIterable:
    """자체 이터레이터 메서드를 가지고 있는 이터러블"""
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date
    def __iter__(self):
        current_day = self.start_date
        while current_day < self.end_date:
            yield current_day
            current_day += timedelta(days=1)
            
r1 = DateRangeIterable(date(2023, 1, 1), date(2023, 1,5))
", ".join(map(str, r1))
'2023-01-01, 2023-01-02, 2023-01-03, 2023-01-04'
max(r1)
datetime.date(2023, 1, 4)

달라진 점은 for 루프는 __iter__를 호출하고 __iter__는 다시 제너레이터를 생성한다는 것이다.
이러한 형태의 객체를 컨테이너 이터러블(container iterable)이라고 한다.

(2) 시퀀스 만들기

객체에 __iter__ 메서드를 정의하지 않았지만 반복하기를 원하는 경우도 있다.
iter() 함수는 객체에 __iter__가 정의되어 있지 않으면 __getitem__을 찾고,
없으면 TypeError를 발생시킨다.

이터러블을 사용하면 메모리를 적게 사용하지만 n번째 요소를 얻기 위한 시간 복잡도는 O(n)이다.
시퀀스로 구현하면 더 많은 메모리가 사용되지만, 특정 요소를 가져오기 위한 인덱싱의 시간복잡도는 O(1)로 상수에 가능하다.

class DateRangeSequence:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._range = self._create_range()
        
    def _create_range(self):
        days = []
        current_day = self.start_date
        while current_day < self.end_date:
            days.append(current_day)
            current_day += timedelta(days=1)
        return days
    
    def __getitem__(self, day_no):
        return self._range[day_no]
    
    def __len__(self):
        return len(self._range)
        
        
s1 = DateRangeSequence(date(2023, 1, 1), date(2023, 1, 5))
for day in s1:
    print(day)
    
2023-01-01
2023-01-02
2023-01-03
2023-01-04


>> s1[1]
datetime.date(2023, 1, 2)
>> s1[0]
datetime.date(2023, 1, 1)
>> s1[-1]
datetime.date(2023, 1, 4)
>> s1[-2]
datetime.date(2023, 1, 3)

두 가지 구현 중 어떤 것을 사용할지 결정할 때 메모리와 CPU사이의 트레이드오프를 계산해보자. 일반적으로 이터레이션이 더 좋은 선택이지만 항상 그런 건 아니다.

(3) 컨테이너 객체

컨테이너 객체는 __contains__ 메서드를 구현한 객체다.
이 메서드는 in 키워드가 발견될 때 호출된다.

element in container

파이썬에서 이 코드는 다음과 같이 해석한다.

container.__contains__(element)
def mark_coordinate(grid, coord):
    if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
        grid[coord] = MARKED
  • if 문이 복잡하여 가독성 저하
  • 위치 결정 전 매번 if 문을 통해 grid 영역 안에 있는지 확인 필요

🧐 Gird 객체 스스로가 판단하게 할 수 없을까?

능동적으로 협력하는 객체 세계를 구축해보자.

class Boundaries:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __contains__(self, coord):
        x, y = coord
        return 0 <= x < self.width and 0 <= y < self.height


class Grid:
    def __int__(self, width, height):
        self.width = width
        self.height = height
        self.limits = Boundaries(width, height)

    def __contains__(self, coord):
        return coord in self.limits


def mark_coordinate(grid, coord):
    if coord in grid:
        grid[coord] = MARKED

(4) 객체의 동적인 속성

__getattr__ 매직 메서드를 사용하면 객체가 속성에 접근하는 방법을 제어할 수 있다.
우리가 <myobject>.<myattribute> 형태로 객체 속성에 접근할 때, 파이썬은 객체의 __dict__ 사전에서 검색을 한다.

class DynamicAttributes:
    def __init__(self, attribute):
        self.attribute = attribute
        
    def __getattr(self, attr):
        if attr.startswith("fallback_"):
            name = attr.replace("fallback_", "")
            return f"fallback resolved {name}]"
        raise AttributeError(f"{self.__class__.__name__}에는 {attr}속성이 없음")
    
    
>> dyn = DynamicAttributes("value")
>> dyn.attribute
'value'
>> dyn.fallback_test
Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/pydevconsole.py", line 364, in runcode
    coro = func()
  File "<input>", line 1, in <module>
AttributeError: 'DynamicAttributes' object has no attribute 'fallback_test'

>> dyn.__dict__["fallback_new"] = "new value"
>> dyn.fallback_new
'new value'

>> getattr(dyn, "something", "default")
'default'

__getattr__ 유용한 점

  • 다른 객체의 프록시 역할
  • 동적으로 계산되는 속성이 필요한 경우

단, __getattr__ 남용 시, 가독성이 떨어진다.

(5) 호출형 객체(callable)

객체를 일반 함수처럼 호출하면 __call__ 매직 메서드가 호출된다.
객체 호출 시 사용된 모든 파라미터는 __call__메서드에 그대로 전달된다.



from collections import defaultdict


class CallCount:
    def __init__(self):
        self._counts = defaultdict(int)
        
    def __call__(self, argument):
        self._counts[argument] += 1
        return self._counts[argument]
    

>> cc = CallCount()
>> cc(1)
1
>> cc(2)
1
>> cc(2)
2
>> cc(2)
3

cc("something")
1

>> callable(cc)
True

0개의 댓글