반응형

Python 공식문서에 따르면 super 클래스의 역할은 아래와 같음

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.

공식문서 설명은 늘 어려움.

쉽게 말해, 부모나 형제 클래스의 임시 객체를 반환하고, 반환된 객체를 이용해 슈퍼 클래스의 메소드를 사용할 수 있음.

즉, super() 를 통해 super class의 메소드에 접근 가능

단일상속에서 super()

 
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

 

class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

square = Square(4)
square.area() # 16
  • Rectangle 클래스를 상속받기 때문에 Rectangle의 area() 메소드 사용 가능

 

super() with parameters


  • super() 는 2가지 파라미터를 가질 수 있음
    • 첫번째 : subclass
    • 두번째 : subclass의 인스턴스 객체
class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square(Rectangle):
    def __init__(self, length):
        super(Square, self).__init__(length, length)
  • 단일 상속인 경우에는 super(Square, self)와 super()는 같은 의미

아래의 경우는?

class Cube(Square):
    def surface_area(self):
        face_area = super(Square, self).area()
        return face_area * 6

super(Square, self).area()

첫번째 argument : subclass 인 Square

  • 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를 넣어줘야함

print(issubclass(Cube, Square)) # True
반응형
반응형

정식 이름은 Assignment expression operator인데 walrus operator라고도 불린다.

walrus는 “바다코끼리”라는 뜻으로 operator가 바다 코끼리의 눈과 이빨을 닮아서 이렇게 부른다.
때론 colon(:) equals(=) operator라고도 한다.

Python 3.8버전부터 새로 등장했다.

 

https://dev.to/davidarmendariz/python-walrus-operator-j13

 

Statement vs Expression in Python

바다코끼리 연산자의 정식 이름을 보면 Assignment expression operator로, expression이라는 단어가 나온다. 

Python에서 statement와 expression이라는 표현이 비슷해 혼동스러운데 간단히 정리하면, 아래와 같다.

  • statement: 코드를 구성할 수 있는 단위 혹은 모든 것
  • expression: 값을 평가하는 statement로 연산자와 피연산자의 조합으로 구성됨

예시

x = 25          # a statement
x = x + 10      # an expression
  • statement는 변수를 생성하는데 사용된다.
  • expression은 x값에 10을 더하는 연산이 수행된 후 결과가 x에 할당되었다.
>>> walrus = False # (1)
>>> walrus
False

>>> (walrus := True) # (2)
True
>>> walrus
True
  1. walrus = False는 값 False가 walrus에 할당된다. (traditional statement)
  2. (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를 할 수 없다.
>>> walrus := True
  File "<stdin>", line 1
    walrus := True
           ^
SyntaxError: invalid syntax

이를 해결하기 위해 많은 경우에 assignment expression 에 괄호를 추가해 python에서 syntax error를 피할 수 있다.

>>> (walrus := True)  # Valid, but regular assignments are preferred
True

 

사용 예시

walrus operator는 반복적으로 사용되는 코드를 간단히 하는데 유용하게 사용될 수 있다.

(1) 수식 검증

예로 복잡한 수식을 코드로 작성하고 이름 검증하고 debugging할 때 walrus operator가 유용할 수 있다.

아래와 같은 수식이 있다고 하자 (참고: haversine formula, 지구 표면의 2점 사이의 거리를 구하는 식)

$$
2 \cdot \text{r} \cdot \arcsin\left(
    \sqrt{
        \sin^2\left(\frac{\phi_2 - \phi_1}{2}\right)
        + \cos(\phi_1) \cdot \cos(\phi_2) \cdot \sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)
    }
\right)
$$ 

  • ϕ: 위도(latitude), λ: 경도(longitude)

위 수식을 이용해  오슬로(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)
  • 위 수식을 검증하기 위해서 수식의 일부 값을 확인해야할 수 있는데 수식의 일부를 복&붙으로 확인할 수 있다.
  • 이때 walrus operator를 이용하면,
2 * rad * asin(
    sqrt(
        **(ϕ_hav := sin((ϕ2 - ϕ1) / 2) ** 2)**
        + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
    )
)

# 7181.7841229421165

ϕ_hav
# 0.008532325425222883
  • 전체 expression의 값을 계산하면서 동시에 ϕ_hav값을 계속 확인할 수 있어서 copy & paste로 인해 발생할 수 있는 오류의 가능성을 줄일 수 있다.

 

(2) Lists 에서 활용될 수 있는 walrus operator

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

위 list에서 길이, 합계, 평균 값을 dictionary에 저장한다고 가정해보자

