Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class.
공식문서 설명은 늘 어려움.
쉽게 말해, 부모나 형제 클래스의 임시 객체를 반환하고, 반환된 객체를 이용해 슈퍼 클래스의 메소드를 사용할 수 있음.
Cube가 아닌 Square기 때문에 super(Square, self)의 반환은 Square 클래스의 부모 클래스인 Rectangle 클래스의 임시 객체
결과적으로 Rectangle 인스턴스에서 area() 메소드를 찾음
Q. Square 클래스에 area 메소드를 구현하면??
그래도 super(Square, self) 가 Rectangle 클래스를 반환하기 때문에 Rectangle 인스턴스에서 area() 메소드를 호출
## super 클래스의 정의
class super(object):
def __init__(self, type1=None, type2=None): # known special case of super.__init__
"""
super() -> same as super(__class__, <first argument>)
super(type) -> unbound super object
**super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)**
Typical use to call a cooperative superclass method:
class C(B):
def meth(self, arg):
super().meth(arg)
This works for class methods too:
class C(B):
@classmethod
def cmeth(cls, arg):
super().cmeth(arg)
"""
# (copied from class doc)
두번째 argument : 첫번째 argument의 클래스 인스턴스를 넣어주거나 subclass를 넣어줘야함
(walrus := True) 는 assignment expression으로 walrus에 값 True를 할당한다.
둘의 미묘한 차이중 하나는 walrus = False는 값을 반환하지 않지만 (walrus := True)는 값을 반환한다는 것이다!
>>> walrus = False
>>> (walrus := True)
True
등장한 이유
PEP 572에 Abstract에 아래와 expression 내에서 변수에 할당하는 방법을 제안하고 있다.
creating a way to assign to variables within an expression using the notation NAME := expr.
C언어에서는 변수에 값을 할당하는 statement도 expression인데 강력하지만 찾기 힘든 버그를 생산하기도 한다.
int main(){
int x = 3, y = 8;
if (x = y) {
printf("x and y are equal (x = %d, y = %d)", x, y);
}
return 0;
}
x와 y값을 비교후 값이 같으면 두 값을 출력하는 코드지만 x와 y값이 다르기 때문에 아무것도 출력 안되길 기대되지만 실제 코드 실행 결과는 아래와 같이 print 문이 출력된다. 왜일까?
x and y are equal (x = 8, y = 8)
문제는 위 코드 세번째 줄 if (x = y) 에서 equality comparison operator(==) 대신 assignment operator(=) 를 사용하고 있기 때문이다. if 문의 조건에는 expression이 와야하는데 C언어에서는 x = y를 expression으로 x값이 8로 할당되고 1이상의 값으로 True로 판단되서 print문이 출력된다.
그럼 Python에서는?
x, y = 3, 8
if x = y:
print(f"x and y are equal ({x = }, {y = })")
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Syntax Error를 내뱉는데 expression이 아닌 statement이기 때문이다. 파이썬은 이를 분명히 구분하고 walrus operator에도 이러한 설계 원칙이 반영되었다. 그래서 walrus operator를 이용해서 일반적인 assignment를 할 수 없다.
위 수식을 이용해 오슬로(59.9°N 10.8°E) 와 밴쿠버(49.3°N 123.1°W) 사이의 거리를 구하면,
from math import asin, cos, radians, sin, sqrt
# Approximate radius of Earth in kilometers
rad = 6371
# Locations of Oslo and Vancouver
ϕ1, λ1 = radians(59.9), radians(10.8)
ϕ2, λ2 = radians(49.3), radians(-123.1)
# Distance between Oslo and Vancouver
print(2 * rad * asin(
sqrt(
sin((ϕ2 - ϕ1) / 2) ** 2
+ cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
)
))
# 7181.7841229421165 (km)
위 수식을 검증하기 위해서 수식의 일부 값을 확인해야할 수 있는데 수식의 일부를 복&붙으로 확인할 수 있다.
이 경우 코드를 읽는 사람들에게 len_numbers와 sum_numbers 변수는 계산을 최적화하기 위해 dictionary 내부에서만 사용했고 다시 사용되지 않음을 명확히 전달 할 수 있다
(3) Text 파일에서 lines, words, character 수 세는 예시
# wc.py
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
counts = (
path.read_text().count("\\n"), # Number of lines
len(path.read_text().split()), # Number of words
len(path.read_text()), # Number of characters
)
print(*counts, path) # 11 32 307 wc.py
wc.py 파일은 11줄, 32단어, 307 character로 구성되어있다
위 코드를 보면 path.read_text() 가 반복적으로 호출되는걸 알 수 있다 ⇒ walrus operator를 이용해 개선해보면,
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
counts = (
**(text := path.read_text()).count("\\n"), # Number of lines**
len(text.split()), # Number of words
len(text), # Number of characters
)
print(*counts, path)
물론 아래처럼 text 변수를 이용하면 코드는 한줄 늘어나지만 readability를 훨신 높일 수 있다.
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
text = path.read_text()
counts = (
text.count("\\n"), # Number of lines
len(text.split()), # Number of words
len(text), # Number of characters
)
print(*counts, path)
그러므로 walrus operator가 코드를 간결하게 해주더라도 readability를 고려해야 한다.
(4) List Comprehensions
List comprehension과 함께 연산이 많은 함수를 사용하게 될 때, walrus operator의 사용은 효과적일 수 있다.
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = [slow(num) for num in numbers if slow(num) > 4]
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 30.01522707939148
numbers 리스트의 각 element에 slow 함수를 적용 후 3보다 큰 경우에만 results에 slow 호출 결과를 저장하는 코드
문제는 slow 함수가 2번 호출됨
slow 호출 후 반환 결과가 3보다 큰지 확인할 때
results 리스트에 저장하기 위해 slow 호출할 때
가장 일반적인 해결책은 list comprehension 대신 for loop을 사용하는 것이다.
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = []
for num in numbers:
slow_num = slow(num)
if slow_num > 4:
results.append(slow_num)
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 25.021725063323975
slow 함수가 모든 경우에 한번씩만 호출됨
하지만 코드 양이 늘어나고 가독성이 떨어짐
walrus operator를 사용하면 list comprehension을 유지하면서 가독성을 높일 수 있음
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = [slow_num for num in numbers if (slow_num := slow(num)) > 4]
print(results)
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 25.018176908493042
(5) While Loop
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
user_answer = input(f"\n{question} ")
while user_answer not in valid_answers:
print(f"Please answer one of {', '.join(valid_answers)}")
user_answer = input(f"\n{question} ")
위 코드는 사용자의 입력을 받는 input 함수가 두번 반복됨
이를 개선하기 위해 While True 와 break를 사용하여 코드를 다시 작성하는 것이 일반적임
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
while True:
user_answer = input(f"\n{question} ")
if user_answer in valid_answers:
break
print(f"Please answer one of {', '.join(valid_answers)}")
walrus operator를 이용해서 while loop을 간결하게 할 수 있음
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
while (user_answer := input(f"\n{question} ")) not in valid_answers:
print(f"Please answer one of {', '.join(valid_answers)}")
사용자로부터 받은 input 입력을 user_answer 변수에 저장하고 동시에 valid_answers 내에 포함되어있는지를 체크하여 가독성을 높일 수 있음
이 글은책컴퓨터 밑바닥의 비밀chapter 3.2-3.3 의 내용을 읽고 요약한 글입니다.
3.2 프로세스는 메모리 안에서 어떤 모습을 하고 있을까?
3.2.1 가상 메모리: 눈에 보이는 것이 항상 실제와 같지는 않다
모든 프로세스의 코드 영역은 0x400000에서 시작
서로 다른 두개의 프로세스가 메모리를 할당하기 위해 malloc을 호출하면 동일한 시작 주소를 반환할 가능성이 매우 높음
Ubuntu 에서 테스트
// malloc_test.c
#include <stdio.h>
#include <stdlib.h>
int main() {
void *ptr = malloc(10); // Allocate 10 bytes
printf("Address returned by malloc: %p\\n", ptr);
free(ptr); // Don't forget to free the memory
return 0;
}
$ ./malloc_test Address returned by malloc: 0x56342fa8d260 $ ./malloc_test Address returned by malloc: 0x55a3e5a9d010
다른 주소가 반환됨. 이유는? → ASLR 이라는 기술이 최근 OS에는 적용되어있음
ASLR (Address Space Layout Randomization)
스택, 힙, 동적 라이브러리 영역의 주소를 랜덤으로 배치해서 공격에 필요한 target address를 예측하기 어렵게 만드는 기술
ASLR 기술을 끄고 테스트하면 같은 주소가 반환되는 걸 볼 수 있다
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ ./malloc_test
$ Address returned by malloc: 0x40068c
$ ./malloc_test
$ Address returned by malloc: 0x40068c
두 프로세스가 모두 같은 주소에 데이터를 쓸 수 있다는 의미인데 괜찮은가?
⇒ 주소값은 가짜 주소(가상 메모리 주소)이고 메모리에 조작이 일어나기 전 실제 물리 메모리 주소로 변경되기 때문에 문제 되지 않음
주목할 2가지 사항
프로세스는 동일한 크기의 chunk로 나뉘어 물리 메모리에 저장 - 위 그림에서 힙 영역은 3개의 chunk로 나뉨
모든 조각은 물리 메모리 전체에 무작위로 흩어져 있음
보기에 아름답지 않지만 운영 체제가 프로세스에 균일한 가상의 주소 공간을 제공하는 것을 방해하지는 않음 → 가상 메모리와 물리 메모리 사이의 mapping을 나타내는 page table로 관리
3.2.2 페이지와 페이지 테이블: 가상에서 현실로
각각의 프로세스에는 단 하나의 페이지 테이블만 있어야 함
mapping은 ‘page’ 라는 단위로 이루어짐
프로세스의 주소 공간을 동일한 크기의 ‘조각’으로 나누고, 이 ‘조각’을 페이지(page)라고 부름
그러므로 두 프로세스가 동일한 메모리 주소에 기록하더라도 페이지 테이블 통해 실제는 다른 물리 메모리 주소에 저장됨
3.3 스택 영역: 함수 호출은 어떻게 구현될까?
아래 코드의 문제점은?
void func(int a)
{
if(a>100000000)
{
return;
}
int arr[100] = { 0 };
func(a + 1);
}
함수 실행 시간 스택(runtime stack)과 함수 호출 스택(call stack) 이해 필요
3.3.2 함수 호출 활동 추적하기: 스택
A, B, C, D 4가지 단계가 존재하고 아래그림 처럼 의존성을 가짐
단계별로 진행 과정
A→B→D→B→A→C→A
선입 선출(Last In First Out, LIFO) 순서로 stack 과 같은 데이터 구초가 처리하기 적합
3.3.3 스택 프레임 및 스택 영역: 거시적 관점
함수 실행시의 자신만의 ‘작은 상자’가 필요한데 이를 call stack 혹은 stack frame 이라고 함
스택은 낮은 주소 방향으로 커지므로 아래와 같이 표현됨
stack frame에는 어떤 정보들이 포함되는지?
3.3.4 함수 점프와 반환은 어떻게 구현될까?
함수 A가 함수 B를 호출하면, 제어권이 A에서 B로 옮겨짐
제어권: CPU가 어떤 함수에 속하는 기계 명령어를 실행하는지 의미
제어권이 넘어갈 때는 2가지 정보가 필요함
반환(return): 어디에서 왔는지에 대한 정보
함수 A의 명령어가 어디까지 실행되었는지에 대한 정보
점프(jump): 어디로 가는지에 대한 정보
함수 B의 첫 번째 기계 명령어가 위치한 주소
위의 정보는 어디서 가져오는지? → 스택 프레임!
함수 A가 함수 B를 호출할 때 CPU는 함수 A의 기계 명령어(주소: 0x400564)를 실행중이라 가정
CPU는 다음 기계 명령어를 실행하는데 call 뒤에 명령어 주소가 함수 B의 첫번째 기계 명령어
이 명령어를 실행한 직후 CPU는 함수 B로 점프하게 됨
함수 B실행이 완료되면 어떻게 함수 A로 돌아오나?
call 명령어 실행 후 지정한 함수로 점프 + call 명령어 다음 위치 주소(0x40056a)를 함수 A의 스택 프레임에 넣음
반환 주소가 추가되면서 스택 프레임이 아래 방향으로 조금 커짐
함수 B에 대응하는 기계 명령어를 실행하면서 B에 대한 스택 프레임도 추가됨
함수 B의 마지막 기계 명령어인 ret은 CPU에 함수 A의 스택 프레임에 저장된 반환주소로 점프하도록 전달하는 역할
3.3.5 매개변수 전달과 반환값은 어떻게 구현될까?
대부분의 경우 레지스터를 통해 매개변수와 반환값을 전달
함수 A가 B를 호출하면, A는 매개변수를 상응하는 레지스터에 저장
CPU가 함수 B를 실행할 때 이 레지스터에서 매개변수 정보를 얻을 수 있음
CPU 내부의 레지스터 수가 제한되는 경우 나머지는 스택 프레임에 넣음
3.3.6 지역 변수는 어디에 있을까?
레지스터에 저장할 수 있지만 로컬 변수가 레지스터 수보다 많으면 스택프레임에 저장
3.3.7 레지스터의 저장과 복원
함수 A와 B가 지역변수 저장을 위해 모두 레지스터를 사용하면 값이 덮어써 질 수 있음
그렇기 때문에 초기에 저장된 값을 레지스터에서 사용하고 나면 다시 그 초기값을 함수의 스택 프레임에 저장해야 함
3.3.8 큰 그림을 그려보자, 우리는 지금 어디에 있을까?
void func(int a)
{
if(a>100000000)
{
return;
}
int arr[100] = { 0 };
func(a + 1);
}
앞에서 본 코드를 다시 보면, 자기 자신 함수를 100000000번 호출
호출 할 때마다 스택 프레임이 함수 실행시 정보를 저장하기 위해 생성되며, 함수 호출 단계가 증가하며 스택 영역이 점점 더 많은 메모리 차지 → stack overflow 발생