Como lidar com atributos de objeto dinamicamente?

Em uma lição anterior, você aprendeu sobre atributos sendo as variáveis que pertencem a um objeto. Isso significa que eles armazenam dados que descrevem o estado ou o comportamento do objeto. Por exemplo, um carro normalmente teria uma marca e um modelo. A marca e o modelo poderiam ser atributos para uma classe Car:
class Car: 
    def __init__(self, brand, model): 
        self.brand = brand 
        self.model = model 

my_car = Car('Lamborghini', 'Gallardo') 
print(my_car.brand) # Lamborghini 
print(my_car.model) # Gallardo
Mas às vezes, você pode não saber quais atributos precisa até que seu programa esteja em execução. Imagine que você está escrevendo um script que recebe nomes de atributos de um usuário ou de um arquivo de configuração. Esses não são atributos que você pode codificar antecipadamente. É aí que entra o manuseio dinâmico de atributos. Dessa forma, você pode acessar, modificar, verificar ou até mesmo excluir atributos usando seus nomes como variáveis e não como nomes fixos no seu código. Isso dá ao seu programa a flexibilidade para responder a diferentes dados ou entradas do usuário em tempo real. Python oferece quatro funções internas práticas para trabalhar dinamicamente com atributos de objetos. Elas são getattr(), setattr(), hasattr() e delattr(). Eles permitem que você acesse, crie, verifique e remova atributos usando nomes de variáveis. Vamos dar uma olhada em cada um em ação. getattr() possibilita ler um atributo de um objeto quando você não sabe seu nome até o tempo de execução. Se o atributo não existir, ele gera um AttributeError, a menos que você forneça um valor padrão. Para usá-lo, você passa o objeto, o nome do atributo e um valor padrão opcional:
getattr(object, attribute_name, default_value)
Aqui está um exemplo:
class Person: 
    def __init__(self, name, age): 
        self.name = name 
        self.age = age 

person = Person('John Doe', 30) 
 
print(getattr(person, 'name')) # John Doe 
print(getattr(person, 'age')) # 30 
print(getattr(person, 'city', 'Milano')) # Milano
No exemplo acima, Milano é um valor padrão porque city não existe na classe Person. Como dissemos anteriormente, o verdadeiro poder de getattr() fica evidente quando o nome do atributo vem de uma variável, como de uma entrada do usuário ou de algum arquivo. Nesse caso, você não pode usar a sintaxe regular object.attribute_name porque o nome do atributo não é fixo.
class Person: 
    def __init__(self, name, age): 
        self.name = name 
        self.age = age 

person = Person('John Doe', 30)

attr_name = input('Enter the attribute you want to see: ')
print(getattr(person, attr_name, 'Attribute not found'))
Neste caso, se o usuário digitar name, ele verá John Doe e se digitar age, ele verá 30. E se digitar algo que não existe na classe como email, ele verá Attribute not found. É exatamente aqui que o manuseio dinâmico de atributos se destaca. Ele permite que seu código responda a entradas e dados que ele não viu antes. Além disso, você pode querer examinar todos os atributos que um objeto possui, não apenas os que você já conhece. A função embutida dir() permite que você faça isso. Ela retorna uma lista com todos os nomes de atributos do objeto. Veja como usá-la:
class Person: 
    def __init__(self, name, age): 
        self.name = name 
        self.age = age 

person = Person('John Doe', 30)

# Loop through all attributes of the person object with dir() function
for attr in dir(person):
    # Ignore dunder methods like __init__ or __str__ and regular methods
    if not attr.startswith('__') and not callable(getattr(person, attr)): 
        value = getattr(person, attr)
        print(f'{attr}: {value}')

# Output
# age: 30
# name: John Doe
No loop acima, callable() é uma função embutida que retorna True se o objeto passado a ela puder ser chamado como uma função ou método, e False caso contrário. Ao verificar not callable(getattr(person, attr)), o loop ignora os métodos e imprime apenas os atributos de dados como name e age. A função setattr() permite que você crie um novo atributo ou atualize um existente dinamicamente. A sintaxe é a seguinte:
setattr(object, attribute_name, value)
Aqui está um exemplo que define atributos de configuração com base em dados de algum arquivo de variável de configuração ou ambiente:
class Configuration:
    pass

# Data loaded at runtime (like from a config or env file)
settings_data = {
    'server_url': 'https://api.example.com',
    'timeout_sec': 30,
    'max_retries': 5
}

config_obj = Configuration()

# Dynamically set attributes using dictionary keys and values
for attr_name, attr_value in settings_data.items():
    setattr(config_obj, attr_name, attr_value)

print(config_obj.server_url) # https://api.example.com
print(config_obj.timeout_sec) # 30
Também existe hasattr(). Antes de fazer algo com um atributo ou excluí-lo, é uma boa prática verificar se ele existe. É isso que hasattr() permite fazer. Ele verifica se um atributo existe e retorna True ou False com base no resultado. Aqui está a sintaxe básica:
hasattr(object, attribute_name)
E aqui está um exemplo que verifica dinamicamente a existência de atributos em uma instância da classe Product:
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

product_a = Product('T-Shirt', 25)

required_attributes = ['name', 'price', 'inventory_id']

for attr in required_attributes:
    if not hasattr(product_a, attr):
        print(f"ERROR: Product is missing the required attribute: '{attr}'")
    else:
        # Access the attributes dynamically once their existence is confirmed
        print(f'{attr}: {getattr(product_a, attr)}')

# Output:
# name: T-Shirt
# price: 25
# ERROR: Product is missing the required attribute: 'inventory_id'
A saída ERROR ocorreu porque inventory_id está ausente da classe Product e de sua instância. Por fim, delattr() permite que você remova um atributo dinamicamente:
delattr(object, attribute_name)
Por exemplo, imagine que um objeto foi totalmente processado, então você decide limpar quaisquer atributos sensíveis ou temporários que possam existir antes de salvar a versão final. Depois disso, você pode usar dir() para percorrer os atributos restantes:
class UserSession:
    def __init__(self, user_id, token):
        self.user_id = user_id
        self.auth_token = token # sensitive
        self.temp_counter = 0 # temporary

session = UserSession(101, 'a1b2c3d4e5')

# List of attributes to remove dynamically before "saving" the session
attributes_to_clean = ['auth_token', 'temp_counter']

# Dynamically remove specified attributes
for attr in attributes_to_clean:
    if hasattr(session, attr):
        delattr(session, attr)
        print(f'Removed attribute: {attr}')

print('\nFinal attributes remaining:')

# Loop through the remaining attributes with dir()
for attr in dir(session):
    # Ignore dunder methods like __init__ or __str__ and regular methods
    if not attr.startswith('__') and not callable(getattr(session, attr)):
        print(f' - {attr}: {getattr(session, attr)}')

# Output:
# Removed attribute: auth_token
# Removed attribute: temp_counter

# Final attributes remaining:
#  - user_id: 101
E é assim que você pode manipular atributos dinamicamente!
Este módulo não possui perguntas. Marque como concluído.