description = {
    "length": len(numbers),
    "sum": sum(numbers),
    "mean": sum(numbers) / len(numbers),
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • description에서 numbers의 len과 sum이 각각 두번씩 호출된다
  • 짧은 list에서는 큰 문제가 되지 않지만 길이가 더 긴 list나 연산이 복잡할 경우에는 최적화할 필요가 있다

 

물론 아래처럼 len_numbers, sum_numbers 변수를 dictionary 밖에서 선언 후 사용할 수도 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

len_numbers = len(numbers)
sum_numbers = sum(numbers)

description = {
    "length": len_numbers,
    "sum": sum_numbers,
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}

 

 

하지만 walrus operator를 이용해 len_numbers, sum_numbers 변수를 dictionary 내부에서만 사용하여 code를 최적화할 수 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

description = {
    "length": (len_numbers := len(numbers)),
    "sum": (sum_numbers := sum(numbers)),
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • 이 경우 코드를 읽는 사람들에게 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 내에 포함되어있는지를 체크하여 가독성을 높일 수 있음

 

Reference


https://realpython.com/python-walrus-operator/

 
반응형
반응형

 

 

파이썬에서 URL을 다루기 위한 패키지로 크게 3가지 종류가 있음; urllib, urllib3, requests

  • urllib은 built-in package이고 나머지 2개는 third party

 

사용방법

 

1. 기본 사용방법

from urllib.request import urlopen                     # (1)

with urlopen("<https://www.example.com>") as response: # (2)
    body = response.read()                             # (3)
    print(type(body))                                  # (4)

(1) urllib.request는 built-in package로 따로 설치하지 않아도 됨. HTTP request를 위해 urlopen을 사용

(2) context manager with 문을 통해 request 후 response를 받을 수 있음

(3) response 는 <http.client.HTTPResponse> 객체

  • read 함수를 통해 bytes로 변환할 수 있음

(4) 실제 body의 type을 print해서 bytes 타입임을 확인

 

2. GET request for json format response

  • API 작업시 response가 json format인 경우가 많음
from urllib.request import urlopen
import json                                            # (1)

url = "<https://jsonplaceholder.typicode.com/todos/1>" # (2)
with urlopen(url) as response:
    body = response.read()

print("body: ", body)                                  # (3)
# body:  b'{\\n  "userId": 1,\\n  "id": 1,\\n  "title": "delectus aut autem",\\n  "completed": false\\n}'

# json bytes to dictionary
todo_item = json.loads(body)                           # (4)
print(todo_item)
# {'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}

(1) urllib 패키지와 함께 json 포맷을 다루기 위해 json package 추가

(2) JSON 형태의 데이터를 얻기 위한 샘플 API 주소

(3) 응답을 print 해보면 json 형태의 bytes format. 이를 dictionary 형태로 변경해주기 위해 json 패키지 필요

(4) json bytes를 파이썬 객체인 dictionary로 변경하기 위해 json.loads 함수 사용

 

3. Response의 header 정보 얻는 방법

from urllib.request import urlopen
from pprint import pprint

with urlopen("<https://www.example.com>") as response:
    pprint(response.headers.items())                       # (1)
    pprint(response.getheader("Connection")) # 'close'     # (2)
  • response의 headers.items()를 통해 header 정보를 얻을 수 있음

(1) pretty print(pprint)를 이용해 header 정보를 보기 좋게 출력하면 아래와 같음

[('Accept-Ranges', 'bytes'), ('Age', '78180'), ('Cache-Control', 'max-age=604800'), ('Content-Type', 'text/html; charset=UTF-8'), ('Date', 'Sat, 24 Aug 2024 18:10:20 GMT'), ('Etag', '"3147526947"'), ('Expires', 'Sat, 31 Aug 2024 18:10:20 GMT'), ('Last-Modified', 'Thu, 17 Oct 2019 07:18:26 GMT'), ('Server', 'ECAcc (lac/5598)'), ('Vary', 'Accept-Encoding'), ('X-Cache', 'HIT'), ('Content-Length', '1256'), ('Connection', 'close')]

(2) header의 개별 정보는 getheader 메소드를 이용해 얻을 수 있음

 

4. bytes를 string으로 변환

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()                            
    print(type(body)) # <class 'bytes'>                       # (1)

decoded_body = body.decode("utf-8")                           # (2)
print(type(decoded_body)) # <class 'str'>                     # (3)
print(decoded_body[:30])

(1) body의 type을 확인해보면 bytes 이고 아래와 같은 형태이다

b'<!doctype html>\n<html>\n<head>\n

(2) bytes를 string으로 변환하기 위해 decode method를 이용 (”utf-8”을 파라미터로 전달)

(3) decoded_body의 type을 확인해보면 string인걸 확인할 수 있고 decoded_body의 일부를 표시하면 아래와 같은 형태

<!doctype html>
<html>
<head>

 

5. Bytes를 file로 변환

크게 2가지 방법이 있음

encoding & decoding 없이 바로 file로 작성

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()

with open("example.html", mode="wb") as html_file:
    html_file.write(body)
  • write binary(wb) mode로 파일을 열어 bytes를 바로 example.html 파일에 작성
  • 코드를 실행하면 example.html 파일이 생성됨

 

contents를 file로 encoding해야하는 경우

from urllib.request import urlopen

with urlopen("<https://www.google.com>") as response:
    body = response.read()

character_set = response.headers.get_content_charset()        # (1) 
content = body.decode(character_set)                          # (2)

with open("google.html", encoding="utf-8", mode="w") as file: # (3)
    file.write(content)

(1)&(2): 구글같은 홈페이지는 location에 따라 다른 encoding 방식을 사용하기도 한다. 그래서 get_content_charset () 메소드를 이용해서 encoding 방식을 확인 후 bytes를 string으로 decoding 함

(3) decoded string을 다시 html에 utf-8 모드로 encoding해서 google.html 파일에 저장함

References


https://realpython.com/lessons/python-urllib-request-overview/

 
반응형
반응형

 

클린 코드의 의미


  • 기계나 스크립트가 판단할 수 없고 전문가가 판단할 수 있는 것
  • 프로그래밍 언어란 아이디어를 다른 개발에게 전달하는 것이고 여기에 클린 코드의 진정한 본질이 있다

⇒ 클린 코드를 정의하기 보다는 좋은 코드 나쁜 코드의 차이점을 확인하고 훌륭한 코드와 좋은 아키텍쳐를 식별하여 자신만의 정의를 하는 것이 좋음

클린코드의 중요성


  1. 민첩한 개발과 지속적인 배포가 가능
  • 유지보수가 가능한 상태로 가독성이 높아야 기획자가 새롭게 기능을 요구할 때마다 리팩토링을 하고 기술 부채를 해결하느라 시간이 오래 걸리지 않음
  1. 기술부채 발생
  • 잠재적인 문제로 언젠가는 돌발 변수가 될 수 있음

클린 코드에서 코드 포매팅의 역할


PEP (Python Enhancement Proposal)

  • PEP 8: Style Guide for Python Code 로 가장 잘 알려진 표준이며 띄어쓰기, 네이밍 컨벤션, 줄 길이 제한 등의 가이드라인을 제공

클린 코드를 위한 보조적인 역할, PEP8을 100% 준수한다고 하더라도 여전히 클린 코드의 요건을 충족하지 못할 수 있음

PEP8 특징

  1. 검색 효율성
  • PEP8: 변수에 값을 할당하는 경우와 함수의 키워드 파라미터에 값을 할당하는 경우를 구분
# core.py
#
#
#

def get_location(location: str = ""):
    pass

#변수에 값을 할당할 때 띄어쓰기 사용 O
current_location = get_location() 

#키워드 인자에 값을 할당할 때 띄어쓰기사용 X
get_location(location=current_location) 

  • location이라는 키워드 인자에 값이 할당되는 경우를 찾는 경우

$ grep -nr “location=” ./core.py:13:get_location(location=current_location)

  • nr 옵션
    • n : line number 표시
    • r : 해당 디렉토리에서 recursive 하게 subdirectory도 검색
  • location이라는 변수에 값이 할당되는 경우를 찾는 경우

$ grep -nr “location =” ./core.py:10:current_location = get_location()

  1. 일관성
  2. 더 나은 오류 처리
  3. 코드 품질

문서화(Documentation)


코드주석(Code comments)

  • 가능한 한 적은 주석을 갖는 것을 목표로 해야 함
    • 주석 처리된 코드는 절대 없어야 함

Docstring

  • 소스 코드에 포함된 문서 (리터럴 문자열)
  • 내가 작성한 컴포넌트를 다른 엔지니어가 사용하려고 할 때 docstring을 보고 동작방식과 입출력 정보등을 확인 할 수 있어야 함
  • Python은 동적인 데이터 타입을 갖기 때문에 docstring이 큰 도움이 됨

docstring은 코드에서 분리되거나 독립된 것이 아니라 일부

단점은 지속적으로 수작업을 해야 한다는 것 (코드가 변경되면 업데이트를 해야함)

⇒ 가치 있는 문서를 만들기 위해 모든 팀원이 문서화에 노력해야 함

어노테이션

  • PEP-3107에서 어노테이션을 소개
    • 코드 사용자에게 함수 인자로 어떤 값이 와야 하는지 힌트를 주자는 것
from dataclasses import dataclass

@dataclass
class Point:
    lat: float
    long: float

def locate(latitude: float, longitude: float) -> Point:
    """맵에서 좌표에 해당하는 객체를 검색"""
    pass

  • 함수 사용자에게 힌트를 주지만 파이썬이 타입을 검사하거나 강제하지는 않음

어노테이션으로 타입만 지정할 수 있는 것은 아니고, 인터프리터에서 유효한 어떤 것(변수의 의도를 설명하는 문자열, 콜백이나 유효성 검사 함수로 사용할 수 있는 callable)도 가능

EX) 몇 초 후에 어떤 작업을 실행하는 함수

def launch_task(delay_in_seconds):
    pass
  • delay_in_seconds 파라미터는 긴 이름을 가지고 있어 많은 정보를 담고 있는 것 같아 보이지만 사실 충분한 정보를 제공하지 못함
    • 허용 가능한 지연시간은 몇초?
    • 분수를 입력해도 되나?
Seconds = float
def launch_task(delay: Seconds):
    pass
  • Seconds 어노테이션을 사용하여 시간을 어떻게 해석할지에 대해 작은 추상화 진행
  • 나중에 입력 값의 형태를 변경하기로 했다면 이제 한 곳에서만 관련 내용을 변경하면 됨
    • Seconds = float

어노테이션을 사용하면 __annotations__ 이라는 특수한 속성이 생김

  • 어노테이션의 이름과 값을 매핑한 dictionary

아래 locate함수에 대해 __annotations__ 을 출력해보면,

from dataclasses import dataclass

@dataclass
class Point:
    lat: float
    long: float

def locate(latitude: float, longitude: float) -> Point:
    """맵에서 좌표에 해당하는 객체를 검색"""
    pass

print(locate.__annotations__)

{'latitude': <class 'float'>, 'longitude': <class 'float'>, 'return': <class 'main.Point'>}

타입 힌트는 단순히 데이터 타입을 확인하는 것이 아니라 유의미한 이름을 사용하거나 적절한 데이터 타입 추상화를 하도록 도와줄 수 있음

def process_clients(clients: list):

def process_clients(clients: list[tuple[int, str]]):

...

from typing import Tuple
Client = Tuple[int, str]
def process_clients(clients: list[Client]):

어노테이션을 도입하면 클래스를 보다 간결하게 작성하고 작은 컨테이너 객체를 쉽게 정의 가능

  • @dataclass 데코레이터를 사용하면 별도의 __init__ 메소드에서 변수를 선언하고 할당하는 작업을 하지 않아도 바로 인스턴스 속성으로 인식
# Before
class Point:
    def __init__(self, lat, long):
        self.lat = lat
        self.long = long
# After
from dataclasses import dataclass
@dataclass
class Point:
    lat: float
    long: float
# After
from dataclasses import dataclass

@dataclass
class Point:
    lat: float
    long: float

print(Point.__annotations__) 
# {'lat': <class 'float'>, 'long': <class 'float'>}
print(Point(1, 2))
# Point(lat=1, long=2)

Q. 어노테이션은 docstring을 대체하는 것일까?

  • 둘은 상호보완적인 개념
def data_from_response(response: dict) -> dict:
    if response["status"] != 200:
        raise ValueError
    return {"data": response["payload"]}
  • input, output 형식에 대해서는 알 수 있지만 상세한 내용은 알 수 없음
    • ex) respone 객체의 올바른 instance 형태

상세한 내용에 대해서 docstring으로 보완할 수 있음

def data_from_response(response: dict) -> dict:
    """response의 HTTP status가 200이라면 response의 payload를 반환
    
    
    - response의 예제::
    {
        "status": 200, # <int>
        "timestamp": "....", # 현재 시간의 ISO 포맷 문자열
        "payload": {...} # 반환하려는 dictionary 데이터
    }
    
    
    """
    if response["status"] != 200:
        raise ValueError
    return {"data": response["payload"]}
  • input, output의 예상 형태를 더 잘 이해할 수 있고 단위 테스트에서도 유용한 정보로 사용됨

도구설정

  • 반복적인 확인 작업을 줄이기 위해 코드 검사를 자동으로 실행하는 기본도구설정

데이터 타입 일관성 검사

  • mypy, pytype 등의 도구를 CI build에 포함시킬 수 있음
$ pip install mypy
from typing import Iterable
import logging

logger = logging.getLogger()

def broadcast_notification(message: str, relevant_user_emails: Iterable[str]):
    for email in relevant_user_emails:
        logger.warning(f"{message} 메세지를 {email}에게 전달")

broadcast_notification("welcome", "user1@domain.com")
# mypy가 오류 내뱉지 않음
$ mypy core.py  
Success: no issues found in 1 source file

welcome 메세지를 u에게 전달
welcome 메세지를 s에게 전달
welcome 메세지를 e에게 전달
welcome 메세지를 r에게 전달
welcome 메세지를 1에게 전달
welcome 메세지를 @에게 전달
welcome 메세지를 d에게 전달
welcome 메세지를 o에게 전달
welcome 메세지를 m에게 전달
welcome 메세지를 a에게 전달
welcome 메세지를 i에게 전달
welcome 메세지를 n에게 전달
welcome 메세지를 .에게 전달
welcome 메세지를 c에게 전달
welcome 메세지를 o에게 전달
welcome 메세지를 m에게 전달

  • 잘못된 호출. 문자열 또한 iterable 객체이므로 for 문이 정상 동작하지만 유효한 이메일 형식이 아님

리스트나 튜플만 허용하도록 더 강력한 타입 제한을 주면,

from typing import List, Tuple, Union
import logging

logger = logging.getLogger()

def broadcast_notification(
    message: str, relevant_user_emails: Union[List[str], Tuple[str]]
):
    for email in relevant_user_emails:
        logger.warning(f"{message} 메세지를 {email}에게 전달")

broadcast_notification("welcome", "user1@domain.com")
> mypy core.py
core.py:14: error: Argument 2 to "broadcast_notification" has incompatible type "str"; expected "list[str] | tuple[str]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

일반적인 코드 검증

  • 데이터 타입 이외에도 일반적인 유형의 품질 검사도 가능
  • pycodestyle(pep8), flake8
  • 더 엄격한 pylint

자동 포매팅

  • black formatter
  • PEP-8보다 엄격하게 포매팅하여 문제의 핵심에 보다 집중
  • —check 옵션을 사용해 코드를 포맷하지않고 표준을 준수하는지 검사만 하는 것도 가능
    • CI 프로세스에 통합하여 유용하게 사용될 수 있음

자동 검사 설정

  • 리눅스 개발환경에서 빌드를 자동화하는 가장 일반적인 방법은 Makefile을 사용하는 것
 
반응형
반응형

상황


  • Google에서 이미지를 크롤링하는 파이썬 스크립트를 쿠버네티스 클러스터에 pod로 띄우려고 했다
  • Script → Docker container 과정으로 테스트 후 정상 동작하는 걸 확인 후 Pod로 띄웠는데 실패…
  • 그동안 Docker container 에서 동작 → Pod에서 동작으로 이해하고 있었다.
  • 결론: 실행환경을 고려할 때 CPU 아키텍쳐도 고려를 해야한다!

 

과정


1. Google에서 이미지 크롤링하는 파이썬 스크립트 (크롬사용)

Selenium은 웹 테스트를 할 때 사용하는 프레임워크인데 BeautifulSoup과 더불어 크롤링할 때 많이 사용되는 도구들 중 하나다.

실제로 우리가 구글에서 검색할때처럼 검색창에 키워드를 입력하고 기다린 후 스크롤을 내리는 과정들을 코드로 작성한다. 코드를 보면 어릴적 게임할 때(라스트킹덤 광물캘때) 사용하던 매크로같은 느낌이 든다.

https://www.browserstack.com/guide/selenium-webdriver-tutorial

 

위 그림처럼 Selenium 패키지를 통해 파이썬 코드로 Browser driver를 통해 실제 browser로 명령/요청을 전달하고 응답을 받는다. 이 과정을 위해 아래의 3가지가 갖춰진 환경이 필요하다.

  1. 크롭 웹 브라우저
  2. 크롬 드라이버
  3. 파이썬 Selenium 패키지

이 때 중요한건 크롬 웹브라우저와 크롬 드라이버의 버전의 호환성이다. 웹 브라우저는 자주 업데이트가 되는데 크롬 드라이버의 버전이 업데이트가 되지 않으면 어느 순간 크롤링이 안되는 문제가 발생한다. 본 글에서는 특정한 버전으로 맞춰서 진행한다.

진행했던 환경은 아래와 같다.

더보기
  • x86_64, Ubuntu 20.04.6 LTS,1CPU, 1GB
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal

$ uname -a 
Linux ubuntu-s-1vcpu-512mb-10gb-sfo3-01 5.4.0-122-generic #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

 

 

 

1) 크롭 웹 브라우저 설치

현재 날짜 기준(2023.12.25) Chrome 웹 브라우저의 stable 버전은 120.0.6099.109

(https://googlechromelabs.github.io/chrome-for-testing/#stable

# 크롬 웹브라우저 다운로드 및 설치 (working directory: /root/crawling)
$ wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.109/linux64/chrome-linux64.zip
$ unzip chrome-linux64.zip

 

2) 크롬 드라이버 설치

위 버전에 맞춰서 크롬 드라이버도 설치

# 크롬 드라이버 다운로드 및 설치 (working directory: /root/crawling)
$ wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.109/linux64/chromedriver-linux64.zip
$ unzip chromedriver-linux64.zip

 

3) Python selenium 패키지 설치

pip install "selenium == 4.15.1"

크롤링하는 코드는 본 유투브 링크를 참고했고, apple이라는 키워드로 검색했을 때 나오는 이미지들을 저장하는 코드이다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys  # 엔터처리용
import time
import urllib.request
import os

options = webdriver.ChromeOptions()

options.add_argument("--headless") # '창이 없는’: 서버에서는 웹브라우저 창을 못띄우니 필요
options.add_argument("--no-sandbox") 
options.add_argument("--disable-dev-shm-usage") # 공유메모리 사용하지 않음
options.add_argument("--single-process")
options.binary_location = '/root/crawling/chrome-linux64/chrome'

service = webdriver.ChromeService(executable_path='/root/crawling/chromedriver-linux64/chromedriver')

driver = webdriver.Chrome(service=service, options=options)

URL = "<https://www.google.co.kr/imghp>"
KEYWORD = "apple"
driver.get(url=URL)

# time.sleep은 fixed, implicity는 flexible로 time_to_wait이 maximum time
driver.implicitly_wait(time_to_wait=10)

keyElement = driver.find_element(By.NAME, "q")
keyElement.send_keys(KEYWORD)
keyElement.send_keys(Keys.RETURN)  # 키보드 엔터

bodyElement = driver.find_element(By.TAG_NAME, "body")
time.sleep(5)  # 엔터치고 이미지 나오는 시간 기다림

image_candidates = []

print("Crawling Images Start!")

for i in range(1):
    bodyElement.send_keys(Keys.PAGE_DOWN)
    time.sleep(0.2)

    images = driver.find_elements(
        By.XPATH, '//*[@id="islrg"]/div[1]/div/a[1]'
    )  # XPATH는 변경될 수 있고 web browser의 개발자도구 이용해서 확인필요

    image_candidates.append(images)

for images in image_candidates:
    for idx, image in enumerate(images):
        image.send_keys(Keys.ENTER)
        time.sleep(0.5)

        high_images = driver.find_elements(
            By.XPATH,
            '//*[@id="Sva75c"]/div[2]/div[2]/div[2]/div[2]/c-wiz/div/div/div/div/div[3]/div[1]/a/img[1]', # XPATH는 변경될 수 있고 web browser의 개발자도구 이용해서 확인필요
        )
        try:
            real_image = high_images[0].get_attribute("src")
        except Exception as e:
            print(f"Exception: {e}")
            continue
        try:
            urllib.request.urlretrieve(
                real_image,
                os.path.join(os.getcwd(), str(idx)) + ".jpg",
            )
        except Exception as e:
            print(e)

정상적으로 동작하는걸 확인했고 Docker 이미지로 만들어서 쿠버네티스 파드로 띄우려고 했는데 아래와 같은 에러가 발생했다.

OSError: [Errno 8] Exec format error: '~~/chromedriver-linux64/chromedriver’

에러메세지가 분명하진 않다.

이것저것 시도 후 파악한 결과는 CPU 아키텍처에 맞는 chrome 웹브라우저와 드라이버를 다운받아야했다. 알아보니 쿠버네티스 pod는 AWS graviton 기반의 인스턴스에 뜨게 되는데 이는 arm64기반의 cpu였다!

결국 arm64 기반의 크롬 웹브라우저와 드라이버를 다운로드 후 해결했다.

ARM은 intel이나 amd와 호환되지 않는 cpu 아키텍쳐로 저전력 고효율을 목적으로 하는 곳(ex) 스마트폰)에 많이 사용되었는데 많이 발전해서 이제 PC용으로도 사용하는 시도가 많아지고 있다고 한다. 앞서 이야기한 AWS의 Graviton이나 Apple의 M1, M2칩이 그 예이다. Cloud환경에서 작업할 때 CPU 아키텍쳐도 고려해야한다는 걸 알 수 있는 경험이었다. 

참고


 
 
반응형
반응형
  • Linked List를 이용해서 stack 구현
class Node:
    def __init__(self, data, next: "Node" = None): 
        self.data = data
        self.next = next

string "Node"로 type hint를 사용한 이유는? https://wschoi.tistory.com/36 참고

class Stack:
    def __init__(self):
        self.last = None

stack = Stack()
  • self.last 속성을 이용해 stack의 push, pop () 메소드 구현
 

 

Push 메소드

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data) # here
        new_node.next = self.last
        self.last = new_node

stack = Stack()
stack.push(1)

new_node = Node(1)

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last # here
        self.last = new_node

stack = Stack()
stack.push(1)

new_node.next = self.last

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node # here

stack = Stack()
stack.push(1)

self.last = new_node

  • last 포인터가 새로운 node를 가리킴

push() 메소드를 한번 더 호출하면?

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data) # here
        new_node.next = self.last
        self.last = new_node

stack = Stack()
stack.push(1)
stack.push(2) # push 한번 더 호출

new_node = Node(2)

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last # here
        self.last = new_node

stack = Stack()
stack.push(1)
stack.push(2)

new_node.next = self.last

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node # here

stack = Stack()
stack.push(1)
stack.push(2)

self.last = new_node

  • last 포인터를 새로 추가된 노드를 가리키도록 업데이트

 

Pop 메소드

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node

    def pop(self):
        data = self.last.data # here
        self.last = self.last.next
        return data

stack = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # here

data = self.last.data

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node

    def pop(self):
        data = self.last.data
        self.last = self.last.next # here
        return data

stack = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2

self.last = self.last.next

  • last 포인터를 업데이트
  • 마지막으로 data를 반환
 
반응형
반응형

팰린드롬이란?

  • 앞뒤가 똑같은 단어나 문장
  • ‘기러기’, ‘오디오’
  • 소주 만 병만 주소

대소문자를 구분하지 않음
- Level
- Anna

영문자와 숫자만을 대상으로 함

  • “A man, a plan, a canal: Panama” → 영문자만 추리면 아래와 같음

  • 팰린드롬 맞음

 

문제에 대한 예시


풀이 1. 리스트로 변환

def is_palindrome_1(s:str)->bool:
    strs = []
		
		# 숫자, 문자만 추출
    for char in s:
        if char.isalnum():
            strs.append(char.lower()) # 소문자로 변환

    while len(strs)>1:
        if strs.pop(0) != strs.pop():
            return False
    return True

 

문제풀이에 필요한 개념

 


String

  • isalnum() 메소드
    • Return True if the string is an alpha-numeric string, False otherwise.
    char1 = 'a'
    print(char1.isalnum()) # True
    
    char2 = '5'
    print(char2.isalnum()) # True
    
    char3 = '$'
    print(char3.isalnum()) # False
    
  • lower() 메소드
    • 대문자를 소문자로 변환해서 반환
    str1 = 'ABC'
    print(str1.lower()) # abc
    
    str2 = 'AbC'
    print(str2.lower()) # abc
    

 

List

  • pop() 메소드
    lists = ['a', 'b', 'c', 'd']
    
    print(lists.pop()) # d
    
    print(lists) # ['a', 'b', c']
    
    print(lists.pop(0)) # 리스트에서 0번째 인덱스(가장 앞)에 해당하는 'a' 반환
    
    print(lists) # ['b', 'c']
    
  • Remove and return item at index (default last).

 

풀이 2. 데크 자료형을 이용한 최적화

def is_palindrome(s:str)->bool:
		import collections
    strs = collections.deque()
		
    # 숫자, 문자만 추출
    for char in s:
        if char.isalnum():
            strs.append(char.lower()) # 소문자로 변환

    while len(strs)>1:
        if strs.popleft() != strs.pop():
            return False
    return True
  • list가 아닌 deque를 이용
if strs.pop(0) != strs.pop(): # strs가 list

if strs.popleft() != strs.pop(): # strs가 deque
  • list의 pop(0)의 시간 복잡도는 O() O(n)
    • [0, 1, 2, 3, 4] pop(0) , [None, 1, 2, 3, 4] → [1, 2, 3, 4]
    • [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    • 맨 앞의 element에 pop()을 적용하면 뒤의 요소들을 한칸씩 왼쪽으로 모두 옮겨야 함
  • 반면 deque의 popleft() 시간 복잡도는 O(1)
    • 내부적으로 doubly linked list으로 삽입/삭제에 효율이 좋음
    • 1→ 2→ 3→4→5→6, 2만 pop하면 1→3→4→5→6 (뒤는 유지)

 

 

풀이 3. 슬라이싱 사용


def is_palindrome_3(s:str) -> bool:
    import re
    s = s.lower()

    s = re.sub('[^a-z0-9]', '', s) # 숫자 문자가 아닌 문자열을 공백으로 변환
    return s==s[::-1]

문제 풀이에 필요한 개념


  • 정규식
- [] : 문자 클래스로 [] 사이의 문자들과 매치
- [a-z] : 소문자 알파벳과 매치
- [a-zA-Z] : 알파벳 모두
- [0-9] : 숫자 모두
- [^a-z] : 소문자 알파벳을 제외한 문자와 매치
- [^a-z0-9] : 소문자 알파벳과 숫자를 제외한 문자와 매치

re.sub(first, second, third)

  • first : 정규식
  • second : first에 매치되는 요소를 second로 치환
  • third : 적용될 문자열
 
str = 'abcABC123!@#'
import re

# 소문자를 공백으로 치환
str_except_small_letter = re.sub('[a-z]', '', str)
print(str_except_small_letter) # ABC123!@#

# 대문자를 공백으로 치환
str_except_big_letter = re.sub('[A-Z]', '', str)
print(str_except_big_letter) # abc123!@#

# 숫자를 공백으로 치환
str_except_numbers = re.sub('[0-9]', '', str)
print(str_except_numbers) # abcABC!@#

# 숫자,문자를 공백으로 치환
str_except_special = re.sub('[a-zA-Z0-9]','', str)
print(str_except_special) # !@#

 

Reference

1) 책 "파이썬 알고리즘 인터뷰"

2) stackoverflow: deque.popleft() and list.pop(0). Is there performance difference?

 

반응형
반응형

개요

  • 글로벌하게 접근 가능한 하나의 객체를 제공하는 패턴
  • 로깅, 데이터베이스 관련 작업 등 동일한 리소스에 대한 동시 요청의 충돌을 방지하기 위해 하나의 인스턴스를 공유하는 작업에 주로 사용

 

싱글톤 패턴을 사용하는 경우

  1. 데이터의 일관성 유지 위해 DB에 작업을 수행하는 하나의 데이터베이스 객체가 필요한 경우
  2. 여러 서비스의 로그를 한 개의 로그파일에 순차적으로 동일한 로깅 객체를 사용해 남기는 경우에 적합

 

코드 구현


class Singleton:
    def __new__(cls):  # (1)
        if not hasattr(cls, "instance"): # (2)
            cls.instance = super().__new__(cls)
        return cls.instance

s1 = Singleton()
s2 = Singleton()
print('s1: ', s1) # s1:  <__main__.Singleton object at 0x000001A4B84A51D0>
print('s2: ', s2) # S2:  <__main__.Singleton object at 0x000001A4B84A51D0>
print('singleton: ', s1 is s2) # True

(1) __new__ 메소드

  • __new__ 메소드는 객체를 생성할 때 호출됨 (__init__ 메소드는 객체에 속성을 추가할 때 호출됨)
    • 호출순서 : __new__ -> __init__ 메소드

(2) 클래스 객체에 instance 속성이 있는지 확인

  • instance 속성이 없으면 super() 인 object 클래스의 __new__ 메소드를 호출하여 instance 속성 생성 후 반환
    • super() : 부모클래스 객체를 반환하는 함수. 상속받는 클래스가 없는경우 object 클래스를 상속받음

 

Singleton 클래스 인스턴스를 2개 생성했지만 각각의 주소가 같으므로 둘은 같은 인스턴스를 가리키고 있음

 

게으른 초기화


  • 싱글톤 패턴을 기반으로 하는 초기화 방식
  • 인스턴스를 꼭 필요할 떄 생성
    • 모듈을 import 할 때 필요하지 않는 시점에 실수로 미리 객체를 생성하는 경우 방지
class Singleton:
    __instance = None

    def __init__(self):
        if not Singleton.__instance:
            print('__init__ method called')
        else:
            print("Instance already created:", self.get_instance())

    @classmethod
    def get_instance(cls):
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance
if __name__=='__main__':
    s = Singleton() # __init__ method called 초기화만하고 객체 생성 X

    print('object created', Singleton.get_instance()) # 객체 생성 
  # object created <__main__.Singleton object at 0x00000183220C51D0>
	
    s1 = Singleton() 
  # Instance already created: <__main__.Singleton object at 0x00000246DBAB51D0>
  • 객체를 생성하기 위해서는 Singleton.getInstance() 호출 

 

모듈 싱글톤


  • 파이썬은 모든 모듈을 기본적으로 싱글톤으로 import함.
  • 아래 코드를 통해 싱글톤 여부 확인 가능
# Singleton.py
shared_variable = "shared variable"
# module1.py
import singleton
print('singleton in module_1: ', singleton.shared_variable)
singleton.shared_variable += "(modified by module_1)"
print('shared_variable was modified in module_1)
# module2.py
import singleton
print('singleton in module2: ', singleton.shared_variable)

위 3개의 .py 파일을 만든 후 모두 import

# test.py
import module1
# singleton in module1:  shared variable

import module2
# singleton in module2:  shared variable(modified by module_1)
  • singleton 모듈을 module1.py, module2.py 에서 각각 import
  • module1.py 에서 수정된 singleton 모듈의 변수(shared_variable)가 module2.py에서도 수정된 채로 확인됨

 

Reference

1) 책 "파이썬 디자인 패턴 2/e"
2) https://wikidocs.net/69361

 

 

반응형
반응형

2022.05.25 - [자료구조] - [자료구조] Linked List - (1) 개념

2022.06.05 - [자료구조] - [자료구조] Linked List - (2) 간단 개념 구현

2022.06.07 - [자료구조] - [자료구조] Linked List - (3) 노드 삽입 (at the end)

2022.06.08 - [자료구조] - [자료구조] Linked List - (4) 노드 삽입 (at the front)

2022.06.12 - [자료구조] - [자료구조] Linked List - (5) 데이터 조회

본 글에서는 Linked List에서 노드 삭제에 대해 다룸. cur이라는 변수를 이용하여 head가 가리키는 노드부터 삭제할 노드를 찾음.

노드 삭제에 대해서 크게 3가지 경우로 나눠서 생각

1) Linked list가 비어있는 경우 : 메소드 바로 종료

cur = self.head

if cur is None:
	return

Linked List가 비어있는 상태

2) head가 가리키는 노드의 값이 삭제하고자하는 값인 경우 (아래 그림에서 data 변수가 3인 경우)

cur = self.head

 

head가 가리키는 노드를 삭제하는 경우

head는 현재 가리키는 노드의 next가 가리키는 노드를 참조해야 함. 그리고 cur에는 None 할당

if cur.data == data: # 첫 노드 삭제
    self.head = cur.next
    cur = None
    return

head가 가리키는 노드 업데이트

3) head가 가리키는 노드가 아닌 다른 노드를 삭제하는 경우 (data에 2가 담긴 노드를 삭제한다고 가정)

cur = self.head

prev 변수를 도입하여 cur이 가리키는 노드를 참조하고, cur 변수는 현재 가리키는 노드의 next가 가리키는 노드 참조.

prev = cur
cur = cur.next

그리고 cur 변수가 가리키는 노드의 data가 삭제할 데이터인지 확인하는 과정을 반복하고 맞으면 반복과정을 종료

while True:
    prev = cur
    cur = cur.next
        
    if cur.data == data:
        break

 

Linked List가 끊기지 않기 위해서는 prev가 가리키는 노드의 next가 cur노드가 가리키는 노드의 next가 가리키는 노드를 참조해야 함 (아래 그림에서 빨간선)

prev.next = cur.next

data에 3이 담긴 노드의 next가 data에 1이 담긴 노드를 가리킴

 

위 3가지 경우가 포함된 delete 메소드는 아래와 같음

    def delete(self, data): # data가 포함된 노드를 linked list에서 삭제
        cur = self.head
        
        if cur == None: # Linked List가 비어있는 경우
            return 

        if cur.data == data: # 첫 노드 삭제
            self.head = cur.next
            cur = None
            return

        while True:
            prev = cur
            cur = cur.next
        
            if cur.data == data: # 중간 노드 삭제 
                break
        
        prev.next = cur.next
        cur = None

전체 코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    # linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.head = new_node
        else:
            self.tail.next = new_node
            
        self.tail = new_node

    # linked list 처음에 노드 추가
    def push(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.tail = new_node
        else:
            new_node.next = self.head
        
        self.head = new_node

    def delete_all(self):
        while self.head is not None:
            cur = self.head
            self.head = self.head.next
            cur = None

    # data가 포함된 노드를 linked list에서 삭제
    def delete(self, data):
        cur = self.head
        
        if cur == None: # Linked List가 비어있는 경우
            return 

        if cur.data == data: # 첫 노드 삭제
            self.head = cur.next
            cur = None
            return

        while True:
            prev = cur
            cur = cur.next
        
            if cur.data == data: # 중간 노드 삭제
                break
        
        prev.next = cur.next
        cur = None

    def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

if __name__ == "__main__":
    linked_list = LinkedList()
    linked_list.print() # 출력 없음
    linked_list.push(1)
    linked_list.print() # 1
    linked_list.push(2)
    linked_list.print() # 2 1
    linked_list.push(3)
    linked_list.print() # 3 2 1

    linked_list.delete(2)
    linked_list.print() # 3 1

 

 

 
 
 
반응형
반응형

2022.06.08 - [자료구조] - [자료구조] Linked List - (4) 노드 삽입 (at the front)

 

[자료구조] Linked List - (4) 노드 삽입 (at the front)

2022.06.07 - [자료구조] - [자료구조] Linked List - (3) 노드 삽입 (at the end) 본 글에서는 위의 글에 이어서 새로운 노드를 Linked List 맨 앞에 추가하는 방법을 다룸. (1) 새로운 노드를 Linked List 맨 앞..

wschoi.tistory.com

본 글에서는 Linked List에 삽입된 노드들의 데이터를 조회하는 메소드를 다룸.

Linked list에 연결된 노드들의 data를 조회 

1) head가 가리키는 노드부터 순차적으로 출력하기 위해 cur 변수를 사용해 head가 참조하는 노드를 같이 참조

cur = self.head

2) cur 변수가 가리키는 노드의 data 출력

print(cur.data, end=" ") # 3

3) cur이 가리키는 노드를 연결된 노드로 이동

cur = cur->next

4) cur이 가리키는 노드의 data 출력 

print(cur.data, end=" ") # 2

위 과정을 print 메소드로 묶어서 표현하면 아래와 같음

def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

최종 코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

	# linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.head = new_node
        else:
            self.tail.next = new_node
            
        self.tail = new_node

	# linked list 처음에 노드 추가
    def push(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.tail = new_node
        else:
            new_node.next = self.head
        
        self.head = new_node

    def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

if __name__ == "__main__":
    linked_list = LinkedList()
    linked_list.print() # 출력 없음
    linked_list.push(1)
    linked_list.print() # 1
    linked_list.push(2)
    linked_list.print() # 2 1
    linked_list.push(3)
    linked_list.print() # 3 2 1
반응형

+ Recent posts